diff --git a/osu.Android.props b/osu.Android.props
index 54469eec43..3a1e6ba9a3 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -52,6 +52,6 @@
-
+
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs
index 3e4995482d..fd6a9c7b7b 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs
@@ -174,8 +174,8 @@ namespace osu.Game.Rulesets.Catch.Tests
private void addToPlayfield(DrawableCatchHitObject drawable)
{
- foreach (var mod in SelectedMods.Value.OfType())
- mod.ApplyToDrawableHitObjects(new[] { drawable });
+ foreach (var mod in SelectedMods.Value.OfType())
+ mod.ApplyToDrawableHitObject(drawable);
drawableRuleset.Playfield.Add(drawable);
}
diff --git a/osu.Game.Rulesets.Catch/Skinning/Default/DefaultCatcher.cs b/osu.Game.Rulesets.Catch/Skinning/Default/DefaultCatcher.cs
index 364fc211a0..e423f21b98 100644
--- a/osu.Game.Rulesets.Catch/Skinning/Default/DefaultCatcher.cs
+++ b/osu.Game.Rulesets.Catch/Skinning/Default/DefaultCatcher.cs
@@ -12,12 +12,10 @@ using osu.Game.Rulesets.Catch.UI;
namespace osu.Game.Rulesets.Catch.Skinning.Default
{
- public class DefaultCatcher : CompositeDrawable, ICatcherSprite
+ public class DefaultCatcher : CompositeDrawable
{
public Bindable CurrentState { get; } = new Bindable();
- public Texture CurrentTexture => sprite.Texture;
-
private readonly Sprite sprite;
private readonly Dictionary textures = new Dictionary();
diff --git a/osu.Game.Rulesets.Catch/Skinning/ICatcherSprite.cs b/osu.Game.Rulesets.Catch/Skinning/ICatcherSprite.cs
deleted file mode 100644
index 073868e947..0000000000
--- a/osu.Game.Rulesets.Catch/Skinning/ICatcherSprite.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using osu.Framework.Graphics.Textures;
-
-namespace osu.Game.Rulesets.Catch.Skinning
-{
- public interface ICatcherSprite
- {
- Texture CurrentTexture { get; }
- }
-}
diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatcherNew.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatcherNew.cs
index 2bf8b28aa2..9df87c92ea 100644
--- a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatcherNew.cs
+++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatcherNew.cs
@@ -9,21 +9,17 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Animations;
using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Sprites;
-using osu.Framework.Graphics.Textures;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Rulesets.Catch.Skinning.Legacy
{
- public class LegacyCatcherNew : CompositeDrawable, ICatcherSprite
+ public class LegacyCatcherNew : CompositeDrawable
{
[Resolved]
private Bindable currentState { get; set; }
- public Texture CurrentTexture => (currentDrawable as TextureAnimation)?.CurrentFrame ?? (currentDrawable as Sprite)?.Texture;
-
private readonly Dictionary drawables = new Dictionary();
private Drawable currentDrawable;
diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatcherOld.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatcherOld.cs
index a8948d2ed0..3e679171b2 100644
--- a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatcherOld.cs
+++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatcherOld.cs
@@ -3,19 +3,14 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
-using osu.Framework.Graphics.Animations;
using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Sprites;
-using osu.Framework.Graphics.Textures;
using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Rulesets.Catch.Skinning.Legacy
{
- public class LegacyCatcherOld : CompositeDrawable, ICatcherSprite
+ public class LegacyCatcherOld : CompositeDrawable
{
- public Texture CurrentTexture => (InternalChild as TextureAnimation)?.CurrentFrame ?? (InternalChild as Sprite)?.Texture;
-
public LegacyCatcherOld()
{
RelativeSizeAxes = Axes.Both;
diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs
index ee2986c73c..1f01dbabb5 100644
--- a/osu.Game.Rulesets.Catch/UI/Catcher.cs
+++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs
@@ -9,7 +9,6 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Pooling;
-using osu.Framework.Graphics.Textures;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
@@ -17,7 +16,6 @@ using osu.Game.Rulesets.Catch.Judgements;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Catch.Skinning;
-using osu.Game.Rulesets.Catch.Skinning.Default;
using osu.Game.Rulesets.Judgements;
using osu.Game.Skinning;
using osuTK;
@@ -83,18 +81,17 @@ namespace osu.Game.Rulesets.Catch.UI
///
private readonly Container droppedObjectTarget;
- [Cached]
- protected readonly Bindable CurrentStateBindable = new Bindable();
-
- public CatcherAnimationState CurrentState => CurrentStateBindable.Value;
+ public CatcherAnimationState CurrentState
+ {
+ get => body.AnimationState.Value;
+ private set => body.AnimationState.Value = value;
+ }
///
/// The width of the catcher which can receive fruit. Equivalent to "catchMargin" in osu-stable.
///
public const float ALLOWED_CATCH_RANGE = 0.8f;
- internal Texture CurrentTexture => ((ICatcherSprite)currentCatcher.Drawable).CurrentTexture;
-
private bool dashing;
public bool Dashing
@@ -121,7 +118,7 @@ namespace osu.Game.Rulesets.Catch.UI
///
private readonly float catchWidth;
- private readonly SkinnableDrawable currentCatcher;
+ private readonly SkinnableCatcher body;
private Color4 hyperDashColour = DEFAULT_HYPER_DASH_COLOUR;
private Color4 hyperDashEndGlowColour = DEFAULT_HYPER_DASH_COLOUR;
@@ -161,13 +158,7 @@ namespace osu.Game.Rulesets.Catch.UI
Anchor = Anchor.TopCentre,
Origin = Anchor.BottomCentre,
},
- currentCatcher = new SkinnableDrawable(
- new CatchSkinComponent(CatchSkinComponents.Catcher),
- _ => new DefaultCatcher())
- {
- Anchor = Anchor.TopCentre,
- OriginPosition = new Vector2(0.5f, 0.06f) * CatcherArea.CATCHER_SIZE
- },
+ body = new SkinnableCatcher(),
hitExplosionContainer = new HitExplosionContainer
{
Anchor = Anchor.TopCentre,
@@ -268,17 +259,16 @@ namespace osu.Game.Rulesets.Catch.UI
SetHyperDashState();
if (result.IsHit)
- updateState(hitObject.Kiai ? CatcherAnimationState.Kiai : CatcherAnimationState.Idle);
+ CurrentState = hitObject.Kiai ? CatcherAnimationState.Kiai : CatcherAnimationState.Idle;
else if (!(hitObject is Banana))
- updateState(CatcherAnimationState.Fail);
+ CurrentState = CatcherAnimationState.Fail;
}
public void OnRevertResult(DrawableCatchHitObject drawableObject, JudgementResult result)
{
var catchResult = (CatchJudgementResult)result;
- if (CurrentState != catchResult.CatcherAnimationState)
- updateState(catchResult.CatcherAnimationState);
+ CurrentState = catchResult.CatcherAnimationState;
if (HyperDashing != catchResult.CatcherHyperDash)
{
@@ -373,14 +363,6 @@ namespace osu.Game.Rulesets.Catch.UI
}
}
- private void updateState(CatcherAnimationState state)
- {
- if (CurrentState == state)
- return;
-
- CurrentStateBindable.Value = state;
- }
-
private void placeCaughtObject(DrawablePalpableCatchHitObject drawableObject, Vector2 position)
{
var caughtObject = getCaughtObject(drawableObject.HitObject);
diff --git a/osu.Game.Rulesets.Catch/UI/CatcherTrail.cs b/osu.Game.Rulesets.Catch/UI/CatcherTrail.cs
new file mode 100644
index 0000000000..80522ab36b
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/UI/CatcherTrail.cs
@@ -0,0 +1,43 @@
+// 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.Pooling;
+using osu.Framework.Timing;
+using osuTK;
+
+namespace osu.Game.Rulesets.Catch.UI
+{
+ ///
+ /// A trail of the catcher.
+ /// It also represents a hyper dash afterimage.
+ ///
+ public class CatcherTrail : PoolableDrawable
+ {
+ public CatcherAnimationState AnimationState
+ {
+ set => body.AnimationState.Value = value;
+ }
+
+ private readonly SkinnableCatcher body;
+
+ public CatcherTrail()
+ {
+ Size = new Vector2(CatcherArea.CATCHER_SIZE);
+ Origin = Anchor.TopCentre;
+ Blending = BlendingParameters.Additive;
+ InternalChild = body = new SkinnableCatcher
+ {
+ // Using a frozen clock because trails should not be animated when the skin has an animated catcher.
+ // TODO: The animation should be frozen at the animation frame at the time of the trail generation.
+ Clock = new FramedClock(new ManualClock()),
+ };
+ }
+
+ protected override void FreeAfterUse()
+ {
+ ClearTransforms();
+ base.FreeAfterUse();
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs b/osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs
index 0aef215797..7e4a5b6a86 100644
--- a/osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs
@@ -19,11 +19,11 @@ namespace osu.Game.Rulesets.Catch.UI
{
private readonly Catcher catcher;
- private readonly DrawablePool trailPool;
+ private readonly DrawablePool trailPool;
- private readonly Container dashTrails;
- private readonly Container hyperDashTrails;
- private readonly Container endGlowSprites;
+ private readonly Container dashTrails;
+ private readonly Container hyperDashTrails;
+ private readonly Container endGlowSprites;
private Color4 hyperDashTrailsColour = Catcher.DEFAULT_HYPER_DASH_COLOUR;
@@ -83,10 +83,10 @@ namespace osu.Game.Rulesets.Catch.UI
InternalChildren = new Drawable[]
{
- trailPool = new DrawablePool(30),
- dashTrails = new Container { RelativeSizeAxes = Axes.Both },
- hyperDashTrails = new Container { RelativeSizeAxes = Axes.Both, Colour = Catcher.DEFAULT_HYPER_DASH_COLOUR },
- endGlowSprites = new Container { RelativeSizeAxes = Axes.Both, Colour = Catcher.DEFAULT_HYPER_DASH_COLOUR },
+ trailPool = new DrawablePool(30),
+ dashTrails = new Container { RelativeSizeAxes = Axes.Both },
+ hyperDashTrails = new Container { RelativeSizeAxes = Axes.Both, Colour = Catcher.DEFAULT_HYPER_DASH_COLOUR },
+ endGlowSprites = new Container { RelativeSizeAxes = Axes.Both, Colour = Catcher.DEFAULT_HYPER_DASH_COLOUR },
};
}
@@ -116,15 +116,12 @@ namespace osu.Game.Rulesets.Catch.UI
Scheduler.AddDelayed(displayTrail, catcher.HyperDashing ? 25 : 50);
}
- private CatcherTrailSprite createTrailSprite(Container target)
+ private CatcherTrail createTrailSprite(Container target)
{
- CatcherTrailSprite sprite = trailPool.Get();
+ CatcherTrail sprite = trailPool.Get();
- sprite.Texture = catcher.CurrentTexture;
- sprite.Anchor = catcher.Anchor;
+ sprite.AnimationState = catcher.CurrentState;
sprite.Scale = catcher.Scale;
- sprite.Blending = BlendingParameters.Additive;
- sprite.RelativePositionAxes = catcher.RelativePositionAxes;
sprite.Position = catcher.Position;
target.Add(sprite);
diff --git a/osu.Game.Rulesets.Catch/UI/CatcherTrailSprite.cs b/osu.Game.Rulesets.Catch/UI/CatcherTrailSprite.cs
deleted file mode 100644
index 0e3e409fac..0000000000
--- a/osu.Game.Rulesets.Catch/UI/CatcherTrailSprite.cs
+++ /dev/null
@@ -1,40 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Pooling;
-using osu.Framework.Graphics.Sprites;
-using osu.Framework.Graphics.Textures;
-using osuTK;
-
-namespace osu.Game.Rulesets.Catch.UI
-{
- public class CatcherTrailSprite : PoolableDrawable
- {
- public Texture Texture
- {
- set => sprite.Texture = value;
- }
-
- private readonly Sprite sprite;
-
- public CatcherTrailSprite()
- {
- InternalChild = sprite = new Sprite
- {
- RelativeSizeAxes = Axes.Both
- };
-
- Size = new Vector2(CatcherArea.CATCHER_SIZE);
-
- // Sets the origin roughly to the centre of the catcher's plate to allow for correct scaling.
- OriginPosition = new Vector2(0.5f, 0.06f) * CatcherArea.CATCHER_SIZE;
- }
-
- protected override void FreeAfterUse()
- {
- ClearTransforms();
- base.FreeAfterUse();
- }
- }
-}
diff --git a/osu.Game.Rulesets.Catch/UI/SkinnableCatcher.cs b/osu.Game.Rulesets.Catch/UI/SkinnableCatcher.cs
new file mode 100644
index 0000000000..fc34ba4c8b
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/UI/SkinnableCatcher.cs
@@ -0,0 +1,33 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Game.Rulesets.Catch.Skinning.Default;
+using osu.Game.Skinning;
+using osuTK;
+
+namespace osu.Game.Rulesets.Catch.UI
+{
+ ///
+ /// The visual representation of the .
+ /// It includes the body part of the catcher and the catcher plate.
+ ///
+ public class SkinnableCatcher : SkinnableDrawable
+ {
+ ///
+ /// This is used by skin elements to determine which texture of the catcher is used.
+ ///
+ [Cached]
+ public readonly Bindable AnimationState = new Bindable();
+
+ public SkinnableCatcher()
+ : base(new CatchSkinComponent(CatchSkinComponents.Catcher), _ => new DefaultCatcher())
+ {
+ Anchor = Anchor.TopCentre;
+ // Sets the origin roughly to the centre of the catcher's plate to allow for correct scaling.
+ OriginPosition = new Vector2(0.5f, 0.06f) * CatcherArea.CATCHER_SIZE;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/ManiaSelectionBlueprintTestScene.cs b/osu.Game.Rulesets.Mania.Tests/Editor/ManiaSelectionBlueprintTestScene.cs
index 176fbba921..124e1a35f9 100644
--- a/osu.Game.Rulesets.Mania.Tests/Editor/ManiaSelectionBlueprintTestScene.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Editor/ManiaSelectionBlueprintTestScene.cs
@@ -1,31 +1,54 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
-using osu.Framework.Timing;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.UI;
+using osu.Game.Rulesets.UI;
+using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Tests.Visual;
-using osuTK.Graphics;
namespace osu.Game.Rulesets.Mania.Tests.Editor
{
public abstract class ManiaSelectionBlueprintTestScene : SelectionBlueprintTestScene
{
- [Cached(Type = typeof(IAdjustableClock))]
- private readonly IAdjustableClock clock = new StopwatchClock();
+ protected override Container Content => blueprints ?? base.Content;
- protected ManiaSelectionBlueprintTestScene()
+ private readonly Container blueprints;
+
+ [Cached(typeof(Playfield))]
+ public Playfield Playfield { get; }
+
+ private readonly ScrollingTestContainer scrollingTestContainer;
+
+ protected ScrollingDirection Direction
{
- Add(new Column(0)
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- AccentColour = Color4.OrangeRed,
- Clock = new FramedClock(new StopwatchClock()), // No scroll
- });
+ set => scrollingTestContainer.Direction = value;
}
- public ManiaPlayfield Playfield => null;
+ protected ManiaSelectionBlueprintTestScene(int columns)
+ {
+ var stageDefinitions = new List { new StageDefinition { Columns = columns } };
+ base.Content.Child = scrollingTestContainer = new ScrollingTestContainer(ScrollingDirection.Up)
+ {
+ RelativeSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ Playfield = new ManiaPlayfield(stageDefinitions)
+ {
+ RelativeSizeAxes = Axes.Both,
+ },
+ blueprints = new Container
+ {
+ RelativeSizeAxes = Axes.Both
+ }
+ }
+ };
+
+ AddToggleStep("Downward scroll", b => Direction = b ? ScrollingDirection.Down : ScrollingDirection.Up);
+ }
}
}
diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneHoldNoteSelectionBlueprint.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneHoldNoteSelectionBlueprint.cs
index 5e99264d7d..9953b8e3c0 100644
--- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneHoldNoteSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneHoldNoteSelectionBlueprint.cs
@@ -1,55 +1,32 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
-using osu.Game.Graphics;
using osu.Game.Rulesets.Mania.Edit.Blueprints;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Objects.Drawables;
-using osu.Game.Rulesets.UI.Scrolling;
-using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Mania.Tests.Editor
{
public class TestSceneHoldNoteSelectionBlueprint : ManiaSelectionBlueprintTestScene
{
- private readonly DrawableHoldNote drawableObject;
-
- protected override Container Content => content ?? base.Content;
- private readonly Container content;
-
public TestSceneHoldNoteSelectionBlueprint()
+ : base(4)
{
- var holdNote = new HoldNote { Column = 0, Duration = 1000 };
- holdNote.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
-
- base.Content.Child = content = new ScrollingTestContainer(ScrollingDirection.Down)
+ for (int i = 0; i < 4; i++)
{
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- AutoSizeAxes = Axes.Y,
- Width = 50,
- Child = drawableObject = new DrawableHoldNote(holdNote)
+ var holdNote = new HoldNote
{
- Height = 300,
- AccentColour = { Value = OsuColour.Gray(0.3f) }
- }
- };
+ Column = i,
+ StartTime = i * 100,
+ Duration = 500
+ };
+ holdNote.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
- AddBlueprint(new HoldNoteSelectionBlueprint(holdNote), drawableObject);
- }
-
- protected override void Update()
- {
- base.Update();
-
- foreach (var nested in drawableObject.NestedHitObjects)
- {
- double finalPosition = (nested.HitObject.StartTime - drawableObject.HitObject.StartTime) / drawableObject.HitObject.Duration;
- nested.Y = (float)(-finalPosition * content.DrawHeight);
+ var drawableHitObject = new DrawableHoldNote(holdNote);
+ Playfield.Add(drawableHitObject);
+ AddBlueprint(new HoldNoteSelectionBlueprint(holdNote), drawableHitObject);
}
}
}
diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs
index 8474279b01..01d80881fa 100644
--- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs
@@ -12,7 +12,7 @@ using osu.Framework.Utils;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Edit;
-using osu.Game.Rulesets.Mania.Edit.Blueprints;
+using osu.Game.Rulesets.Mania.Edit.Blueprints.Components;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Mania.Skinning.Default;
@@ -184,8 +184,8 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
AddAssert("head note positioned correctly", () => Precision.AlmostEquals(holdNote.ScreenSpaceDrawQuad.BottomLeft, holdNote.Head.ScreenSpaceDrawQuad.BottomLeft));
AddAssert("tail note positioned correctly", () => Precision.AlmostEquals(holdNote.ScreenSpaceDrawQuad.TopLeft, holdNote.Tail.ScreenSpaceDrawQuad.BottomLeft));
- AddAssert("head blueprint positioned correctly", () => this.ChildrenOfType().ElementAt(0).DrawPosition == holdNote.Head.DrawPosition);
- AddAssert("tail blueprint positioned correctly", () => this.ChildrenOfType().ElementAt(1).DrawPosition == holdNote.Tail.DrawPosition);
+ AddAssert("head blueprint positioned correctly", () => this.ChildrenOfType().ElementAt(0).DrawPosition == holdNote.Head.DrawPosition);
+ AddAssert("tail blueprint positioned correctly", () => this.ChildrenOfType().ElementAt(1).DrawPosition == holdNote.Tail.DrawPosition);
}
private void setScrollStep(ScrollingDirection direction)
diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneNoteSelectionBlueprint.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneNoteSelectionBlueprint.cs
index 9c3ad0b4ff..3586eecc44 100644
--- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneNoteSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneNoteSelectionBlueprint.cs
@@ -1,40 +1,32 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Mania.Edit.Blueprints;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Objects.Drawables;
-using osu.Game.Rulesets.UI.Scrolling;
-using osu.Game.Tests.Visual;
-using osuTK;
namespace osu.Game.Rulesets.Mania.Tests.Editor
{
public class TestSceneNoteSelectionBlueprint : ManiaSelectionBlueprintTestScene
{
- protected override Container Content => content ?? base.Content;
- private readonly Container content;
-
public TestSceneNoteSelectionBlueprint()
+ : base(4)
{
- var note = new Note { Column = 0 };
- note.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
-
- DrawableNote drawableObject;
-
- base.Content.Child = content = new ScrollingTestContainer(ScrollingDirection.Down)
+ for (int i = 0; i < 4; i++)
{
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Size = new Vector2(50, 20),
- Child = drawableObject = new DrawableNote(note)
- };
+ var note = new Note
+ {
+ Column = i,
+ StartTime = i * 200,
+ };
+ note.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
- AddBlueprint(new NoteSelectionBlueprint(note), drawableObject);
+ var drawableHitObject = new DrawableNote(note);
+ Playfield.Add(drawableHitObject);
+ AddBlueprint(new NoteSelectionBlueprint(note), drawableHitObject);
+ }
}
}
}
diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteNoteOverlay.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteNoteOverlay.cs
deleted file mode 100644
index 6933571be8..0000000000
--- a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteNoteOverlay.cs
+++ /dev/null
@@ -1,43 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
-using osu.Game.Rulesets.Mania.Edit.Blueprints.Components;
-using osu.Game.Rulesets.Mania.Objects.Drawables;
-
-namespace osu.Game.Rulesets.Mania.Edit.Blueprints
-{
- public class HoldNoteNoteOverlay : CompositeDrawable
- {
- private readonly HoldNoteSelectionBlueprint holdNoteBlueprint;
- private readonly HoldNotePosition position;
-
- public HoldNoteNoteOverlay(HoldNoteSelectionBlueprint holdNoteBlueprint, HoldNotePosition position)
- {
- this.holdNoteBlueprint = holdNoteBlueprint;
- this.position = position;
-
- InternalChild = new EditNotePiece { RelativeSizeAxes = Axes.X };
- }
-
- protected override void Update()
- {
- base.Update();
-
- var drawableObject = holdNoteBlueprint.DrawableObject;
-
- // Todo: This shouldn't exist, mania should not reference the drawable hitobject directly.
- if (drawableObject.IsLoaded)
- {
- DrawableNote note = position == HoldNotePosition.Start ? (DrawableNote)drawableObject.Head : drawableObject.Tail;
-
- Anchor = note.Anchor;
- Origin = note.Origin;
-
- Size = note.DrawSize;
- Position = note.DrawPosition;
- }
- }
- }
-}
diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePosition.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePosition.cs
deleted file mode 100644
index 219dad566d..0000000000
--- a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePosition.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-namespace osu.Game.Rulesets.Mania.Edit.Blueprints
-{
- public enum HoldNotePosition
- {
- Start,
- End
- }
-}
diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs
index d04c5cd4aa..5259fcbd5f 100644
--- a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs
@@ -2,14 +2,13 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
-using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
+using osu.Game.Rulesets.Mania.Edit.Blueprints.Components;
using osu.Game.Rulesets.Mania.Objects;
-using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.UI.Scrolling;
using osuTK;
@@ -17,13 +16,12 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
{
public class HoldNoteSelectionBlueprint : ManiaSelectionBlueprint
{
- public new DrawableHoldNote DrawableObject => (DrawableHoldNote)base.DrawableObject;
-
- private readonly IBindable direction = new Bindable();
-
[Resolved]
private OsuColour colours { get; set; }
+ private EditNotePiece head;
+ private EditNotePiece tail;
+
public HoldNoteSelectionBlueprint(HoldNote hold)
: base(hold)
{
@@ -32,12 +30,10 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
[BackgroundDependencyLoader]
private void load(IScrollingInfo scrollingInfo)
{
- direction.BindTo(scrollingInfo.Direction);
-
InternalChildren = new Drawable[]
{
- new HoldNoteNoteOverlay(this, HoldNotePosition.Start),
- new HoldNoteNoteOverlay(this, HoldNotePosition.End),
+ head = new EditNotePiece { RelativeSizeAxes = Axes.X },
+ tail = new EditNotePiece { RelativeSizeAxes = Axes.X },
new Container
{
RelativeSizeAxes = Axes.Both,
@@ -58,21 +54,13 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
{
base.Update();
- // Todo: This shouldn't exist, mania should not reference the drawable hitobject directly.
- if (DrawableObject.IsLoaded)
- {
- Size = DrawableObject.DrawSize + new Vector2(0, DrawableObject.Tail.DrawHeight);
-
- // This is a side-effect of not matching the hitobject's anchors/origins, which is kinda hard to do
- // When scrolling upwards our origin is already at the top of the head note (which is the intended location),
- // but when scrolling downwards our origin is at the _bottom_ of the tail note (where we need to be at the _top_ of the tail note)
- if (direction.Value == ScrollingDirection.Down)
- Y -= DrawableObject.Tail.DrawHeight;
- }
+ head.Y = HitObjectContainer.PositionAtTime(HitObject.Head.StartTime, HitObject.StartTime);
+ tail.Y = HitObjectContainer.PositionAtTime(HitObject.Tail.StartTime, HitObject.StartTime);
+ Height = HitObjectContainer.LengthAtTime(HitObject.StartTime, HitObject.EndTime) + tail.DrawHeight;
}
public override Quad SelectionQuad => ScreenSpaceDrawQuad;
- public override Vector2 ScreenSpaceSelectionPoint => DrawableObject.Head.ScreenSpaceDrawQuad.Centre;
+ public override Vector2 ScreenSpaceSelectionPoint => head.ScreenSpaceDrawQuad.Centre;
}
}
diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs
index e744bd3c83..955336db57 100644
--- a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs
@@ -5,20 +5,23 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Objects;
-using osu.Game.Rulesets.Mania.Objects.Drawables;
+using osu.Game.Rulesets.Mania.UI;
+using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
-using osuTK;
namespace osu.Game.Rulesets.Mania.Edit.Blueprints
{
public abstract class ManiaSelectionBlueprint : HitObjectSelectionBlueprint
where T : ManiaHitObject
{
- public new DrawableManiaHitObject DrawableObject => (DrawableManiaHitObject)base.DrawableObject;
+ [Resolved]
+ private Playfield playfield { get; set; }
[Resolved]
private IScrollingInfo scrollingInfo { get; set; }
+ protected ScrollingHitObjectContainer HitObjectContainer => ((ManiaPlayfield)playfield).GetColumn(HitObject.Column).HitObjectContainer;
+
protected ManiaSelectionBlueprint(T hitObject)
: base(hitObject)
{
@@ -29,19 +32,13 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
{
base.Update();
- Position = Parent.ToLocalSpace(DrawableObject.ToScreenSpace(Vector2.Zero));
- }
+ var anchor = scrollingInfo.Direction.Value == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre;
+ Anchor = Origin = anchor;
+ foreach (var child in InternalChildren)
+ child.Anchor = child.Origin = anchor;
- public override void Show()
- {
- DrawableObject.AlwaysAlive = true;
- base.Show();
- }
-
- public override void Hide()
- {
- DrawableObject.AlwaysAlive = false;
- base.Hide();
+ Position = Parent.ToLocalSpace(HitObjectContainer.ScreenSpacePositionAtTime(HitObject.StartTime)) - AnchorPosition;
+ Width = HitObjectContainer.DrawWidth;
}
}
}
diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/NoteSelectionBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/NoteSelectionBlueprint.cs
index e2b6ee0048..e7a03905d2 100644
--- a/osu.Game.Rulesets.Mania/Edit/Blueprints/NoteSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/NoteSelectionBlueprint.cs
@@ -14,14 +14,5 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
{
AddInternal(new EditNotePiece { RelativeSizeAxes = Axes.X });
}
-
- protected override void Update()
- {
- base.Update();
-
- // Todo: This shouldn't exist, mania should not reference the drawable hitobject directly.
- if (DrawableObject.IsLoaded)
- Size = DrawableObject.DrawSize;
- }
}
}
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs
index 380ab35339..3ec68bfb56 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs
@@ -85,63 +85,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
AccentColour.UnbindFrom(ParentHitObject.AccentColour);
}
- private double computedLifetimeStart;
-
- public override double LifetimeStart
- {
- get => base.LifetimeStart;
- set
- {
- computedLifetimeStart = value;
-
- if (!AlwaysAlive)
- base.LifetimeStart = value;
- }
- }
-
- private double computedLifetimeEnd;
-
- public override double LifetimeEnd
- {
- get => base.LifetimeEnd;
- set
- {
- computedLifetimeEnd = value;
-
- if (!AlwaysAlive)
- base.LifetimeEnd = value;
- }
- }
-
- private bool alwaysAlive;
-
- ///
- /// Whether this should always remain alive.
- ///
- internal bool AlwaysAlive
- {
- get => alwaysAlive;
- set
- {
- if (alwaysAlive == value)
- return;
-
- alwaysAlive = value;
-
- if (value)
- {
- // Set the base lifetimes directly, to avoid mangling the computed lifetimes
- base.LifetimeStart = double.MinValue;
- base.LifetimeEnd = double.MaxValue;
- }
- else
- {
- LifetimeStart = computedLifetimeStart;
- LifetimeEnd = computedLifetimeEnd;
- }
- }
- }
-
protected virtual void OnDirectionChanged(ValueChangedEvent e)
{
Anchor = Origin = e.NewValue == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre;
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs
index 58e46b6687..f6e8a771ed 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs
@@ -21,20 +21,37 @@ namespace osu.Game.Rulesets.Osu.Tests
private int depthIndex;
[Test]
- public void TestVariousHitCircles()
+ public void TestHits()
+ {
+ AddStep("Hit Big Single", () => SetContents(_ => testSingle(2, true)));
+ AddStep("Hit Medium Single", () => SetContents(_ => testSingle(5, true)));
+ AddStep("Hit Small Single", () => SetContents(_ => testSingle(7, true)));
+ AddStep("Hit Big Stream", () => SetContents(_ => testStream(2, true)));
+ AddStep("Hit Medium Stream", () => SetContents(_ => testStream(5, true)));
+ AddStep("Hit Small Stream", () => SetContents(_ => testStream(7, true)));
+ }
+
+ [Test]
+ public void TestHittingEarly()
+ {
+ AddStep("Hit stream early", () => SetContents(_ => testStream(5, true, -150)));
+ }
+
+ [Test]
+ public void TestMisses()
{
AddStep("Miss Big Single", () => SetContents(_ => testSingle(2)));
AddStep("Miss Medium Single", () => SetContents(_ => testSingle(5)));
AddStep("Miss Small Single", () => SetContents(_ => testSingle(7)));
- AddStep("Hit Big Single", () => SetContents(_ => testSingle(2, true)));
- AddStep("Hit Medium Single", () => SetContents(_ => testSingle(5, true)));
- AddStep("Hit Small Single", () => SetContents(_ => testSingle(7, true)));
AddStep("Miss Big Stream", () => SetContents(_ => testStream(2)));
AddStep("Miss Medium Stream", () => SetContents(_ => testStream(5)));
AddStep("Miss Small Stream", () => SetContents(_ => testStream(7)));
- AddStep("Hit Big Stream", () => SetContents(_ => testStream(2, true)));
- AddStep("Hit Medium Stream", () => SetContents(_ => testStream(5, true)));
- AddStep("Hit Small Stream", () => SetContents(_ => testStream(7, true)));
+ }
+
+ [Test]
+ public void TestHittingLate()
+ {
+ AddStep("Hit stream late", () => SetContents(_ => testStream(5, true, 150)));
}
private Drawable testSingle(float circleSize, bool auto = false, double timeOffset = 0, Vector2? positionOffset = null)
@@ -46,7 +63,7 @@ namespace osu.Game.Rulesets.Osu.Tests
return playfield;
}
- private Drawable testStream(float circleSize, bool auto = false)
+ private Drawable testStream(float circleSize, bool auto = false, double hitOffset = 0)
{
var playfield = new TestOsuPlayfield();
@@ -54,14 +71,14 @@ namespace osu.Game.Rulesets.Osu.Tests
for (int i = 0; i <= 1000; i += 100)
{
- playfield.Add(createSingle(circleSize, auto, i, pos));
+ playfield.Add(createSingle(circleSize, auto, i, pos, hitOffset));
pos.X += 50;
}
return playfield;
}
- private TestDrawableHitCircle createSingle(float circleSize, bool auto, double timeOffset, Vector2? positionOffset)
+ private TestDrawableHitCircle createSingle(float circleSize, bool auto, double timeOffset, Vector2? positionOffset, double hitOffset = 0)
{
positionOffset ??= Vector2.Zero;
@@ -73,14 +90,14 @@ namespace osu.Game.Rulesets.Osu.Tests
circle.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { CircleSize = circleSize });
- var drawable = CreateDrawableHitCircle(circle, auto);
+ var drawable = CreateDrawableHitCircle(circle, auto, hitOffset);
- foreach (var mod in SelectedMods.Value.OfType())
- mod.ApplyToDrawableHitObjects(new[] { drawable });
+ foreach (var mod in SelectedMods.Value.OfType())
+ mod.ApplyToDrawableHitObject(drawable);
return drawable;
}
- protected virtual TestDrawableHitCircle CreateDrawableHitCircle(HitCircle circle, bool auto) => new TestDrawableHitCircle(circle, auto)
+ protected virtual TestDrawableHitCircle CreateDrawableHitCircle(HitCircle circle, bool auto, double hitOffset = 0) => new TestDrawableHitCircle(circle, auto, hitOffset)
{
Depth = depthIndex++
};
@@ -88,18 +105,20 @@ namespace osu.Game.Rulesets.Osu.Tests
protected class TestDrawableHitCircle : DrawableHitCircle
{
private readonly bool auto;
+ private readonly double hitOffset;
- public TestDrawableHitCircle(HitCircle h, bool auto)
+ public TestDrawableHitCircle(HitCircle h, bool auto, double hitOffset)
: base(h)
{
this.auto = auto;
+ this.hitOffset = hitOffset;
}
- public void TriggerJudgement() => UpdateResult(true);
+ public void TriggerJudgement() => Schedule(() => UpdateResult(true));
protected override void CheckForResult(bool userTriggered, double timeOffset)
{
- if (auto && !userTriggered && timeOffset > 0)
+ if (auto && !userTriggered && timeOffset > hitOffset && CheckHittable?.Invoke(this, Time.Current) != false)
{
// force success
ApplyResult(r => r.Type = HitResult.Great);
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleComboChange.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleComboChange.cs
index 5695462859..ff600172d2 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleComboChange.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleComboChange.cs
@@ -16,11 +16,11 @@ namespace osu.Game.Rulesets.Osu.Tests
Scheduler.AddDelayed(() => comboIndex.Value++, 250, true);
}
- protected override TestDrawableHitCircle CreateDrawableHitCircle(HitCircle circle, bool auto)
+ protected override TestDrawableHitCircle CreateDrawableHitCircle(HitCircle circle, bool auto, double hitOffset = 0)
{
circle.ComboIndexBindable.BindTo(comboIndex);
circle.IndexInCurrentComboBindable.BindTo(comboIndex);
- return base.CreateDrawableHitCircle(circle, auto);
+ return base.CreateDrawableHitCircle(circle, auto, hitOffset);
}
}
}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneShaking.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneShaking.cs
index 7e973d0971..43900c9a5c 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneShaking.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneShaking.cs
@@ -26,9 +26,9 @@ namespace osu.Game.Rulesets.Osu.Tests
return base.CreateBeatmapForSkinProvider();
}
- protected override TestDrawableHitCircle CreateDrawableHitCircle(HitCircle circle, bool auto)
+ protected override TestDrawableHitCircle CreateDrawableHitCircle(HitCircle circle, bool auto, double hitOffset = 0)
{
- var drawableHitObject = base.CreateDrawableHitCircle(circle, auto);
+ var drawableHitObject = base.CreateDrawableHitCircle(circle, auto, hitOffset);
Debug.Assert(drawableHitObject.HitObject.HitWindows != null);
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs
index fc5fcf2358..81902c25af 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs
@@ -335,8 +335,8 @@ namespace osu.Game.Rulesets.Osu.Tests
var drawable = CreateDrawableSlider(slider);
- foreach (var mod in SelectedMods.Value.OfType())
- mod.ApplyToDrawableHitObjects(new[] { drawable });
+ foreach (var mod in SelectedMods.Value.OfType())
+ mod.ApplyToDrawableHitObject(drawable);
drawable.OnNewResult += onNewResult;
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs
index b21b7a6f4a..2dea9837f3 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs
@@ -85,8 +85,8 @@ namespace osu.Game.Rulesets.Osu.Tests
Scale = new Vector2(0.75f)
};
- foreach (var mod in SelectedMods.Value.OfType())
- mod.ApplyToDrawableHitObjects(new[] { drawableSpinner });
+ foreach (var mod in SelectedMods.Value.OfType())
+ mod.ApplyToDrawableHitObject(drawableSpinner);
return drawableSpinner;
}
diff --git a/osu.Game.Rulesets.Osu/Mods/IMutateApproachCircles.cs b/osu.Game.Rulesets.Osu/Mods/IMutateApproachCircles.cs
new file mode 100644
index 0000000000..60a5825241
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Mods/IMutateApproachCircles.cs
@@ -0,0 +1,12 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+namespace osu.Game.Rulesets.Osu.Mods
+{
+ ///
+ /// Any mod which affects the animation or visibility of approach circles. Should be used for incompatibility purposes.
+ ///
+ public interface IMutateApproachCircles
+ {
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModApproachDifferent.cs b/osu.Game.Rulesets.Osu/Mods/OsuModApproachDifferent.cs
index 3e638c4833..526e29ad53 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModApproachDifferent.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModApproachDifferent.cs
@@ -1,9 +1,8 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using System.Collections.Generic;
+using System;
using osu.Framework.Bindables;
-using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Game.Configuration;
@@ -13,7 +12,7 @@ using osu.Game.Rulesets.Osu.Objects.Drawables;
namespace osu.Game.Rulesets.Osu.Mods
{
- public class OsuModApproachDifferent : Mod, IApplicableToDrawableHitObjects
+ public class OsuModApproachDifferent : Mod, IApplicableToDrawableHitObject, IMutateApproachCircles
{
public override string Name => "Approach Different";
public override string Acronym => "AD";
@@ -21,6 +20,8 @@ namespace osu.Game.Rulesets.Osu.Mods
public override double ScoreMultiplier => 1;
public override IconUsage? Icon { get; } = FontAwesome.Regular.Circle;
+ public override Type[] IncompatibleMods => new[] { typeof(IMutateApproachCircles) };
+
[SettingSource("Initial size", "Change the initial size of the approach circle, relative to hit circles.", 0)]
public BindableFloat Scale { get; } = new BindableFloat(4)
{
@@ -32,22 +33,19 @@ namespace osu.Game.Rulesets.Osu.Mods
[SettingSource("Style", "Change the animation style of the approach circles.", 1)]
public Bindable Style { get; } = new Bindable();
- public void ApplyToDrawableHitObjects(IEnumerable drawables)
+ public void ApplyToDrawableHitObject(DrawableHitObject drawable)
{
- drawables.ForEach(drawable =>
+ drawable.ApplyCustomUpdateState += (drawableObject, state) =>
{
- drawable.ApplyCustomUpdateState += (drawableObject, state) =>
- {
- if (!(drawableObject is DrawableHitCircle drawableHitCircle)) return;
+ if (!(drawableObject is DrawableHitCircle drawableHitCircle)) return;
- var hitCircle = drawableHitCircle.HitObject;
+ var hitCircle = drawableHitCircle.HitObject;
- drawableHitCircle.ApproachCircle.ClearTransforms(targetMember: nameof(Scale));
+ drawableHitCircle.ApproachCircle.ClearTransforms(targetMember: nameof(Scale));
- using (drawableHitCircle.BeginAbsoluteSequence(hitCircle.StartTime - hitCircle.TimePreempt))
- drawableHitCircle.ApproachCircle.ScaleTo(Scale.Value).ScaleTo(1f, hitCircle.TimePreempt, getEasing(Style.Value));
- };
- });
+ using (drawableHitCircle.BeginAbsoluteSequence(hitCircle.StartTime - hitCircle.TimePreempt))
+ drawableHitCircle.ApproachCircle.ScaleTo(Scale.Value).ScaleTo(1f, hitCircle.TimePreempt, getEasing(Style.Value));
+ };
}
private Easing getEasing(AnimationStyle style)
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModBarrelRoll.cs b/osu.Game.Rulesets.Osu/Mods/OsuModBarrelRoll.cs
index 9ae9653e9b..9e71f657ce 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModBarrelRoll.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModBarrelRoll.cs
@@ -1,7 +1,6 @@
// 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.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects;
@@ -9,22 +8,19 @@ using osu.Game.Rulesets.Osu.Objects.Drawables;
namespace osu.Game.Rulesets.Osu.Mods
{
- public class OsuModBarrelRoll : ModBarrelRoll, IApplicableToDrawableHitObjects
+ public class OsuModBarrelRoll : ModBarrelRoll, IApplicableToDrawableHitObject
{
- public void ApplyToDrawableHitObjects(IEnumerable drawables)
+ public void ApplyToDrawableHitObject(DrawableHitObject d)
{
- foreach (var d in drawables)
+ d.OnUpdate += _ =>
{
- d.OnUpdate += _ =>
+ switch (d)
{
- switch (d)
- {
- case DrawableHitCircle circle:
- circle.CirclePiece.Rotation = -CurrentRotation;
- break;
- }
- };
- }
+ case DrawableHitCircle circle:
+ circle.CirclePiece.Rotation = -CurrentRotation;
+ break;
+ }
+ };
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs
index 77dea5b0dc..e04a30d06c 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs
@@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using System.Collections.Generic;
using System.Linq;
using osu.Framework.Bindables;
using osu.Game.Configuration;
@@ -15,7 +14,7 @@ using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Osu.Mods
{
- public class OsuModClassic : ModClassic, IApplicableToHitObject, IApplicableToDrawableHitObjects, IApplicableToDrawableRuleset
+ public class OsuModClassic : ModClassic, IApplicableToHitObject, IApplicableToDrawableHitObject, IApplicableToDrawableRuleset
{
[SettingSource("No slider head accuracy requirement", "Scores sliders proportionally to the number of ticks hit.")]
public Bindable NoSliderHeadAccuracy { get; } = new BindableBool(true);
@@ -54,24 +53,21 @@ namespace osu.Game.Rulesets.Osu.Mods
osuRuleset.Playfield.HitPolicy = new ObjectOrderedHitPolicy();
}
- public void ApplyToDrawableHitObjects(IEnumerable drawables)
+ public void ApplyToDrawableHitObject(DrawableHitObject obj)
{
- foreach (var obj in drawables)
+ switch (obj)
{
- switch (obj)
- {
- case DrawableSlider slider:
- slider.Ball.InputTracksVisualSize = !FixedFollowCircleHitArea.Value;
- break;
+ case DrawableSlider slider:
+ slider.Ball.InputTracksVisualSize = !FixedFollowCircleHitArea.Value;
+ break;
- case DrawableSliderHead head:
- head.TrackFollowCircle = !NoSliderHeadMovement.Value;
- break;
+ case DrawableSliderHead head:
+ head.TrackFollowCircle = !NoSliderHeadMovement.Value;
+ break;
- case DrawableSliderTail tail:
- tail.SamplePlaysOnlyOnHit = !AlwaysPlayTailSample.Value;
- break;
- }
+ case DrawableSliderTail tail:
+ tail.SamplePlaysOnlyOnHit = !AlwaysPlayTailSample.Value;
+ break;
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs b/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs
index 683b35f282..300a9d48aa 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs
@@ -2,8 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System;
-using System.Collections.Generic;
-using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Input;
@@ -19,7 +17,7 @@ using osuTK;
namespace osu.Game.Rulesets.Osu.Mods
{
- public class OsuModFlashlight : ModFlashlight, IApplicableToDrawableHitObjects
+ public class OsuModFlashlight : ModFlashlight, IApplicableToDrawableHitObject
{
public override double ScoreMultiplier => 1.12;
@@ -31,12 +29,10 @@ namespace osu.Game.Rulesets.Osu.Mods
public override Flashlight CreateFlashlight() => flashlight = new OsuFlashlight();
- public void ApplyToDrawableHitObjects(IEnumerable drawables)
+ public void ApplyToDrawableHitObject(DrawableHitObject drawable)
{
- foreach (var s in drawables.OfType())
- {
+ if (drawable is DrawableSlider s)
s.Tracking.ValueChanged += flashlight.OnSliderTrackingChange;
- }
}
public override void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset)
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs
index 2752feb0a1..a7c79aa2a0 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs
@@ -14,12 +14,12 @@ using osu.Game.Rulesets.Osu.Objects;
namespace osu.Game.Rulesets.Osu.Mods
{
- public class OsuModHidden : ModHidden
+ public class OsuModHidden : ModHidden, IMutateApproachCircles
{
public override string Description => @"Play with no approach circles and fading circles/sliders.";
public override double ScoreMultiplier => 1.06;
- public override Type[] IncompatibleMods => new[] { typeof(OsuModTraceable), typeof(OsuModSpinIn) };
+ public override Type[] IncompatibleMods => new[] { typeof(IMutateApproachCircles) };
private const double fade_in_duration_multiplier = 0.4;
private const double fade_out_duration_multiplier = 0.3;
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModObjectScaleTween.cs b/osu.Game.Rulesets.Osu/Mods/OsuModObjectScaleTween.cs
index d1be162f73..6dfabed0df 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModObjectScaleTween.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModObjectScaleTween.cs
@@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Osu.Mods
///
/// Adjusts the size of hit objects during their fade in animation.
///
- public abstract class OsuModObjectScaleTween : ModWithVisibilityAdjustment
+ public abstract class OsuModObjectScaleTween : ModWithVisibilityAdjustment, IMutateApproachCircles
{
public override ModType Type => ModType.Fun;
@@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Mods
protected virtual float EndScale => 1;
- public override Type[] IncompatibleMods => new[] { typeof(OsuModSpinIn), typeof(OsuModTraceable) };
+ public override Type[] IncompatibleMods => new[] { typeof(IMutateApproachCircles) };
protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state)
{
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSpinIn.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpinIn.cs
index 96ba58da23..d3ca2973f0 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModSpinIn.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpinIn.cs
@@ -12,7 +12,7 @@ using osuTK;
namespace osu.Game.Rulesets.Osu.Mods
{
- public class OsuModSpinIn : ModWithVisibilityAdjustment
+ public class OsuModSpinIn : ModWithVisibilityAdjustment, IMutateApproachCircles
{
public override string Name => "Spin In";
public override string Acronym => "SI";
@@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override double ScoreMultiplier => 1;
// todo: this mod should be able to be compatible with hidden with a bit of further implementation.
- public override Type[] IncompatibleMods => new[] { typeof(OsuModObjectScaleTween), typeof(OsuModHidden), typeof(OsuModTraceable) };
+ public override Type[] IncompatibleMods => new[] { typeof(IMutateApproachCircles) };
private const int rotate_offset = 360;
private const float rotate_starting_width = 2;
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs
index b12d735474..c7f4811701 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs
@@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System;
-using System.Collections.Generic;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Utils;
@@ -13,7 +12,7 @@ using osu.Game.Rulesets.Osu.Objects.Drawables;
namespace osu.Game.Rulesets.Osu.Mods
{
- public class OsuModSpunOut : Mod, IApplicableToDrawableHitObjects
+ public class OsuModSpunOut : Mod, IApplicableToDrawableHitObject
{
public override string Name => "Spun Out";
public override string Acronym => "SO";
@@ -23,15 +22,12 @@ namespace osu.Game.Rulesets.Osu.Mods
public override double ScoreMultiplier => 0.9;
public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(OsuModAutopilot) };
- public void ApplyToDrawableHitObjects(IEnumerable drawables)
+ public void ApplyToDrawableHitObject(DrawableHitObject hitObject)
{
- foreach (var hitObject in drawables)
+ if (hitObject is DrawableSpinner spinner)
{
- if (hitObject is DrawableSpinner spinner)
- {
- spinner.HandleUserInput = false;
- spinner.OnUpdate += onSpinnerUpdate;
- }
+ spinner.HandleUserInput = false;
+ spinner.OnUpdate += onSpinnerUpdate;
}
}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs
index 4b0939db16..84263221a7 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs
@@ -11,7 +11,7 @@ using osu.Game.Rulesets.Osu.Skinning.Default;
namespace osu.Game.Rulesets.Osu.Mods
{
- public class OsuModTraceable : ModWithVisibilityAdjustment
+ public class OsuModTraceable : ModWithVisibilityAdjustment, IMutateApproachCircles
{
public override string Name => "Traceable";
public override string Acronym => "TC";
@@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override string Description => "Put your faith in the approach circles...";
public override double ScoreMultiplier => 1;
- public override Type[] IncompatibleMods => new[] { typeof(OsuModHidden), typeof(OsuModSpinIn), typeof(OsuModObjectScaleTween) };
+ public override Type[] IncompatibleMods => new[] { typeof(IMutateApproachCircles) };
protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state)
{
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
index 236af4b3f1..ca2e6578db 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
@@ -172,6 +172,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
base.UpdateStartTimeStateTransforms();
+ // always fade out at the circle's start time (to match user expectations).
ApproachCircle.FadeOut(50);
}
@@ -182,6 +183,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
// todo: temporary / arbitrary, used for lifetime optimisation.
this.Delay(800).FadeOut();
+ // in the case of an early state change, the fade should be expedited to the current point in time.
+ if (HitStateUpdateTime < HitObject.StartTime)
+ ApproachCircle.FadeOut(50);
+
switch (state)
{
case ArmedState.Idle:
diff --git a/osu.Game.Rulesets.Taiko.Tests/Mods/TaikoModTestScene.cs b/osu.Game.Rulesets.Taiko.Tests/Mods/TaikoModTestScene.cs
new file mode 100644
index 0000000000..3090facf8c
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko.Tests/Mods/TaikoModTestScene.cs
@@ -0,0 +1,12 @@
+// 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.Tests.Visual;
+
+namespace osu.Game.Rulesets.Taiko.Tests.Mods
+{
+ public abstract class TaikoModTestScene : ModTestScene
+ {
+ protected sealed override Ruleset CreatePlayerRuleset() => new TaikoRuleset();
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModHidden.cs b/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModHidden.cs
new file mode 100644
index 0000000000..7abbb9d186
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModHidden.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 System.Linq;
+using NUnit.Framework;
+using osu.Game.Rulesets.Taiko.Mods;
+
+namespace osu.Game.Rulesets.Taiko.Tests.Mods
+{
+ public class TestSceneTaikoModHidden : TaikoModTestScene
+ {
+ [Test]
+ public void TestDefaultBeatmapTest() => CreateModTest(new ModTestData
+ {
+ Mod = new TaikoModHidden(),
+ Autoplay = true,
+ PassCondition = checkSomeAutoplayHits
+ });
+
+ private bool checkSomeAutoplayHits()
+ => Player.ScoreProcessor.JudgedHits >= 4
+ && Player.Results.All(result => result.Type == result.Judgement.MaxResult);
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs
index 7739ecaf5b..0fd3625a93 100644
--- a/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs
+++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs
@@ -1,23 +1,93 @@
// 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.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Rulesets.Taiko.Objects.Drawables;
namespace osu.Game.Rulesets.Taiko.Mods
{
- public class TaikoModHidden : ModHidden
+ public class TaikoModHidden : ModHidden, IApplicableToDifficulty
{
public override string Description => @"Beats fade out before you hit them!";
public override double ScoreMultiplier => 1.06;
- public override bool HasImplementation => false;
+
+ ///
+ /// In osu-stable, the hit position is 160, so the active playfield is essentially 160 pixels shorter
+ /// than the actual screen width. The normalized playfield height is 480, so on a 4:3 screen the
+ /// playfield ratio of the active area up to the hit position will actually be (640 - 160) / 480 = 1.
+ /// For custom resolutions/aspect ratios (x:y), the screen width given the normalized height becomes 480 * x / y instead,
+ /// and the playfield ratio becomes (480 * x / y - 160) / 480 = x / y - 1/3.
+ /// This constant is equal to the playfield ratio on 4:3 screens divided by the playfield ratio on 16:9 screens.
+ ///
+ private const double hd_sv_scale = (4.0 / 3.0 - 1.0 / 3.0) / (16.0 / 9.0 - 1.0 / 3.0);
+
+ private double originalSliderMultiplier;
+
+ private ControlPointInfo controlPointInfo;
protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state)
{
+ ApplyNormalVisibilityState(hitObject, state);
+ }
+
+ protected double MultiplierAt(double position)
+ {
+ double beatLength = controlPointInfo.TimingPointAt(position).BeatLength;
+ double speedMultiplier = controlPointInfo.DifficultyPointAt(position).SpeedMultiplier;
+
+ return originalSliderMultiplier * speedMultiplier * TimingControlPoint.DEFAULT_BEAT_LENGTH / beatLength;
}
protected override void ApplyNormalVisibilityState(DrawableHitObject hitObject, ArmedState state)
{
+ switch (hitObject)
+ {
+ case DrawableDrumRollTick _:
+ case DrawableHit _:
+ double preempt = 10000 / MultiplierAt(hitObject.HitObject.StartTime);
+ double start = hitObject.HitObject.StartTime - preempt * 0.6;
+ double duration = preempt * 0.3;
+
+ using (hitObject.BeginAbsoluteSequence(start))
+ {
+ hitObject.FadeOut(duration);
+
+ // DrawableHitObject sets LifetimeEnd to LatestTransformEndTime if it isn't manually changed.
+ // in order for the object to not be killed before its actual end time (as the latest transform ends earlier), set lifetime end explicitly.
+ hitObject.LifetimeEnd = state == ArmedState.Idle || !hitObject.AllJudged
+ ? hitObject.HitObject.GetEndTime() + hitObject.HitObject.HitWindows.WindowFor(HitResult.Miss)
+ : hitObject.HitStateUpdateTime;
+ }
+
+ break;
+ }
+ }
+
+ public void ReadFromDifficulty(BeatmapDifficulty difficulty)
+ {
+ }
+
+ public void ApplyToDifficulty(BeatmapDifficulty difficulty)
+ {
+ // needs to be read after all processing has been run (TaikoBeatmapConverter applies an adjustment which would otherwise be omitted).
+ originalSliderMultiplier = difficulty.SliderMultiplier;
+
+ // osu-stable has an added playfield cover that essentially forces a 4:3 playfield ratio, by cutting off all objects past that size.
+ // This is not yet implemented; instead a playfield adjustment container is present which maintains a 16:9 ratio.
+ // For now, increase the slider multiplier proportionally so that the notes stay on the screen for the same amount of time as on stable.
+ // Note that this means that the notes will scroll faster as they have a longer distance to travel on the screen in that same amount of time.
+ difficulty.SliderMultiplier /= hd_sv_scale;
+ }
+
+ public override void ApplyToBeatmap(IBeatmap beatmap)
+ {
+ controlPointInfo = beatmap.ControlPointInfo;
}
}
}
diff --git a/osu.Game.Tests/Mods/ModUtilsTest.cs b/osu.Game.Tests/Mods/ModUtilsTest.cs
index 7384471c41..9f27289d7e 100644
--- a/osu.Game.Tests/Mods/ModUtilsTest.cs
+++ b/osu.Game.Tests/Mods/ModUtilsTest.cs
@@ -21,6 +21,14 @@ namespace osu.Game.Tests.Mods
Assert.That(ModUtils.CheckCompatibleSet(new[] { mod.Object }));
}
+ [Test]
+ public void TestModIsCompatibleByItselfWithIncompatibleInterface()
+ {
+ var mod = new Mock();
+ mod.Setup(m => m.IncompatibleMods).Returns(new[] { typeof(IModCompatibilitySpecification) });
+ Assert.That(ModUtils.CheckCompatibleSet(new[] { mod.Object }));
+ }
+
[Test]
public void TestIncompatibleThroughTopLevel()
{
@@ -34,6 +42,20 @@ namespace osu.Game.Tests.Mods
Assert.That(ModUtils.CheckCompatibleSet(new Mod[] { mod2.Object, mod1.Object }), Is.False);
}
+ [Test]
+ public void TestIncompatibleThroughInterface()
+ {
+ var mod1 = new Mock();
+ var mod2 = new Mock();
+
+ mod1.Setup(m => m.IncompatibleMods).Returns(new[] { typeof(IModCompatibilitySpecification) });
+ mod2.Setup(m => m.IncompatibleMods).Returns(new[] { typeof(IModCompatibilitySpecification) });
+
+ // Test both orderings.
+ Assert.That(ModUtils.CheckCompatibleSet(new Mod[] { mod1.Object, mod2.Object }), Is.False);
+ Assert.That(ModUtils.CheckCompatibleSet(new Mod[] { mod2.Object, mod1.Object }), Is.False);
+ }
+
[Test]
public void TestMultiModIncompatibleWithTopLevel()
{
@@ -149,11 +171,15 @@ namespace osu.Game.Tests.Mods
Assert.That(invalid.Select(t => t.GetType()), Is.EquivalentTo(expectedInvalid));
}
- public abstract class CustomMod1 : Mod
+ public abstract class CustomMod1 : Mod, IModCompatibilitySpecification
{
}
- public abstract class CustomMod2 : Mod
+ public abstract class CustomMod2 : Mod, IModCompatibilitySpecification
+ {
+ }
+
+ public interface IModCompatibilitySpecification
{
}
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs
index 5ef3eff856..3ed274690e 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs
@@ -66,12 +66,12 @@ namespace osu.Game.Tests.Visual.Gameplay
}
[Test]
- public void TestStoryboardExitToSkipOutro()
+ public void TestStoryboardExitDuringOutroStillExits()
{
CreateTest(null);
AddUntilStep("completion set by processor", () => Player.ScoreProcessor.HasCompleted.Value);
AddStep("exit via pause", () => Player.ExitViaPause());
- AddAssert("score shown", () => Player.IsScoreShown);
+ AddAssert("player exited", () => !Player.IsCurrentScreen() && Player.GetChildScreen() == null);
}
[TestCase(false)]
diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs
index d3a4b635f5..25d0843a71 100644
--- a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs
+++ b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using JetBrains.Annotations;
using Newtonsoft.Json;
using osu.Framework.Bindables;
using osu.Framework.Lists;
@@ -66,6 +67,7 @@ namespace osu.Game.Beatmaps.ControlPoints
///
/// The time to find the difficulty control point at.
/// The difficulty control point.
+ [NotNull]
public DifficultyControlPoint DifficultyPointAt(double time) => binarySearchWithFallback(DifficultyPoints, time, DifficultyControlPoint.DEFAULT);
///
@@ -73,6 +75,7 @@ namespace osu.Game.Beatmaps.ControlPoints
///
/// The time to find the effect control point at.
/// The effect control point.
+ [NotNull]
public EffectControlPoint EffectPointAt(double time) => binarySearchWithFallback(EffectPoints, time, EffectControlPoint.DEFAULT);
///
@@ -80,6 +83,7 @@ namespace osu.Game.Beatmaps.ControlPoints
///
/// The time to find the sound control point at.
/// The sound control point.
+ [NotNull]
public SampleControlPoint SamplePointAt(double time) => binarySearchWithFallback(SamplePoints, time, SamplePoints.Count > 0 ? SamplePoints[0] : SampleControlPoint.DEFAULT);
///
@@ -87,6 +91,7 @@ namespace osu.Game.Beatmaps.ControlPoints
///
/// The time to find the timing control point at.
/// The timing control point.
+ [NotNull]
public TimingControlPoint TimingPointAt(double time) => binarySearchWithFallback(TimingPoints, time, TimingPoints.Count > 0 ? TimingPoints[0] : TimingControlPoint.DEFAULT);
///
diff --git a/osu.Game/Graphics/UserInterface/OsuTabControl.cs b/osu.Game/Graphics/UserInterface/OsuTabControl.cs
index dbcce9a84a..0c220336a5 100644
--- a/osu.Game/Graphics/UserInterface/OsuTabControl.cs
+++ b/osu.Game/Graphics/UserInterface/OsuTabControl.cs
@@ -160,7 +160,7 @@ namespace osu.Game.Graphics.UserInterface
Margin = new MarginPadding { Top = 5, Bottom = 5 },
Origin = Anchor.BottomLeft,
Anchor = Anchor.BottomLeft,
- Text = (value as IHasDescription)?.Description ?? (value as Enum)?.GetDescription() ?? value.ToString(),
+ Text = (value as IHasDescription)?.Description ?? (value as Enum)?.GetLocalisableDescription() ?? value.ToString(),
Font = OsuFont.GetFont(size: 14)
},
Bar = new Box
diff --git a/osu.Game/Graphics/UserInterface/PageTabControl.cs b/osu.Game/Graphics/UserInterface/PageTabControl.cs
index d05a08108a..1ba9ad53bb 100644
--- a/osu.Game/Graphics/UserInterface/PageTabControl.cs
+++ b/osu.Game/Graphics/UserInterface/PageTabControl.cs
@@ -11,6 +11,7 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
+using osu.Framework.Localisation;
using osu.Game.Graphics.Sprites;
namespace osu.Game.Graphics.UserInterface
@@ -81,7 +82,7 @@ namespace osu.Game.Graphics.UserInterface
Active.BindValueChanged(active => Text.Font = Text.Font.With(Typeface.Torus, weight: active.NewValue ? FontWeight.Bold : FontWeight.Medium), true);
}
- protected virtual string CreateText() => (Value as Enum)?.GetDescription() ?? Value.ToString();
+ protected virtual LocalisableString CreateText() => (Value as Enum)?.GetLocalisableDescription() ?? Value.ToString();
protected override bool OnHover(HoverEvent e)
{
diff --git a/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs b/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs
index 7d6c76bc2f..ee72df4c10 100644
--- a/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs
+++ b/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs
@@ -6,7 +6,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
-using JetBrains.Annotations;
using MessagePack;
using osu.Game.Online.API;
@@ -28,11 +27,9 @@ namespace osu.Game.Online.Multiplayer
[Key(3)]
public string Name { get; set; } = "Unnamed room";
- [NotNull]
[Key(4)]
public IEnumerable RequiredMods { get; set; } = Enumerable.Empty();
- [NotNull]
[Key(5)]
public IEnumerable AllowedMods { get; set; } = Enumerable.Empty();
diff --git a/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs b/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs
index c654127b94..a49a8f083c 100644
--- a/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs
+++ b/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs
@@ -6,7 +6,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
-using JetBrains.Annotations;
using MessagePack;
using Newtonsoft.Json;
using osu.Game.Online.API;
@@ -35,7 +34,6 @@ namespace osu.Game.Online.Multiplayer
/// Any mods applicable only to the local user.
///
[Key(3)]
- [NotNull]
public IEnumerable Mods { get; set; } = Enumerable.Empty();
[IgnoreMember]
diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchScoreFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchScoreFilterRow.cs
index abfffe907f..b39934b56f 100644
--- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchScoreFilterRow.cs
+++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchScoreFilterRow.cs
@@ -1,9 +1,9 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using System;
using System.Collections.Generic;
using System.Linq;
+using osu.Framework.Extensions;
using osu.Framework.Localisation;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Scoring;
@@ -33,38 +33,7 @@ namespace osu.Game.Overlays.BeatmapListing
{
}
- protected override LocalisableString LabelFor(ScoreRank value)
- {
- switch (value)
- {
- case ScoreRank.XH:
- return BeatmapsStrings.RankXH;
-
- case ScoreRank.X:
- return BeatmapsStrings.RankX;
-
- case ScoreRank.SH:
- return BeatmapsStrings.RankSH;
-
- case ScoreRank.S:
- return BeatmapsStrings.RankS;
-
- case ScoreRank.A:
- return BeatmapsStrings.RankA;
-
- case ScoreRank.B:
- return BeatmapsStrings.RankB;
-
- case ScoreRank.C:
- return BeatmapsStrings.RankC;
-
- case ScoreRank.D:
- return BeatmapsStrings.RankD;
-
- default:
- throw new ArgumentException("Unsupported value.", nameof(value));
- }
- }
+ protected override LocalisableString LabelFor(ScoreRank value) => value.GetLocalisableDescription();
}
}
}
diff --git a/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs b/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs
index d64ee59682..46cb1e822f 100644
--- a/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs
+++ b/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs
@@ -67,7 +67,7 @@ namespace osu.Game.Overlays.BeatmapListing
///
/// Returns the label text to be used for the supplied .
///
- protected virtual LocalisableString LabelFor(T value) => (value as Enum)?.GetDescription() ?? value.ToString();
+ protected virtual LocalisableString LabelFor(T value) => (value as Enum)?.GetLocalisableDescription() ?? value.ToString();
private void updateState()
{
diff --git a/osu.Game/Overlays/BeatmapListing/SearchCategory.cs b/osu.Game/Overlays/BeatmapListing/SearchCategory.cs
index 84859bf5b5..8a9df76af3 100644
--- a/osu.Game/Overlays/BeatmapListing/SearchCategory.cs
+++ b/osu.Game/Overlays/BeatmapListing/SearchCategory.cs
@@ -1,10 +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 System;
using System.ComponentModel;
+using osu.Framework.Localisation;
+using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Overlays.BeatmapListing
{
+ [LocalisableEnum(typeof(SearchCategoryEnumLocalisationMapper))]
public enum SearchCategory
{
Any,
@@ -23,4 +27,43 @@ namespace osu.Game.Overlays.BeatmapListing
[Description("My Maps")]
Mine,
}
+
+ public class SearchCategoryEnumLocalisationMapper : EnumLocalisationMapper
+ {
+ public override LocalisableString Map(SearchCategory value)
+ {
+ switch (value)
+ {
+ case SearchCategory.Any:
+ return BeatmapsStrings.StatusAny;
+
+ case SearchCategory.Leaderboard:
+ return BeatmapsStrings.StatusLeaderboard;
+
+ case SearchCategory.Ranked:
+ return BeatmapsStrings.StatusRanked;
+
+ case SearchCategory.Qualified:
+ return BeatmapsStrings.StatusQualified;
+
+ case SearchCategory.Loved:
+ return BeatmapsStrings.StatusLoved;
+
+ case SearchCategory.Favourites:
+ return BeatmapsStrings.StatusFavourites;
+
+ case SearchCategory.Pending:
+ return BeatmapsStrings.StatusPending;
+
+ case SearchCategory.Graveyard:
+ return BeatmapsStrings.StatusGraveyard;
+
+ case SearchCategory.Mine:
+ return BeatmapsStrings.StatusMine;
+
+ default:
+ throw new ArgumentOutOfRangeException(nameof(value), value, null);
+ }
+ }
+ }
}
diff --git a/osu.Game/Overlays/BeatmapListing/SearchExplicit.cs b/osu.Game/Overlays/BeatmapListing/SearchExplicit.cs
index 3e57cdd48c..78e6a4e094 100644
--- a/osu.Game/Overlays/BeatmapListing/SearchExplicit.cs
+++ b/osu.Game/Overlays/BeatmapListing/SearchExplicit.cs
@@ -1,11 +1,34 @@
// 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.Localisation;
+using osu.Game.Resources.Localisation.Web;
+
namespace osu.Game.Overlays.BeatmapListing
{
+ [LocalisableEnum(typeof(SearchExplicitEnumLocalisationMapper))]
public enum SearchExplicit
{
Hide,
Show
}
+
+ public class SearchExplicitEnumLocalisationMapper : EnumLocalisationMapper
+ {
+ public override LocalisableString Map(SearchExplicit value)
+ {
+ switch (value)
+ {
+ case SearchExplicit.Hide:
+ return BeatmapsStrings.NsfwExclude;
+
+ case SearchExplicit.Show:
+ return BeatmapsStrings.NsfwInclude;
+
+ default:
+ throw new ArgumentOutOfRangeException(nameof(value), value, null);
+ }
+ }
+ }
}
diff --git a/osu.Game/Overlays/BeatmapListing/SearchExtra.cs b/osu.Game/Overlays/BeatmapListing/SearchExtra.cs
index af37e3264f..4b3fb6e833 100644
--- a/osu.Game/Overlays/BeatmapListing/SearchExtra.cs
+++ b/osu.Game/Overlays/BeatmapListing/SearchExtra.cs
@@ -1,10 +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 System;
using System.ComponentModel;
+using osu.Framework.Localisation;
+using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Overlays.BeatmapListing
{
+ [LocalisableEnum(typeof(SearchExtraEnumLocalisationMapper))]
public enum SearchExtra
{
[Description("Has Video")]
@@ -13,4 +17,22 @@ namespace osu.Game.Overlays.BeatmapListing
[Description("Has Storyboard")]
Storyboard
}
+
+ public class SearchExtraEnumLocalisationMapper : EnumLocalisationMapper
+ {
+ public override LocalisableString Map(SearchExtra value)
+ {
+ switch (value)
+ {
+ case SearchExtra.Video:
+ return BeatmapsStrings.ExtraVideo;
+
+ case SearchExtra.Storyboard:
+ return BeatmapsStrings.ExtraStoryboard;
+
+ default:
+ throw new ArgumentOutOfRangeException(nameof(value), value, null);
+ }
+ }
+ }
}
diff --git a/osu.Game/Overlays/BeatmapListing/SearchGeneral.cs b/osu.Game/Overlays/BeatmapListing/SearchGeneral.cs
index 175942c626..b4c629f7fa 100644
--- a/osu.Game/Overlays/BeatmapListing/SearchGeneral.cs
+++ b/osu.Game/Overlays/BeatmapListing/SearchGeneral.cs
@@ -1,10 +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 System;
using System.ComponentModel;
+using osu.Framework.Localisation;
+using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Overlays.BeatmapListing
{
+ [LocalisableEnum(typeof(SearchGeneralEnumLocalisationMapper))]
public enum SearchGeneral
{
[Description("Recommended difficulty")]
@@ -16,4 +20,25 @@ namespace osu.Game.Overlays.BeatmapListing
[Description("Subscribed mappers")]
Follows
}
+
+ public class SearchGeneralEnumLocalisationMapper : EnumLocalisationMapper
+ {
+ public override LocalisableString Map(SearchGeneral value)
+ {
+ switch (value)
+ {
+ case SearchGeneral.Recommended:
+ return BeatmapsStrings.GeneralRecommended;
+
+ case SearchGeneral.Converts:
+ return BeatmapsStrings.GeneralConverts;
+
+ case SearchGeneral.Follows:
+ return BeatmapsStrings.GeneralFollows;
+
+ default:
+ throw new ArgumentOutOfRangeException(nameof(value), value, null);
+ }
+ }
+ }
}
diff --git a/osu.Game/Overlays/BeatmapListing/SearchGenre.cs b/osu.Game/Overlays/BeatmapListing/SearchGenre.cs
index de437fac3e..b2709ecd2e 100644
--- a/osu.Game/Overlays/BeatmapListing/SearchGenre.cs
+++ b/osu.Game/Overlays/BeatmapListing/SearchGenre.cs
@@ -1,10 +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 System;
using System.ComponentModel;
+using osu.Framework.Localisation;
+using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Overlays.BeatmapListing
{
+ [LocalisableEnum(typeof(SearchGenreEnumLocalisationMapper))]
public enum SearchGenre
{
Any = 0,
@@ -26,4 +30,58 @@ namespace osu.Game.Overlays.BeatmapListing
Folk = 13,
Jazz = 14
}
+
+ public class SearchGenreEnumLocalisationMapper : EnumLocalisationMapper
+ {
+ public override LocalisableString Map(SearchGenre value)
+ {
+ switch (value)
+ {
+ case SearchGenre.Any:
+ return BeatmapsStrings.GenreAny;
+
+ case SearchGenre.Unspecified:
+ return BeatmapsStrings.GenreUnspecified;
+
+ case SearchGenre.VideoGame:
+ return BeatmapsStrings.GenreVideoGame;
+
+ case SearchGenre.Anime:
+ return BeatmapsStrings.GenreAnime;
+
+ case SearchGenre.Rock:
+ return BeatmapsStrings.GenreRock;
+
+ case SearchGenre.Pop:
+ return BeatmapsStrings.GenrePop;
+
+ case SearchGenre.Other:
+ return BeatmapsStrings.GenreOther;
+
+ case SearchGenre.Novelty:
+ return BeatmapsStrings.GenreNovelty;
+
+ case SearchGenre.HipHop:
+ return BeatmapsStrings.GenreHipHop;
+
+ case SearchGenre.Electronic:
+ return BeatmapsStrings.GenreElectronic;
+
+ case SearchGenre.Metal:
+ return BeatmapsStrings.GenreMetal;
+
+ case SearchGenre.Classical:
+ return BeatmapsStrings.GenreClassical;
+
+ case SearchGenre.Folk:
+ return BeatmapsStrings.GenreFolk;
+
+ case SearchGenre.Jazz:
+ return BeatmapsStrings.GenreJazz;
+
+ default:
+ throw new ArgumentOutOfRangeException(nameof(value), value, null);
+ }
+ }
+ }
}
diff --git a/osu.Game/Overlays/BeatmapListing/SearchLanguage.cs b/osu.Game/Overlays/BeatmapListing/SearchLanguage.cs
index 015cee8ce3..fc176c305a 100644
--- a/osu.Game/Overlays/BeatmapListing/SearchLanguage.cs
+++ b/osu.Game/Overlays/BeatmapListing/SearchLanguage.cs
@@ -1,10 +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 System;
+using osu.Framework.Localisation;
using osu.Framework.Utils;
+using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Overlays.BeatmapListing
{
+ [LocalisableEnum(typeof(SearchLanguageEnumLocalisationMapper))]
[HasOrderedElements]
public enum SearchLanguage
{
@@ -53,4 +57,61 @@ namespace osu.Game.Overlays.BeatmapListing
[Order(13)]
Other
}
+
+ public class SearchLanguageEnumLocalisationMapper : EnumLocalisationMapper
+ {
+ public override LocalisableString Map(SearchLanguage value)
+ {
+ switch (value)
+ {
+ case SearchLanguage.Any:
+ return BeatmapsStrings.LanguageAny;
+
+ case SearchLanguage.Unspecified:
+ return BeatmapsStrings.LanguageUnspecified;
+
+ case SearchLanguage.English:
+ return BeatmapsStrings.LanguageEnglish;
+
+ case SearchLanguage.Japanese:
+ return BeatmapsStrings.LanguageJapanese;
+
+ case SearchLanguage.Chinese:
+ return BeatmapsStrings.LanguageChinese;
+
+ case SearchLanguage.Instrumental:
+ return BeatmapsStrings.LanguageInstrumental;
+
+ case SearchLanguage.Korean:
+ return BeatmapsStrings.LanguageKorean;
+
+ case SearchLanguage.French:
+ return BeatmapsStrings.LanguageFrench;
+
+ case SearchLanguage.German:
+ return BeatmapsStrings.LanguageGerman;
+
+ case SearchLanguage.Swedish:
+ return BeatmapsStrings.LanguageSwedish;
+
+ case SearchLanguage.Spanish:
+ return BeatmapsStrings.LanguageSpanish;
+
+ case SearchLanguage.Italian:
+ return BeatmapsStrings.LanguageItalian;
+
+ case SearchLanguage.Russian:
+ return BeatmapsStrings.LanguageRussian;
+
+ case SearchLanguage.Polish:
+ return BeatmapsStrings.LanguagePolish;
+
+ case SearchLanguage.Other:
+ return BeatmapsStrings.LanguageOther;
+
+ default:
+ throw new ArgumentOutOfRangeException(nameof(value), value, null);
+ }
+ }
+ }
}
diff --git a/osu.Game/Overlays/BeatmapListing/SearchPlayed.cs b/osu.Game/Overlays/BeatmapListing/SearchPlayed.cs
index eb7fb46158..f24cf46c2d 100644
--- a/osu.Game/Overlays/BeatmapListing/SearchPlayed.cs
+++ b/osu.Game/Overlays/BeatmapListing/SearchPlayed.cs
@@ -1,12 +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 System;
+using osu.Framework.Localisation;
+using osu.Game.Resources.Localisation.Web;
+
namespace osu.Game.Overlays.BeatmapListing
{
+ [LocalisableEnum(typeof(SearchPlayedEnumLocalisationMapper))]
public enum SearchPlayed
{
Any,
Played,
Unplayed
}
+
+ public class SearchPlayedEnumLocalisationMapper : EnumLocalisationMapper
+ {
+ public override LocalisableString Map(SearchPlayed value)
+ {
+ switch (value)
+ {
+ case SearchPlayed.Any:
+ return BeatmapsStrings.PlayedAny;
+
+ case SearchPlayed.Played:
+ return BeatmapsStrings.PlayedPlayed;
+
+ case SearchPlayed.Unplayed:
+ return BeatmapsStrings.PlayedUnplayed;
+
+ default:
+ throw new ArgumentOutOfRangeException(nameof(value), value, null);
+ }
+ }
+ }
}
diff --git a/osu.Game/Overlays/BeatmapListing/SortCriteria.cs b/osu.Game/Overlays/BeatmapListing/SortCriteria.cs
index e409cbdda7..5ea885eecc 100644
--- a/osu.Game/Overlays/BeatmapListing/SortCriteria.cs
+++ b/osu.Game/Overlays/BeatmapListing/SortCriteria.cs
@@ -1,8 +1,13 @@
// 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.Localisation;
+using osu.Game.Resources.Localisation.Web;
+
namespace osu.Game.Overlays.BeatmapListing
{
+ [LocalisableEnum(typeof(SortCriteriaLocalisationMapper))]
public enum SortCriteria
{
Title,
@@ -14,4 +19,40 @@ namespace osu.Game.Overlays.BeatmapListing
Favourites,
Relevance
}
+
+ public class SortCriteriaLocalisationMapper : EnumLocalisationMapper
+ {
+ public override LocalisableString Map(SortCriteria value)
+ {
+ switch (value)
+ {
+ case SortCriteria.Title:
+ return BeatmapsStrings.ListingSearchSortingTitle;
+
+ case SortCriteria.Artist:
+ return BeatmapsStrings.ListingSearchSortingArtist;
+
+ case SortCriteria.Difficulty:
+ return BeatmapsStrings.ListingSearchSortingDifficulty;
+
+ case SortCriteria.Ranked:
+ return BeatmapsStrings.ListingSearchSortingRanked;
+
+ case SortCriteria.Rating:
+ return BeatmapsStrings.ListingSearchSortingRating;
+
+ case SortCriteria.Plays:
+ return BeatmapsStrings.ListingSearchSortingPlays;
+
+ case SortCriteria.Favourites:
+ return BeatmapsStrings.ListingSearchSortingFavourites;
+
+ case SortCriteria.Relevance:
+ return BeatmapsStrings.ListingSearchSortingRelevance;
+
+ default:
+ throw new ArgumentOutOfRangeException(nameof(value), value, null);
+ }
+ }
+ }
}
diff --git a/osu.Game/Overlays/BeatmapSet/AuthorInfo.cs b/osu.Game/Overlays/BeatmapSet/AuthorInfo.cs
index 1ffcf9722a..265d9bf125 100644
--- a/osu.Game/Overlays/BeatmapSet/AuthorInfo.cs
+++ b/osu.Game/Overlays/BeatmapSet/AuthorInfo.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@@ -22,8 +23,8 @@ namespace osu.Game.Overlays.BeatmapSet
{
private const float height = 50;
- private readonly UpdateableAvatar avatar;
- private readonly FillFlowContainer fields;
+ private UpdateableAvatar avatar;
+ private FillFlowContainer fields;
private BeatmapSetInfo beatmapSet;
@@ -35,11 +36,46 @@ namespace osu.Game.Overlays.BeatmapSet
if (value == beatmapSet) return;
beatmapSet = value;
-
- updateDisplay();
+ Scheduler.AddOnce(updateDisplay);
}
}
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ RelativeSizeAxes = Axes.X;
+ Height = height;
+
+ Children = new Drawable[]
+ {
+ new Container
+ {
+ AutoSizeAxes = Axes.Both,
+ CornerRadius = 4,
+ Masking = true,
+ Child = avatar = new UpdateableAvatar(showGuestOnNull: false)
+ {
+ Size = new Vector2(height),
+ },
+ EdgeEffect = new EdgeEffectParameters
+ {
+ Colour = Color4.Black.Opacity(0.25f),
+ Type = EdgeEffectType.Shadow,
+ Radius = 4,
+ Offset = new Vector2(0f, 1f),
+ },
+ },
+ fields = new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ Direction = FillDirection.Vertical,
+ Padding = new MarginPadding { Left = height + 5 },
+ },
+ };
+
+ Scheduler.AddOnce(updateDisplay);
+ }
+
private void updateDisplay()
{
avatar.User = BeatmapSet?.Metadata.Author;
@@ -69,45 +105,6 @@ namespace osu.Game.Overlays.BeatmapSet
}
}
- public AuthorInfo()
- {
- RelativeSizeAxes = Axes.X;
- Height = height;
-
- Children = new Drawable[]
- {
- new Container
- {
- AutoSizeAxes = Axes.Both,
- CornerRadius = 4,
- Masking = true,
- Child = avatar = new UpdateableAvatar
- {
- ShowGuestOnNull = false,
- Size = new Vector2(height),
- },
- EdgeEffect = new EdgeEffectParameters
- {
- Colour = Color4.Black.Opacity(0.25f),
- Type = EdgeEffectType.Shadow,
- Radius = 4,
- Offset = new Vector2(0f, 1f),
- },
- },
- fields = new FillFlowContainer
- {
- RelativeSizeAxes = Axes.Both,
- Direction = FillDirection.Vertical,
- Padding = new MarginPadding { Left = height + 5 },
- },
- };
- }
-
- private void load()
- {
- updateDisplay();
- }
-
private class Field : FillFlowContainer
{
public Field(string first, string second, FontUsage secondFont)
diff --git a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreUserSection.cs b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreUserSection.cs
index 9111a0cfc7..736366fb5c 100644
--- a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreUserSection.cs
+++ b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreUserSection.cs
@@ -61,7 +61,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
},
}
},
- avatar = new UpdateableAvatar
+ avatar = new UpdateableAvatar(showGuestOnNull: false)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -75,7 +75,6 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
Offset = new Vector2(0, 2),
Radius = 1,
},
- ShowGuestOnNull = false,
},
new FillFlowContainer
{
diff --git a/osu.Game/Overlays/Chat/Tabs/PrivateChannelTabItem.cs b/osu.Game/Overlays/Chat/Tabs/PrivateChannelTabItem.cs
index 00f46b0035..7c82420e08 100644
--- a/osu.Game/Overlays/Chat/Tabs/PrivateChannelTabItem.cs
+++ b/osu.Game/Overlays/Chat/Tabs/PrivateChannelTabItem.cs
@@ -51,7 +51,7 @@ namespace osu.Game.Overlays.Chat.Tabs
Child = new DelayedLoadWrapper(avatar = new ClickableAvatar(value.Users.First())
{
RelativeSizeAxes = Axes.Both,
- OpenOnClick = { Value = false },
+ OpenOnClick = false,
})
{
RelativeSizeAxes = Axes.Both,
diff --git a/osu.Game/Overlays/Mods/LocalPlayerModSelectOverlay.cs b/osu.Game/Overlays/Mods/LocalPlayerModSelectOverlay.cs
index 78cd9bdae5..db76581108 100644
--- a/osu.Game/Overlays/Mods/LocalPlayerModSelectOverlay.cs
+++ b/osu.Game/Overlays/Mods/LocalPlayerModSelectOverlay.cs
@@ -12,7 +12,7 @@ namespace osu.Game.Overlays.Mods
base.OnModSelected(mod);
foreach (var section in ModSectionsContainer.Children)
- section.DeselectTypes(mod.IncompatibleMods, true);
+ section.DeselectTypes(mod.IncompatibleMods, true, mod);
}
}
}
diff --git a/osu.Game/Overlays/Mods/ModSection.cs b/osu.Game/Overlays/Mods/ModSection.cs
index aa8a5efd39..6e289dc8aa 100644
--- a/osu.Game/Overlays/Mods/ModSection.cs
+++ b/osu.Game/Overlays/Mods/ModSection.cs
@@ -159,12 +159,16 @@ namespace osu.Game.Overlays.Mods
///
/// The types of s which should be deselected.
/// Whether the deselection should happen immediately. Should only be used when required to ensure correct selection flow.
- public void DeselectTypes(IEnumerable modTypes, bool immediate = false)
+ /// If this deselection is triggered by a user selection, this should contain the newly selected type. This type will never be deselected, even if it matches one provided in .
+ public void DeselectTypes(IEnumerable modTypes, bool immediate = false, Mod newSelection = null)
{
foreach (var button in Buttons)
{
if (button.SelectedMod == null) continue;
+ if (button.SelectedMod == newSelection)
+ continue;
+
foreach (var type in modTypes)
{
if (type.IsInstanceOfType(button.SelectedMod))
diff --git a/osu.Game/Overlays/OverlaySortTabControl.cs b/osu.Game/Overlays/OverlaySortTabControl.cs
index 15c43eeb01..b230acca11 100644
--- a/osu.Game/Overlays/OverlaySortTabControl.cs
+++ b/osu.Game/Overlays/OverlaySortTabControl.cs
@@ -144,7 +144,7 @@ namespace osu.Game.Overlays
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold),
- Text = (value as Enum)?.GetDescription() ?? value.ToString()
+ Text = (value as Enum)?.GetLocalisableDescription() ?? value.ToString()
}
}
});
diff --git a/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs
index e0642d650c..d751424367 100644
--- a/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs
+++ b/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs
@@ -58,13 +58,11 @@ namespace osu.Game.Overlays.Profile.Header
Origin = Anchor.CentreLeft,
Children = new Drawable[]
{
- avatar = new UpdateableAvatar
+ avatar = new UpdateableAvatar(openOnClick: false, showGuestOnNull: false)
{
Size = new Vector2(avatar_size),
Masking = true,
CornerRadius = avatar_size * 0.25f,
- OpenOnClick = { Value = false },
- ShowGuestOnNull = false,
},
new Container
{
diff --git a/osu.Game/Overlays/TabControlOverlayHeader.cs b/osu.Game/Overlays/TabControlOverlayHeader.cs
index 7798dfa576..e6f7e250a7 100644
--- a/osu.Game/Overlays/TabControlOverlayHeader.cs
+++ b/osu.Game/Overlays/TabControlOverlayHeader.cs
@@ -106,7 +106,19 @@ namespace osu.Game.Overlays
public OverlayHeaderTabItem(T value)
: base(value)
{
- Text.Text = ((Value as Enum)?.GetDescription() ?? Value.ToString()).ToLower();
+ if (!(Value is Enum enumValue))
+ Text.Text = Value.ToString().ToLower();
+ else
+ {
+ var localisableDescription = enumValue.GetLocalisableDescription();
+ var nonLocalisableDescription = enumValue.GetDescription();
+
+ // If localisable == non-localisable, then we must have a basic string, so .ToLower() is used.
+ Text.Text = localisableDescription.Equals(nonLocalisableDescription)
+ ? nonLocalisableDescription.ToLower()
+ : localisableDescription;
+ }
+
Text.Font = OsuFont.GetFont(size: 14);
Text.Margin = new MarginPadding { Vertical = 16.5f }; // 15px padding + 1.5px line-height difference compensation
Bar.Margin = new MarginPadding { Bottom = bar_height };
diff --git a/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs b/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs
index db4e491d9a..165c095514 100644
--- a/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs
+++ b/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs
@@ -32,14 +32,13 @@ namespace osu.Game.Overlays.Toolbar
Add(new OpaqueBackground { Depth = 1 });
- Flow.Add(avatar = new UpdateableAvatar
+ Flow.Add(avatar = new UpdateableAvatar(openOnClick: false)
{
Masking = true,
Size = new Vector2(32),
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
CornerRadius = 4,
- OpenOnClick = { Value = false },
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Shadow,
diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs
index b47cf97a4d..a7005954b2 100644
--- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs
+++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs
@@ -43,6 +43,9 @@ namespace osu.Game.Rulesets.Edit
protected readonly Ruleset Ruleset;
+ // Provides `Playfield`
+ private DependencyContainer dependencies;
+
[Resolved]
protected EditorClock EditorClock { get; private set; }
@@ -69,6 +72,9 @@ namespace osu.Game.Rulesets.Edit
Ruleset = ruleset;
}
+ protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) =>
+ dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
+
[BackgroundDependencyLoader]
private void load()
{
@@ -88,6 +94,8 @@ namespace osu.Game.Rulesets.Edit
return;
}
+ dependencies.CacheAs(Playfield);
+
const float toolbar_width = 200;
InternalChildren = new Drawable[]
diff --git a/osu.Game/Rulesets/Mods/IApplicableToDrawableHitObject.cs b/osu.Game/Rulesets/Mods/IApplicableToDrawableHitObject.cs
index 5630315770..c8a9ff2f9a 100644
--- a/osu.Game/Rulesets/Mods/IApplicableToDrawableHitObject.cs
+++ b/osu.Game/Rulesets/Mods/IApplicableToDrawableHitObject.cs
@@ -1,7 +1,6 @@
// 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.Rulesets.Objects.Drawables;
namespace osu.Game.Rulesets.Mods
@@ -9,13 +8,12 @@ namespace osu.Game.Rulesets.Mods
///
/// An interface for s that can be applied to s.
///
- public interface IApplicableToDrawableHitObjects : IApplicableMod
+ public interface IApplicableToDrawableHitObject : IApplicableMod
{
///
- /// Applies this to a list of s.
+ /// Applies this to a .
/// This will only be invoked with top-level s. Access if adjusting nested objects is necessary.
///
- /// The list of s to apply to.
- void ApplyToDrawableHitObjects(IEnumerable drawables);
+ void ApplyToDrawableHitObject(DrawableHitObject drawable);
}
}
diff --git a/osu.Game/Rulesets/Mods/IApplicableToDrawableHitObjects.cs b/osu.Game/Rulesets/Mods/IApplicableToDrawableHitObjects.cs
new file mode 100644
index 0000000000..7f926dd8b8
--- /dev/null
+++ b/osu.Game/Rulesets/Mods/IApplicableToDrawableHitObjects.cs
@@ -0,0 +1,18 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using osu.Framework.Extensions.IEnumerableExtensions;
+using osu.Game.Rulesets.Objects.Drawables;
+
+namespace osu.Game.Rulesets.Mods
+{
+ [Obsolete(@"Use the singular version IApplicableToDrawableHitObject instead.")] // Can be removed 20211216
+ public interface IApplicableToDrawableHitObjects : IApplicableToDrawableHitObject
+ {
+ void ApplyToDrawableHitObjects(IEnumerable drawables);
+
+ void IApplicableToDrawableHitObject.ApplyToDrawableHitObject(DrawableHitObject drawable) => ApplyToDrawableHitObjects(drawable.Yield());
+ }
+}
diff --git a/osu.Game/Rulesets/Mods/ModWithVisibilityAdjustment.cs b/osu.Game/Rulesets/Mods/ModWithVisibilityAdjustment.cs
index 5b119b5e46..b58ee5ff36 100644
--- a/osu.Game/Rulesets/Mods/ModWithVisibilityAdjustment.cs
+++ b/osu.Game/Rulesets/Mods/ModWithVisibilityAdjustment.cs
@@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Mods
/// A which applies visibility adjustments to s
/// with an optional increased visibility adjustment depending on the user's "increase first object visibility" setting.
///
- public abstract class ModWithVisibilityAdjustment : Mod, IReadFromConfig, IApplicableToBeatmap, IApplicableToDrawableHitObjects
+ public abstract class ModWithVisibilityAdjustment : Mod, IReadFromConfig, IApplicableToBeatmap, IApplicableToDrawableHitObject
{
///
/// The first adjustable object.
@@ -73,19 +73,16 @@ namespace osu.Game.Rulesets.Mods
}
}
- public virtual void ApplyToDrawableHitObjects(IEnumerable drawables)
+ public virtual void ApplyToDrawableHitObject(DrawableHitObject dho)
{
- foreach (var dho in drawables)
+ dho.ApplyCustomUpdateState += (o, state) =>
{
- dho.ApplyCustomUpdateState += (o, state) =>
- {
- // Increased visibility is applied to the entire first object, including all of its nested hitobjects.
- if (IncreaseFirstObjectVisibility.Value && isObjectEqualToOrNestedIn(o.HitObject, FirstObject))
- ApplyIncreasedVisibilityState(o, state);
- else
- ApplyNormalVisibilityState(o, state);
- };
- }
+ // Increased visibility is applied to the entire first object, including all of its nested hitobjects.
+ if (IncreaseFirstObjectVisibility.Value && isObjectEqualToOrNestedIn(o.HitObject, FirstObject))
+ ApplyIncreasedVisibilityState(o, state);
+ else
+ ApplyNormalVisibilityState(o, state);
+ };
}
///
diff --git a/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs b/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs
index bc8994bbe5..d3ee10dd23 100644
--- a/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs
+++ b/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs
@@ -6,7 +6,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
-using JetBrains.Annotations;
using osu.Game.Input.Handlers;
using osu.Game.Replays;
@@ -117,7 +116,7 @@ namespace osu.Game.Rulesets.Replays
}
}
- protected virtual bool IsImportant([NotNull] TFrame frame) => false;
+ protected virtual bool IsImportant(TFrame frame) => false;
///
/// Update the current frame based on an incoming time value.
diff --git a/osu.Game/Rulesets/RulesetStore.cs b/osu.Game/Rulesets/RulesetStore.cs
index 0a34ca9598..1f12f3dfeb 100644
--- a/osu.Game/Rulesets/RulesetStore.cs
+++ b/osu.Game/Rulesets/RulesetStore.cs
@@ -96,13 +96,25 @@ namespace osu.Game.Rulesets
context.SaveChanges();
- // add any other modes
var existingRulesets = context.RulesetInfo.ToList();
+ // add any other rulesets which have assemblies present but are not yet in the database.
foreach (var r in instances.Where(r => !(r is ILegacyRuleset)))
{
if (existingRulesets.FirstOrDefault(ri => ri.InstantiationInfo.Equals(r.RulesetInfo.InstantiationInfo, StringComparison.Ordinal)) == null)
- context.RulesetInfo.Add(r.RulesetInfo);
+ {
+ var existingSameShortName = existingRulesets.FirstOrDefault(ri => ri.ShortName == r.RulesetInfo.ShortName);
+
+ if (existingSameShortName != null)
+ {
+ // even if a matching InstantiationInfo was not found, there may be an existing ruleset with the same ShortName.
+ // this generally means the user or ruleset provider has renamed their dll but the underlying ruleset is *likely* the same one.
+ // in such cases, update the instantiation info of the existing entry to point to the new one.
+ existingSameShortName.InstantiationInfo = r.RulesetInfo.InstantiationInfo;
+ }
+ else
+ context.RulesetInfo.Add(r.RulesetInfo);
+ }
}
context.SaveChanges();
diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs
index 0ab8b94e3f..8dcc1ca164 100644
--- a/osu.Game/Rulesets/UI/DrawableRuleset.cs
+++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs
@@ -199,8 +199,11 @@ namespace osu.Game.Rulesets.UI
Playfield.PostProcess();
- foreach (var mod in Mods.OfType())
- mod.ApplyToDrawableHitObjects(Playfield.AllHitObjects);
+ foreach (var mod in Mods.OfType())
+ {
+ foreach (var drawableHitObject in Playfield.AllHitObjects)
+ mod.ApplyToDrawableHitObject(drawableHitObject);
+ }
}
public override void RequestResume(Action continueResume)
diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs
index b154288dba..52aecb27de 100644
--- a/osu.Game/Rulesets/UI/Playfield.cs
+++ b/osu.Game/Rulesets/UI/Playfield.cs
@@ -356,8 +356,8 @@ namespace osu.Game.Rulesets.UI
// This is done before Apply() so that the state is updated once when the hitobject is applied.
if (mods != null)
{
- foreach (var m in mods.OfType())
- m.ApplyToDrawableHitObjects(dho.Yield());
+ foreach (var m in mods.OfType())
+ m.ApplyToDrawableHitObject(dho);
}
}
diff --git a/osu.Game/Scoring/ScoreRank.cs b/osu.Game/Scoring/ScoreRank.cs
index 696d493830..f3b4551ff8 100644
--- a/osu.Game/Scoring/ScoreRank.cs
+++ b/osu.Game/Scoring/ScoreRank.cs
@@ -1,10 +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 System;
using System.ComponentModel;
+using osu.Framework.Localisation;
+using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Scoring
{
+ [LocalisableEnum(typeof(ScoreRankEnumLocalisationMapper))]
public enum ScoreRank
{
[Description(@"D")]
@@ -31,4 +35,40 @@ namespace osu.Game.Scoring
[Description(@"SS+")]
XH,
}
+
+ public class ScoreRankEnumLocalisationMapper : EnumLocalisationMapper
+ {
+ public override LocalisableString Map(ScoreRank value)
+ {
+ switch (value)
+ {
+ case ScoreRank.XH:
+ return BeatmapsStrings.RankXH;
+
+ case ScoreRank.X:
+ return BeatmapsStrings.RankX;
+
+ case ScoreRank.SH:
+ return BeatmapsStrings.RankSH;
+
+ case ScoreRank.S:
+ return BeatmapsStrings.RankS;
+
+ case ScoreRank.A:
+ return BeatmapsStrings.RankA;
+
+ case ScoreRank.B:
+ return BeatmapsStrings.RankB;
+
+ case ScoreRank.C:
+ return BeatmapsStrings.RankC;
+
+ case ScoreRank.D:
+ return BeatmapsStrings.RankD;
+
+ default:
+ throw new ArgumentOutOfRangeException(nameof(value), value, null);
+ }
+ }
+ }
}
diff --git a/osu.Game/Screens/OnlinePlay/Components/ParticipantsList.cs b/osu.Game/Screens/OnlinePlay/Components/ParticipantsList.cs
index 9aceb39a27..e531ddb0ec 100644
--- a/osu.Game/Screens/OnlinePlay/Components/ParticipantsList.cs
+++ b/osu.Game/Screens/OnlinePlay/Components/ParticipantsList.cs
@@ -5,7 +5,6 @@ using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Threading;
using osu.Game.Users;
@@ -91,7 +90,7 @@ namespace osu.Game.Screens.OnlinePlay.Components
});
}
- private class UserTile : CompositeDrawable, IHasTooltip
+ private class UserTile : CompositeDrawable
{
public User User
{
@@ -99,8 +98,6 @@ namespace osu.Game.Screens.OnlinePlay.Components
set => avatar.User = value;
}
- public string TooltipText => User?.Username ?? string.Empty;
-
private readonly UpdateableAvatar avatar;
public UserTile()
@@ -116,7 +113,7 @@ namespace osu.Game.Screens.OnlinePlay.Components
RelativeSizeAxes = Axes.Both,
Colour = Color4Extensions.FromHex(@"27252d"),
},
- avatar = new UpdateableAvatar { RelativeSizeAxes = Axes.Both },
+ avatar = new UpdateableAvatar(showUsernameTooltip: true) { RelativeSizeAxes = Axes.Both },
};
}
}
diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs
index a2ef715367..567ea6b988 100644
--- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs
+++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs
@@ -4,6 +4,7 @@
using System;
using System.Diagnostics;
using System.Linq;
+using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Screens;
@@ -54,9 +55,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
return new PlaylistsResultsScreen(score, RoomId.Value.Value, PlaylistItem, true);
}
- protected override void PrepareScoreForResults()
+ protected override async Task PrepareScoreForResultsAsync(Score score)
{
- base.PrepareScoreForResults();
+ await base.PrepareScoreForResultsAsync(score).ConfigureAwait(false);
Score.ScoreInfo.TotalScore = (int)Math.Round(ScoreProcessor.GetStandardisedScore());
}
diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs
index f9036780aa..cadcc474b2 100644
--- a/osu.Game/Screens/Play/Player.cs
+++ b/osu.Game/Screens/Play/Player.cs
@@ -181,12 +181,6 @@ namespace osu.Game.Screens.Play
DrawableRuleset.SetRecordTarget(Score);
}
- protected virtual void PrepareScoreForResults()
- {
- // perform one final population to ensure everything is up-to-date.
- ScoreProcessor.PopulateScore(Score.ScoreInfo);
- }
-
[BackgroundDependencyLoader(true)]
private void load(AudioManager audio, OsuConfigManager config, OsuGameBase game)
{
@@ -301,12 +295,12 @@ namespace osu.Game.Screens.Play
DimmableStoryboard.HasStoryboardEnded.ValueChanged += storyboardEnded =>
{
- if (storyboardEnded.NewValue && completionProgressDelegate == null)
- updateCompletionState();
+ if (storyboardEnded.NewValue)
+ progressToResults(true);
};
// Bind the judgement processors to ourselves
- ScoreProcessor.HasCompleted.BindValueChanged(_ => updateCompletionState());
+ ScoreProcessor.HasCompleted.BindValueChanged(scoreCompletionChanged);
HealthProcessor.Failed += onFail;
foreach (var mod in Mods.Value.OfType())
@@ -380,7 +374,7 @@ namespace osu.Game.Screens.Play
},
skipOutroOverlay = new SkipOverlay(Beatmap.Value.Storyboard.LatestEventTime ?? 0)
{
- RequestSkip = () => updateCompletionState(true),
+ RequestSkip = () => progressToResults(false),
Alpha = 0
},
FailOverlay = new FailOverlay
@@ -512,19 +506,25 @@ namespace osu.Game.Screens.Play
}
///
- /// Exits the .
+ /// Attempts to complete a user request to exit gameplay.
///
+ ///
+ ///
+ /// - This should only be called in response to a user interaction. Exiting is not guaranteed.
+ /// - This will interrupt any pending progression to the results screen, even if the transition has begun.
+ ///
+ ///
///
/// Whether the pause or fail dialog should be shown before performing an exit.
- /// If true and a dialog is not yet displayed, the exit will be blocked the the relevant dialog will display instead.
+ /// If and a dialog is not yet displayed, the exit will be blocked and the relevant dialog will display instead.
///
protected void PerformExit(bool showDialogFirst)
{
- // if a restart has been requested, cancel any pending completion (user has shown intent to restart).
- completionProgressDelegate?.Cancel();
+ // if an exit has been requested, cancel any pending completion (the user has shown intention to exit).
+ resultsDisplayDelegate?.Cancel();
- // there is a chance that the exit was performed after the transition to results has started.
- // we want to give the user what they want, so forcefully return to this screen (to proceed with the upwards exit process).
+ // there is a chance that an exit request occurs after the transition to results has already started.
+ // even in such a case, the user has shown intent, so forcefully return to this screen (to proceed with the upwards exit process).
if (!this.IsCurrentScreen())
{
ValidForResume = false;
@@ -547,7 +547,7 @@ namespace osu.Game.Screens.Play
return;
}
- // there's a chance the pausing is not supported in the current state, at which point immediate exit should be preferred.
+ // even if this call has requested a dialog, there is a chance the current player mode doesn't support pausing.
if (pausingSupportedByCurrentState)
{
// in the case a dialog needs to be shown, attempt to pause and show it.
@@ -555,14 +555,12 @@ namespace osu.Game.Screens.Play
Pause();
return;
}
-
- // if the score is ready for display but results screen has not been pushed yet (e.g. storyboard is still playing beyond gameplay), then transition to results screen instead of exiting.
- if (prepareScoreForDisplayTask != null && completionProgressDelegate == null)
- {
- updateCompletionState(true);
- }
}
+ // The actual exit is performed if
+ // - the pause / fail dialog was not requested
+ // - the pause / fail dialog was requested but is already displayed (user showing intention to exit).
+ // - the pause / fail dialog was requested but couldn't be displayed due to the type or state of this Player instance.
this.Exit();
}
@@ -626,98 +624,141 @@ namespace osu.Game.Screens.Play
PerformExit(false);
}
- private ScheduledDelegate completionProgressDelegate;
+ ///
+ /// This delegate, when set, means the results screen has been queued to appear.
+ /// The display of the results screen may be delayed by any work being done in .
+ ///
+ ///
+ /// Once set, this can *only* be cancelled by rewinding, ie. if ScoreProcessor.HasCompleted becomes .
+ /// Even if the user requests an exit, it will forcefully proceed to the results screen (see special case in ).
+ ///
+ private ScheduledDelegate resultsDisplayDelegate;
+
+ ///
+ /// A task which asynchronously prepares a completed score for display at results.
+ /// This may include performing net requests or importing the score into the database, generally to ensure things are in a sane state for the play session.
+ ///
private Task prepareScoreForDisplayTask;
///
/// Handles changes in player state which may progress the completion of gameplay / this screen's lifetime.
///
- /// If in a state where a storyboard outro is to be played, offers the choice of skipping beyond it.
/// Thrown if this method is called more than once without changing state.
- private void updateCompletionState(bool skipStoryboardOutro = false)
+ private void scoreCompletionChanged(ValueChangedEvent completed)
{
- // screen may be in the exiting transition phase.
+ // If this player instance is in the middle of an exit, don't attempt any kind of state update.
if (!this.IsCurrentScreen())
return;
- if (!ScoreProcessor.HasCompleted.Value)
+ // Special case to handle rewinding post-completion. This is the only way already queued forward progress can be cancelled.
+ // TODO: Investigate whether this can be moved to a RewindablePlayer subclass or similar.
+ // Currently, even if this scenario is hit, prepareScoreForDisplay has already been queued (and potentially run).
+ // In scenarios where rewinding is possible (replay, spectating) this is a non-issue as no submission/import work is done,
+ // but it still doesn't feel right that this exists here.
+ if (!completed.NewValue)
{
- completionProgressDelegate?.Cancel();
- completionProgressDelegate = null;
+ resultsDisplayDelegate?.Cancel();
+ resultsDisplayDelegate = null;
+
ValidForResume = true;
skipOutroOverlay.Hide();
return;
}
- if (completionProgressDelegate != null)
- throw new InvalidOperationException($"{nameof(updateCompletionState)} was fired more than once");
-
// Only show the completion screen if the player hasn't failed
if (HealthProcessor.HasFailed)
return;
+ // Setting this early in the process means that even if something were to go wrong in the order of events following, there
+ // is no chance that a user could return to the (already completed) Player instance from a child screen.
ValidForResume = false;
- // ensure we are not writing to the replay any more, as we are about to consume and store the score.
+ // Ensure we are not writing to the replay any more, as we are about to consume and store the score.
DrawableRuleset.SetRecordTarget(null);
- if (!Configuration.ShowResults) return;
-
- prepareScoreForDisplayTask ??= Task.Run(async () =>
- {
- PrepareScoreForResults();
-
- try
- {
- await PrepareScoreForResultsAsync(Score).ConfigureAwait(false);
- }
- catch (Exception ex)
- {
- Logger.Error(ex, "Score preparation failed!");
- }
-
- try
- {
- await ImportScore(Score).ConfigureAwait(false);
- }
- catch (Exception ex)
- {
- Logger.Error(ex, "Score import failed!");
- }
-
- return Score.ScoreInfo;
- });
-
- if (skipStoryboardOutro)
- {
- scheduleCompletion();
+ if (!Configuration.ShowResults)
return;
- }
+
+ prepareScoreForDisplayTask ??= Task.Run(prepareScoreForResults);
bool storyboardHasOutro = DimmableStoryboard.ContentDisplayed && !DimmableStoryboard.HasStoryboardEnded.Value;
if (storyboardHasOutro)
{
+ // if the current beatmap has a storyboard, the progression to results will be handled by the storyboard ending
+ // or the user pressing the skip outro button.
skipOutroOverlay.Show();
return;
}
- using (BeginDelayedSequence(RESULTS_DISPLAY_DELAY))
- scheduleCompletion();
+ progressToResults(true);
}
- private void scheduleCompletion() => completionProgressDelegate = Schedule(() =>
+ ///
+ /// Asynchronously run score preparation operations (database import, online submission etc.).
+ ///
+ /// The final score.
+ private async Task prepareScoreForResults()
{
- if (!prepareScoreForDisplayTask.IsCompleted)
+ try
{
- scheduleCompletion();
- return;
+ await PrepareScoreForResultsAsync(Score).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ Logger.Error(ex, @"Score preparation failed!");
}
- // screen may be in the exiting transition phase.
- if (this.IsCurrentScreen())
+ try
+ {
+ await ImportScore(Score).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ Logger.Error(ex, @"Score import failed!");
+ }
+
+ return Score.ScoreInfo;
+ }
+
+ ///
+ /// Queue the results screen for display.
+ ///
+ ///
+ /// A final display will only occur once all work is completed in . This means that even after calling this method, the results screen will never be shown until ScoreProcessor.HasCompleted becomes .
+ ///
+ /// Calling this method multiple times will have no effect.
+ ///
+ /// Whether a minimum delay () should be added before the screen is displayed.
+ private void progressToResults(bool withDelay)
+ {
+ if (resultsDisplayDelegate != null)
+ // Note that if progressToResults is called one withDelay=true and then withDelay=false, this no-delay timing will not be
+ // accounted for. shouldn't be a huge concern (a user pressing the skip button after a results progression has already been queued
+ // may take x00 more milliseconds than expected in the very rare edge case).
+ //
+ // If required we can handle this more correctly by rescheduling here.
+ return;
+
+ double delay = withDelay ? RESULTS_DISPLAY_DELAY : 0;
+
+ resultsDisplayDelegate = new ScheduledDelegate(() =>
+ {
+ if (prepareScoreForDisplayTask?.IsCompleted != true)
+ // If the asynchronous preparation has not completed, keep repeating this delegate.
+ return;
+
+ resultsDisplayDelegate?.Cancel();
+
+ if (!this.IsCurrentScreen())
+ // This player instance may already be in the process of exiting.
+ return;
+
this.Push(CreateResults(prepareScoreForDisplayTask.Result));
- });
+ }, Time.Current + delay, 50);
+
+ Scheduler.Add(resultsDisplayDelegate);
+ }
protected override bool OnScroll(ScrollEvent e) => mouseWheelDisabled.Value && !GameplayClockContainer.IsPaused.Value;
@@ -915,13 +956,6 @@ namespace osu.Game.Screens.Play
{
screenSuspension?.Expire();
- if (completionProgressDelegate != null && !completionProgressDelegate.Cancelled && !completionProgressDelegate.Completed)
- {
- // proceed to result screen if beatmap already finished playing
- completionProgressDelegate.RunTask();
- return true;
- }
-
// EndPlaying() is typically called from ReplayRecorder.Dispose(). Disposal is currently asynchronous.
// To resolve test failures, forcefully end playing synchronously when this screen exits.
// Todo: Replace this with a more permanent solution once osu-framework has a synchronous cleanup method.
@@ -984,7 +1018,13 @@ namespace osu.Game.Screens.Play
///
/// The to prepare.
/// A task that prepares the provided score. On completion, the score is assumed to be ready for display.
- protected virtual Task PrepareScoreForResultsAsync(Score score) => Task.CompletedTask;
+ protected virtual Task PrepareScoreForResultsAsync(Score score)
+ {
+ // perform one final population to ensure everything is up-to-date.
+ ScoreProcessor.PopulateScore(score.ScoreInfo);
+
+ return Task.CompletedTask;
+ }
///
/// Creates the for a .
diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs
index ca041da801..8a31e4576a 100644
--- a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs
+++ b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs
@@ -25,7 +25,7 @@ namespace osu.Game.Storyboards.Drawables
///
public IBindable HasStoryboardEnded => hasStoryboardEnded;
- private readonly BindableBool hasStoryboardEnded = new BindableBool();
+ private readonly BindableBool hasStoryboardEnded = new BindableBool(true);
protected override Container Content { get; }
diff --git a/osu.Game/Users/Drawables/ClickableAvatar.cs b/osu.Game/Users/Drawables/ClickableAvatar.cs
index 0fca9c7c9b..c3bf740108 100644
--- a/osu.Game/Users/Drawables/ClickableAvatar.cs
+++ b/osu.Game/Users/Drawables/ClickableAvatar.cs
@@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
-using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Textures;
@@ -13,16 +12,32 @@ namespace osu.Game.Users.Drawables
{
public class ClickableAvatar : Container
{
+ private const string default_tooltip_text = "view profile";
+
///
/// Whether to open the user's profile when clicked.
///
- public readonly BindableBool OpenOnClick = new BindableBool(true);
+ public bool OpenOnClick
+ {
+ set => clickableArea.Enabled.Value = value;
+ }
+
+ ///
+ /// By default, the tooltip will show "view profile" as avatars are usually displayed next to a username.
+ /// Setting this to true exposes the username via tooltip for special cases where this is not true.
+ ///
+ public bool ShowUsernameTooltip
+ {
+ set => clickableArea.TooltipText = value ? (user?.Username ?? string.Empty) : default_tooltip_text;
+ }
private readonly User user;
[Resolved(CanBeNull = true)]
private OsuGame game { get; set; }
+ private readonly ClickableArea clickableArea;
+
///
/// A clickable avatar for the specified user, with UI sounds included.
/// If is true, clicking will open the user's profile.
@@ -31,35 +46,35 @@ namespace osu.Game.Users.Drawables
public ClickableAvatar(User user = null)
{
this.user = user;
- }
- [BackgroundDependencyLoader]
- private void load(LargeTextureStore textures)
- {
- ClickableArea clickableArea;
Add(clickableArea = new ClickableArea
{
RelativeSizeAxes = Axes.Both,
Action = openProfile
});
+ }
+ [BackgroundDependencyLoader]
+ private void load(LargeTextureStore textures)
+ {
LoadComponentAsync(new DrawableAvatar(user), clickableArea.Add);
-
- clickableArea.Enabled.BindTo(OpenOnClick);
}
private void openProfile()
{
- if (!OpenOnClick.Value)
- return;
-
if (user?.Id > 1)
game?.ShowUser(user.Id);
}
private class ClickableArea : OsuClickableContainer
{
- public override string TooltipText => Enabled.Value ? @"view profile" : null;
+ private string tooltip = default_tooltip_text;
+
+ public override string TooltipText
+ {
+ get => Enabled.Value ? tooltip : null;
+ set => tooltip = value;
+ }
protected override bool OnClick(ClickEvent e)
{
diff --git a/osu.Game/Users/Drawables/UpdateableAvatar.cs b/osu.Game/Users/Drawables/UpdateableAvatar.cs
index 927e48cb56..df724404e9 100644
--- a/osu.Game/Users/Drawables/UpdateableAvatar.cs
+++ b/osu.Game/Users/Drawables/UpdateableAvatar.cs
@@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
@@ -45,33 +44,38 @@ namespace osu.Game.Users.Drawables
protected override double LoadDelay => 200;
- ///
- /// Whether to show a default guest representation on null user (as opposed to nothing).
- ///
- public bool ShowGuestOnNull = true;
+ private readonly bool openOnClick;
+ private readonly bool showUsernameTooltip;
+ private readonly bool showGuestOnNull;
///
- /// Whether to open the user's profile when clicked.
+ /// Construct a new UpdateableAvatar.
///
- public readonly BindableBool OpenOnClick = new BindableBool(true);
-
- public UpdateableAvatar(User user = null)
+ /// The initial user to display.
+ /// Whether to open the user's profile when clicked.
+ /// Whether to show the username rather than "view profile" on the tooltip.
+ /// Whether to show a default guest representation on null user (as opposed to nothing).
+ public UpdateableAvatar(User user = null, bool openOnClick = true, bool showUsernameTooltip = false, bool showGuestOnNull = true)
{
+ this.openOnClick = openOnClick;
+ this.showUsernameTooltip = showUsernameTooltip;
+ this.showGuestOnNull = showGuestOnNull;
+
User = user;
}
protected override Drawable CreateDrawable(User user)
{
- if (user == null && !ShowGuestOnNull)
+ if (user == null && !showGuestOnNull)
return null;
var avatar = new ClickableAvatar(user)
{
+ OpenOnClick = openOnClick,
+ ShowUsernameTooltip = showUsernameTooltip,
RelativeSizeAxes = Axes.Both,
};
- avatar.OpenOnClick.BindTo(OpenOnClick);
-
return avatar;
}
}
diff --git a/osu.Game/Users/ExtendedUserPanel.cs b/osu.Game/Users/ExtendedUserPanel.cs
index 2604815751..24317e6069 100644
--- a/osu.Game/Users/ExtendedUserPanel.cs
+++ b/osu.Game/Users/ExtendedUserPanel.cs
@@ -48,11 +48,7 @@ namespace osu.Game.Users
statusIcon.FinishTransforms();
}
- protected UpdateableAvatar CreateAvatar() => new UpdateableAvatar
- {
- User = User,
- OpenOnClick = { Value = false }
- };
+ protected UpdateableAvatar CreateAvatar() => new UpdateableAvatar(User, false);
protected UpdateableFlag CreateFlag() => new UpdateableFlag(User.Country)
{
diff --git a/osu.Game/Utils/ModUtils.cs b/osu.Game/Utils/ModUtils.cs
index 1c3558fc90..98766cb844 100644
--- a/osu.Game/Utils/ModUtils.cs
+++ b/osu.Game/Utils/ModUtils.cs
@@ -60,6 +60,9 @@ namespace osu.Game.Utils
{
foreach (var invalid in combination.Where(m => type.IsInstanceOfType(m)))
{
+ if (invalid == mod)
+ continue;
+
invalidMods ??= new List();
invalidMods.Add(invalid);
}
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index 560e5e2493..fe7214de38 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -34,7 +34,7 @@
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
diff --git a/osu.iOS.props b/osu.iOS.props
index 7b3033db9c..01c9b27cc7 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -70,7 +70,7 @@
-
+
@@ -93,7 +93,7 @@
-
+