diff --git a/.gitignore b/.gitignore
index d122d25054..de6a3ac848 100644
--- a/.gitignore
+++ b/.gitignore
@@ -336,3 +336,6 @@ inspectcode
/BenchmarkDotNet.Artifacts
*.GeneratedMSBuildEditorConfig.editorconfig
+
+# Fody (pulled in by Realm) - schema file
+FodyWeavers.xsd
diff --git a/FodyWeavers.xml b/FodyWeavers.xml
new file mode 100644
index 0000000000..cc07b89533
--- /dev/null
+++ b/FodyWeavers.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/README.md b/README.md
index 3054f19e79..2213b42121 100644
--- a/README.md
+++ b/README.md
@@ -43,7 +43,7 @@ If your platform is not listed above, there is still a chance you can manually b
osu! is designed to have extensible modular gameplay modes, called "rulesets". Building one of these allows a developer to harness the power of osu! for their own game style. To get started working on a ruleset, we have some templates available [here](https://github.com/ppy/osu/tree/master/Templates).
-You can see some examples of custom rulesets by visiting the [custom ruleset directory](https://github.com/ppy/osu/issues/5852).
+You can see some examples of custom rulesets by visiting the [custom ruleset directory](https://github.com/ppy/osu/discussions/13096).
## Developing osu!
diff --git a/osu.Android.props b/osu.Android.props
index 490e43b5e6..1dc99bb60a 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -51,7 +51,11 @@
-
-
+
+
+
+
+
+
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/Editor/TestSceneOsuEditorHitAnimations.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorHitAnimations.cs
new file mode 100644
index 0000000000..7ffa2c1f94
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorHitAnimations.cs
@@ -0,0 +1,114 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Transforms;
+using osu.Framework.Testing;
+using osu.Framework.Utils;
+using osu.Game.Configuration;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Osu.Edit;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
+
+namespace osu.Game.Rulesets.Osu.Tests.Editor
+{
+ [TestFixture]
+ public class TestSceneOsuEditorHitAnimations : TestSceneOsuEditor
+ {
+ [Resolved]
+ private OsuConfigManager config { get; set; }
+
+ [Test]
+ public void TestHitCircleAnimationDisable()
+ {
+ HitCircle hitCircle = null;
+ DrawableHitCircle drawableHitCircle = null;
+
+ AddStep("retrieve first hit circle", () => hitCircle = getHitCircle(0));
+ toggleAnimations(true);
+ seekSmoothlyTo(() => hitCircle.StartTime + 10);
+
+ AddStep("retrieve drawable", () => drawableHitCircle = (DrawableHitCircle)getDrawableObjectFor(hitCircle));
+ assertFutureTransforms(() => drawableHitCircle.CirclePiece, true);
+
+ AddStep("retrieve second hit circle", () => hitCircle = getHitCircle(1));
+ toggleAnimations(false);
+ seekSmoothlyTo(() => hitCircle.StartTime + 10);
+
+ AddStep("retrieve drawable", () => drawableHitCircle = (DrawableHitCircle)getDrawableObjectFor(hitCircle));
+ assertFutureTransforms(() => drawableHitCircle.CirclePiece, false);
+ AddAssert("hit circle has longer fade-out applied", () =>
+ {
+ var alphaTransform = drawableHitCircle.Transforms.Last(t => t.TargetMember == nameof(Alpha));
+ return alphaTransform.EndTime - alphaTransform.StartTime == DrawableOsuEditorRuleset.EDITOR_HIT_OBJECT_FADE_OUT_EXTENSION;
+ });
+ }
+
+ [Test]
+ public void TestSliderAnimationDisable()
+ {
+ Slider slider = null;
+ DrawableSlider drawableSlider = null;
+ DrawableSliderRepeat sliderRepeat = null;
+
+ AddStep("retrieve first slider with repeats", () => slider = getSliderWithRepeats(0));
+ toggleAnimations(true);
+ seekSmoothlyTo(() => slider.StartTime + slider.SpanDuration + 10);
+
+ retrieveDrawables();
+ assertFutureTransforms(() => sliderRepeat, true);
+
+ AddStep("retrieve second slider with repeats", () => slider = getSliderWithRepeats(1));
+ toggleAnimations(false);
+ seekSmoothlyTo(() => slider.StartTime + slider.SpanDuration + 10);
+
+ retrieveDrawables();
+ assertFutureTransforms(() => sliderRepeat.Arrow, false);
+ seekSmoothlyTo(() => slider.GetEndTime());
+ AddAssert("slider has longer fade-out applied", () =>
+ {
+ var alphaTransform = drawableSlider.Transforms.Last(t => t.TargetMember == nameof(Alpha));
+ return alphaTransform.EndTime - alphaTransform.StartTime == DrawableOsuEditorRuleset.EDITOR_HIT_OBJECT_FADE_OUT_EXTENSION;
+ });
+
+ void retrieveDrawables() =>
+ AddStep("retrieve drawables", () =>
+ {
+ drawableSlider = (DrawableSlider)getDrawableObjectFor(slider);
+ sliderRepeat = (DrawableSliderRepeat)getDrawableObjectFor(slider.NestedHitObjects.OfType().First());
+ });
+ }
+
+ private HitCircle getHitCircle(int index)
+ => EditorBeatmap.HitObjects.OfType().ElementAt(index);
+
+ private Slider getSliderWithRepeats(int index)
+ => EditorBeatmap.HitObjects.OfType().Where(s => s.RepeatCount >= 1).ElementAt(index);
+
+ private DrawableHitObject getDrawableObjectFor(HitObject hitObject)
+ => this.ChildrenOfType().Single(ho => ho.HitObject == hitObject);
+
+ private IEnumerable getTransformsRecursively(Drawable drawable)
+ => drawable.ChildrenOfType().SelectMany(d => d.Transforms);
+
+ private void toggleAnimations(bool enabled)
+ => AddStep($"toggle animations {(enabled ? "on" : "off")}", () => config.SetValue(OsuSetting.EditorHitAnimations, enabled));
+
+ private void seekSmoothlyTo(Func targetTime)
+ {
+ AddStep("seek smoothly", () => EditorClock.SeekSmoothlyTo(targetTime.Invoke()));
+ AddUntilStep("wait for seek", () => Precision.AlmostEquals(targetTime.Invoke(), EditorClock.CurrentTime));
+ }
+
+ private void assertFutureTransforms(Func getDrawable, bool hasFutureTransforms)
+ => AddAssert($"object {(hasFutureTransforms ? "has" : "has no")} future transforms",
+ () => getTransformsRecursively(getDrawable()).Any(t => t.EndTime >= EditorClock.CurrentTime) == hasFutureTransforms);
+ }
+}
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/Edit/DrawableOsuEditorRuleset.cs b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditorRuleset.cs
index aeeae84d14..0e61c02e2d 100644
--- a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditorRuleset.cs
+++ b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditorRuleset.cs
@@ -20,6 +20,12 @@ namespace osu.Game.Rulesets.Osu.Edit
{
public class DrawableOsuEditorRuleset : DrawableOsuRuleset
{
+ ///
+ /// Hit objects are intentionally made to fade out at a constant slower rate than in gameplay.
+ /// This allows a mapper to gain better historical context and use recent hitobjects as reference / snap points.
+ ///
+ public const double EDITOR_HIT_OBJECT_FADE_OUT_EXTENSION = 700;
+
public DrawableOsuEditorRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods)
: base(ruleset, beatmap, mods)
{
@@ -46,12 +52,6 @@ namespace osu.Game.Rulesets.Osu.Edit
d.ApplyCustomUpdateState += updateState;
}
- ///
- /// Hit objects are intentionally made to fade out at a constant slower rate than in gameplay.
- /// This allows a mapper to gain better historical context and use recent hitobjects as reference / snap points.
- ///
- private const double editor_hit_object_fade_out_extension = 700;
-
private void updateState(DrawableHitObject hitObject, ArmedState state)
{
if (state == ArmedState.Idle || hitAnimations.Value)
@@ -60,7 +60,7 @@ namespace osu.Game.Rulesets.Osu.Edit
if (hitObject is DrawableHitCircle circle)
{
circle.ApproachCircle
- .FadeOutFromOne(editor_hit_object_fade_out_extension * 4)
+ .FadeOutFromOne(EDITOR_HIT_OBJECT_FADE_OUT_EXTENSION * 4)
.Expire();
circle.ApproachCircle.ScaleTo(1.1f, 300, Easing.OutQuint);
@@ -69,14 +69,20 @@ namespace osu.Game.Rulesets.Osu.Edit
if (hitObject is IHasMainCirclePiece mainPieceContainer)
{
// clear any explode animation logic.
- mainPieceContainer.CirclePiece.ApplyTransformsAt(hitObject.HitStateUpdateTime, true);
- mainPieceContainer.CirclePiece.ClearTransformsAfter(hitObject.HitStateUpdateTime, true);
+ // this is scheduled after children to ensure that the clear happens after invocations of ApplyCustomUpdateState on the circle piece's nested skinnables.
+ ScheduleAfterChildren(() =>
+ {
+ if (hitObject.HitObject == null) return;
+
+ mainPieceContainer.CirclePiece.ApplyTransformsAt(hitObject.StateUpdateTime, true);
+ mainPieceContainer.CirclePiece.ClearTransformsAfter(hitObject.StateUpdateTime, true);
+ });
}
if (hitObject is DrawableSliderRepeat repeat)
{
- repeat.Arrow.ApplyTransformsAt(hitObject.HitStateUpdateTime, true);
- repeat.Arrow.ClearTransformsAfter(hitObject.HitStateUpdateTime, true);
+ repeat.Arrow.ApplyTransformsAt(hitObject.StateUpdateTime, true);
+ repeat.Arrow.ClearTransformsAfter(hitObject.StateUpdateTime, true);
}
// adjust the visuals of top-level object types to make them stay on screen for longer than usual.
@@ -93,7 +99,7 @@ namespace osu.Game.Rulesets.Osu.Edit
hitObject.RemoveTransform(existing);
using (hitObject.BeginAbsoluteSequence(hitObject.HitStateUpdateTime))
- hitObject.FadeOut(editor_hit_object_fade_out_extension).Expire();
+ hitObject.FadeOut(EDITOR_HIT_OBJECT_FADE_OUT_EXTENSION).Expire();
break;
}
}
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
new file mode 100644
index 0000000000..526e29ad53
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModApproachDifferent.cs
@@ -0,0 +1,100 @@
+// 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.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Sprites;
+using osu.Game.Configuration;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
+
+namespace osu.Game.Rulesets.Osu.Mods
+{
+ public class OsuModApproachDifferent : Mod, IApplicableToDrawableHitObject, IMutateApproachCircles
+ {
+ public override string Name => "Approach Different";
+ public override string Acronym => "AD";
+ public override string Description => "Never trust the approach circles...";
+ 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)
+ {
+ Precision = 0.1f,
+ MinValue = 2,
+ MaxValue = 10,
+ };
+
+ [SettingSource("Style", "Change the animation style of the approach circles.", 1)]
+ public Bindable Style { get; } = new Bindable();
+
+ public void ApplyToDrawableHitObject(DrawableHitObject drawable)
+ {
+ drawable.ApplyCustomUpdateState += (drawableObject, state) =>
+ {
+ if (!(drawableObject is DrawableHitCircle drawableHitCircle)) return;
+
+ var hitCircle = drawableHitCircle.HitObject;
+
+ 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));
+ };
+ }
+
+ private Easing getEasing(AnimationStyle style)
+ {
+ switch (style)
+ {
+ default:
+ return Easing.None;
+
+ case AnimationStyle.Accelerate1:
+ return Easing.In;
+
+ case AnimationStyle.Accelerate2:
+ return Easing.InCubic;
+
+ case AnimationStyle.Accelerate3:
+ return Easing.InQuint;
+
+ case AnimationStyle.Gravity:
+ return Easing.InBack;
+
+ case AnimationStyle.Decelerate1:
+ return Easing.Out;
+
+ case AnimationStyle.Decelerate2:
+ return Easing.OutCubic;
+
+ case AnimationStyle.Decelerate3:
+ return Easing.OutQuint;
+
+ case AnimationStyle.InOut1:
+ return Easing.InOutCubic;
+
+ case AnimationStyle.InOut2:
+ return Easing.InOutQuint;
+ }
+ }
+
+ public enum AnimationStyle
+ {
+ Gravity,
+ InOut1,
+ InOut2,
+ Accelerate1,
+ Accelerate2,
+ Accelerate3,
+ Decelerate1,
+ Decelerate2,
+ Decelerate3,
+ }
+ }
+}
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..16b38cd0b1 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs
@@ -11,15 +11,16 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.Skinning;
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;
@@ -110,6 +111,9 @@ namespace osu.Game.Rulesets.Osu.Mods
// hide elements we don't care about.
// todo: hide background
+ spinner.Body.OnSkinChanged += () => hideSpinnerApproachCircle(spinner);
+ hideSpinnerApproachCircle(spinner);
+
using (spinner.BeginAbsoluteSequence(fadeStartTime))
spinner.FadeOut(fadeDuration);
@@ -160,5 +164,15 @@ namespace osu.Game.Rulesets.Osu.Mods
}
}
}
+
+ private static void hideSpinnerApproachCircle(DrawableSpinner spinner)
+ {
+ var approachCircle = (spinner.Body.Drawable as IHasApproachCircle)?.ApproachCircle;
+ if (approachCircle == null)
+ return;
+
+ using (spinner.BeginAbsoluteSequence(spinner.HitObject.StartTime - spinner.HitObject.TimePreempt))
+ approachCircle.Hide();
+ }
}
}
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..46fc8f99b2 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
@@ -12,6 +12,7 @@ using osu.Framework.Input.Bindings;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Judgements;
+using osu.Game.Rulesets.Osu.Skinning;
using osu.Game.Rulesets.Osu.Skinning.Default;
using osu.Game.Rulesets.Scoring;
using osu.Game.Skinning;
@@ -19,7 +20,7 @@ using osuTK;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
- public class DrawableHitCircle : DrawableOsuHitObject, IHasMainCirclePiece
+ public class DrawableHitCircle : DrawableOsuHitObject, IHasMainCirclePiece, IHasApproachCircle
{
public OsuAction? HitAction => HitArea.HitAction;
protected virtual OsuSkinComponents CirclePieceComponent => OsuSkinComponents.HitCircle;
@@ -28,6 +29,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public HitReceptor HitArea { get; private set; }
public SkinnableDrawable CirclePiece { get; private set; }
+ Drawable IHasApproachCircle.ApproachCircle => ApproachCircle;
+
private Container scaleContainer;
private InputManager inputManager;
@@ -172,6 +175,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 +186,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.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
index 19cee61f26..ec87d3bfdf 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
@@ -29,6 +29,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public new OsuSpinnerJudgementResult Result => (OsuSpinnerJudgementResult)base.Result;
+ public SkinnableDrawable Body { get; private set; }
+
public SpinnerRotationTracker RotationTracker { get; private set; }
private SpinnerSpmCalculator spmCalculator;
@@ -86,7 +88,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
RelativeSizeAxes = Axes.Y,
Children = new Drawable[]
{
- new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SpinnerBody), _ => new DefaultSpinner()),
+ Body = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SpinnerBody), _ => new DefaultSpinner()),
RotationTracker = new SpinnerRotationTracker(this)
}
},
diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs
index 69e22dc45d..5f37b0d040 100644
--- a/osu.Game.Rulesets.Osu/OsuRuleset.cs
+++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs
@@ -187,6 +187,7 @@ namespace osu.Game.Rulesets.Osu
new MultiMod(new ModWindUp(), new ModWindDown()),
new OsuModTraceable(),
new OsuModBarrelRoll(),
+ new OsuModApproachDifferent(),
};
case ModType.System:
diff --git a/osu.Game.Rulesets.Osu/OsuSkinComponents.cs b/osu.Game.Rulesets.Osu/OsuSkinComponents.cs
index fcb544fa5b..46e501758b 100644
--- a/osu.Game.Rulesets.Osu/OsuSkinComponents.cs
+++ b/osu.Game.Rulesets.Osu/OsuSkinComponents.cs
@@ -10,7 +10,6 @@ namespace osu.Game.Rulesets.Osu
Cursor,
CursorTrail,
SliderScorePoint,
- ApproachCircle,
ReverseArrow,
HitCircleText,
SliderHeadHitCircle,
diff --git a/osu.Game.Rulesets.Osu/Skinning/IHasApproachCircle.cs b/osu.Game.Rulesets.Osu/Skinning/IHasApproachCircle.cs
new file mode 100644
index 0000000000..7fbc5b144b
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Skinning/IHasApproachCircle.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 osu.Framework.Graphics;
+
+namespace osu.Game.Rulesets.Osu.Skinning
+{
+ ///
+ /// A common interface between implementations which provide an approach circle.
+ ///
+ public interface IHasApproachCircle
+ {
+ ///
+ /// The approach circle drawable.
+ ///
+ Drawable ApproachCircle { get; }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs
index 22fb3aab86..ae8d6a61f8 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs
@@ -55,28 +55,40 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
- Texture = source.GetTexture("spinner-bottom")
+ Texture = source.GetTexture("spinner-bottom"),
},
discTop = new Sprite
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
- Texture = source.GetTexture("spinner-top")
+ Texture = source.GetTexture("spinner-top"),
},
fixedMiddle = new Sprite
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
- Texture = source.GetTexture("spinner-middle")
+ Texture = source.GetTexture("spinner-middle"),
},
spinningMiddle = new Sprite
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
- Texture = source.GetTexture("spinner-middle2")
- }
+ Texture = source.GetTexture("spinner-middle2"),
+ },
}
});
+
+ if (!(source.FindProvider(s => s.GetTexture("spinner-top") != null) is DefaultLegacySkin))
+ {
+ AddInternal(ApproachCircle = new Sprite
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.Centre,
+ Texture = source.GetTexture("spinner-approachcircle"),
+ Scale = new Vector2(SPRITE_SCALE * 1.86f),
+ Y = SPINNER_Y_CENTRE,
+ });
+ }
}
protected override void UpdateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state)
diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyOldStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyOldStyleSpinner.cs
index d80e061662..cbe721d21d 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyOldStyleSpinner.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyOldStyleSpinner.cs
@@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{
spinnerBlink = source.GetConfig(OsuSkinConfiguration.SpinnerNoBlink)?.Value != true;
- AddRangeInternal(new Drawable[]
+ AddRangeInternal(new[]
{
new Sprite
{
@@ -68,6 +68,14 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
Origin = Anchor.TopLeft,
Scale = new Vector2(SPRITE_SCALE)
}
+ },
+ ApproachCircle = new Sprite
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.Centre,
+ Texture = source.GetTexture("spinner-approachcircle"),
+ Scale = new Vector2(SPRITE_SCALE * 1.86f),
+ Y = SPINNER_Y_CENTRE,
}
});
}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs
index 959589620b..317649785e 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs
@@ -15,8 +15,10 @@ using osuTK;
namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{
- public abstract class LegacySpinner : CompositeDrawable
+ public abstract class LegacySpinner : CompositeDrawable, IHasApproachCircle
{
+ public const float SPRITE_SCALE = 0.625f;
+
///
/// All constants are in osu!stable's gamefield space, which is shifted 16px downwards.
/// This offset is negated in both osu!stable and osu!lazer to bring all constants into window-space.
@@ -26,12 +28,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
protected const float SPINNER_Y_CENTRE = SPINNER_TOP_OFFSET + 219f;
- protected const float SPRITE_SCALE = 0.625f;
-
private const float spm_hide_offset = 50f;
protected DrawableSpinner DrawableSpinner { get; private set; }
+ public Drawable ApproachCircle { get; protected set; }
+
private Sprite spin;
private Sprite clear;
@@ -175,6 +177,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
spmCounter.MoveToOffset(new Vector2(0, -spm_hide_offset), d.HitObject.TimeFadeIn, Easing.Out);
}
+ using (BeginAbsoluteSequence(d.HitObject.StartTime))
+ ApproachCircle?.ScaleTo(SPRITE_SCALE * 0.1f, d.HitObject.Duration);
+
double spinFadeOutLength = Math.Min(400, d.HitObject.Duration);
using (BeginAbsoluteSequence(drawableHitObject.HitStateUpdateTime - spinFadeOutLength, true))
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/Chat/MessageFormatterTests.cs b/osu.Game.Tests/Chat/MessageFormatterTests.cs
index ecb37706b0..2c2c4dc24e 100644
--- a/osu.Game.Tests/Chat/MessageFormatterTests.cs
+++ b/osu.Game.Tests/Chat/MessageFormatterTests.cs
@@ -28,6 +28,8 @@ namespace osu.Game.Tests.Chat
[TestCase(LinkAction.OpenBeatmapSet, "123", "https://dev.ppy.sh/beatmapsets/123")]
[TestCase(LinkAction.OpenBeatmapSet, "123", "https://dev.ppy.sh/beatmapsets/123/whatever")]
[TestCase(LinkAction.External, "https://dev.ppy.sh/beatmapsets/abc", "https://dev.ppy.sh/beatmapsets/abc")]
+ [TestCase(LinkAction.External, "https://dev.ppy.sh/beatmapsets/discussions", "https://dev.ppy.sh/beatmapsets/discussions")]
+ [TestCase(LinkAction.External, "https://dev.ppy.sh/beatmapsets/discussions/123", "https://dev.ppy.sh/beatmapsets/discussions/123")]
public void TestBeatmapLinks(LinkAction expectedAction, string expectedArg, string link)
{
MessageFormatter.WebsiteRootUrl = "dev.ppy.sh";
diff --git a/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs b/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs
new file mode 100644
index 0000000000..cac331451b
--- /dev/null
+++ b/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs
@@ -0,0 +1,101 @@
+// 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.IO;
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Input.Bindings;
+using osu.Framework.Platform;
+using osu.Game.Database;
+using osu.Game.Input;
+using osu.Game.Input.Bindings;
+using Realms;
+
+namespace osu.Game.Tests.Database
+{
+ [TestFixture]
+ public class TestRealmKeyBindingStore
+ {
+ private NativeStorage storage;
+
+ private RealmKeyBindingStore keyBindingStore;
+
+ private RealmContextFactory realmContextFactory;
+
+ [SetUp]
+ public void SetUp()
+ {
+ var directory = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()));
+
+ storage = new NativeStorage(directory.FullName);
+
+ realmContextFactory = new RealmContextFactory(storage);
+ keyBindingStore = new RealmKeyBindingStore(realmContextFactory);
+ }
+
+ [Test]
+ public void TestDefaultsPopulationAndQuery()
+ {
+ Assert.That(query().Count, Is.EqualTo(0));
+
+ KeyBindingContainer testContainer = new TestKeyBindingContainer();
+
+ keyBindingStore.Register(testContainer);
+
+ Assert.That(query().Count, Is.EqualTo(3));
+
+ Assert.That(query().Where(k => k.ActionInt == (int)GlobalAction.Back).Count, Is.EqualTo(1));
+ Assert.That(query().Where(k => k.ActionInt == (int)GlobalAction.Select).Count, Is.EqualTo(2));
+ }
+
+ private IQueryable query() => realmContextFactory.Context.All();
+
+ [Test]
+ public void TestUpdateViaQueriedReference()
+ {
+ KeyBindingContainer testContainer = new TestKeyBindingContainer();
+
+ keyBindingStore.Register(testContainer);
+
+ var backBinding = query().Single(k => k.ActionInt == (int)GlobalAction.Back);
+
+ Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.Escape }));
+
+ var tsr = ThreadSafeReference.Create(backBinding);
+
+ using (var usage = realmContextFactory.GetForWrite())
+ {
+ var binding = usage.Realm.ResolveReference(tsr);
+ binding.KeyCombination = new KeyCombination(InputKey.BackSpace);
+
+ usage.Commit();
+ }
+
+ Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.BackSpace }));
+
+ // check still correct after re-query.
+ backBinding = query().Single(k => k.ActionInt == (int)GlobalAction.Back);
+ Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.BackSpace }));
+ }
+
+ [TearDown]
+ public void TearDown()
+ {
+ realmContextFactory.Dispose();
+ storage.DeleteDirectory(string.Empty);
+ }
+
+ public class TestKeyBindingContainer : KeyBindingContainer
+ {
+ public override IEnumerable DefaultKeyBindings =>
+ new[]
+ {
+ new KeyBinding(InputKey.Escape, GlobalAction.Back),
+ new KeyBinding(InputKey.Enter, GlobalAction.Select),
+ new KeyBinding(InputKey.Space, GlobalAction.Select),
+ };
+ }
+ }
+}
diff --git a/osu.Game.Tests/Gameplay/TestSceneDrawableHitObject.cs b/osu.Game.Tests/Gameplay/TestSceneDrawableHitObject.cs
index da0d57f9d1..0ce71696bd 100644
--- a/osu.Game.Tests/Gameplay/TestSceneDrawableHitObject.cs
+++ b/osu.Game.Tests/Gameplay/TestSceneDrawableHitObject.cs
@@ -44,11 +44,9 @@ namespace osu.Game.Tests.Gameplay
{
TestDrawableHitObject dho = null;
TestLifetimeEntry entry = null;
- AddStep("Create DHO", () =>
+ AddStep("Create DHO", () => Child = dho = new TestDrawableHitObject
{
- dho = new TestDrawableHitObject(null);
- dho.Apply(entry = new TestLifetimeEntry(new HitObject()));
- Child = dho;
+ Entry = entry = new TestLifetimeEntry(new HitObject())
});
AddStep("KeepAlive = true", () =>
@@ -81,12 +79,10 @@ namespace osu.Game.Tests.Gameplay
AddAssert("Lifetime is updated", () => entry.LifetimeStart == -TestLifetimeEntry.INITIAL_LIFETIME_OFFSET);
TestDrawableHitObject dho = null;
- AddStep("Create DHO", () =>
+ AddStep("Create DHO", () => Child = dho = new TestDrawableHitObject
{
- dho = new TestDrawableHitObject(null);
- dho.Apply(entry);
- Child = dho;
- dho.SetLifetimeStartOnApply = true;
+ Entry = entry,
+ SetLifetimeStartOnApply = true
});
AddStep("ApplyDefaults", () => entry.HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()));
AddAssert("Lifetime is correct", () => dho.LifetimeStart == TestDrawableHitObject.LIFETIME_ON_APPLY && entry.LifetimeStart == TestDrawableHitObject.LIFETIME_ON_APPLY);
@@ -97,11 +93,9 @@ namespace osu.Game.Tests.Gameplay
{
TestDrawableHitObject dho = null;
TestLifetimeEntry entry = null;
- AddStep("Create DHO", () =>
+ AddStep("Create DHO", () => Child = dho = new TestDrawableHitObject
{
- dho = new TestDrawableHitObject(null);
- dho.Apply(entry = new TestLifetimeEntry(new HitObject()));
- Child = dho;
+ Entry = entry = new TestLifetimeEntry(new HitObject())
});
AddStep("Set entry lifetime", () =>
@@ -135,7 +129,7 @@ namespace osu.Game.Tests.Gameplay
public bool SetLifetimeStartOnApply;
- public TestDrawableHitObject(HitObject hitObject)
+ public TestDrawableHitObject(HitObject hitObject = null)
: base(hitObject)
{
}
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/NonVisual/CustomDataDirectoryTest.cs b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs
index a763544c37..a540ad7247 100644
--- a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs
+++ b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs
@@ -142,7 +142,10 @@ namespace osu.Game.Tests.NonVisual
foreach (var file in osuStorage.IgnoreFiles)
{
- Assert.That(File.Exists(Path.Combine(originalDirectory, file)));
+ // avoid touching realm files which may be a pipe and break everything.
+ // this is also done locally inside OsuStorage via the IgnoreFiles list.
+ if (file.EndsWith(".ini", StringComparison.Ordinal))
+ Assert.That(File.Exists(Path.Combine(originalDirectory, file)));
Assert.That(storage.Exists(file), Is.False);
}
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.Tests/Visual/TestSceneOsuGame.cs b/osu.Game.Tests/Visual/TestSceneOsuGame.cs
index b347c39c1e..4e5e8517a4 100644
--- a/osu.Game.Tests/Visual/TestSceneOsuGame.cs
+++ b/osu.Game.Tests/Visual/TestSceneOsuGame.cs
@@ -74,7 +74,6 @@ namespace osu.Game.Tests.Visual
typeof(FileStore),
typeof(ScoreManager),
typeof(BeatmapManager),
- typeof(KeyBindingStore),
typeof(SettingsStore),
typeof(RulesetConfigCache),
typeof(OsuColour),
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/Database/IHasGuidPrimaryKey.cs b/osu.Game/Database/IHasGuidPrimaryKey.cs
new file mode 100644
index 0000000000..c9cd9b257a
--- /dev/null
+++ b/osu.Game/Database/IHasGuidPrimaryKey.cs
@@ -0,0 +1,16 @@
+// 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 Newtonsoft.Json;
+using Realms;
+
+namespace osu.Game.Database
+{
+ public interface IHasGuidPrimaryKey
+ {
+ [JsonIgnore]
+ [PrimaryKey]
+ Guid ID { get; set; }
+ }
+}
diff --git a/osu.Game/Database/IRealmFactory.cs b/osu.Game/Database/IRealmFactory.cs
new file mode 100644
index 0000000000..c79442134c
--- /dev/null
+++ b/osu.Game/Database/IRealmFactory.cs
@@ -0,0 +1,27 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using Realms;
+
+namespace osu.Game.Database
+{
+ public interface IRealmFactory
+ {
+ ///
+ /// The main realm context, bound to the update thread.
+ ///
+ Realm Context { get; }
+
+ ///
+ /// Get a fresh context for read usage.
+ ///
+ RealmContextFactory.RealmUsage GetForRead();
+
+ ///
+ /// Request a context for write usage.
+ /// This method may block if a write is already active on a different thread.
+ ///
+ /// A usage containing a usable context.
+ RealmContextFactory.RealmWriteUsage GetForWrite();
+ }
+}
diff --git a/osu.Game/Database/OsuDbContext.cs b/osu.Game/Database/OsuDbContext.cs
index 2aae62edea..e0c0f56cb3 100644
--- a/osu.Game/Database/OsuDbContext.cs
+++ b/osu.Game/Database/OsuDbContext.cs
@@ -24,13 +24,15 @@ namespace osu.Game.Database
public DbSet BeatmapDifficulty { get; set; }
public DbSet BeatmapMetadata { get; set; }
public DbSet BeatmapSetInfo { get; set; }
- public DbSet DatabasedKeyBinding { get; set; }
public DbSet DatabasedSetting { get; set; }
public DbSet FileInfo { get; set; }
public DbSet RulesetInfo { get; set; }
public DbSet SkinInfo { get; set; }
public DbSet ScoreInfo { get; set; }
+ // migrated to realm
+ public DbSet DatabasedKeyBinding { get; set; }
+
private readonly string connectionString;
private static readonly Lazy logger = new Lazy(() => new OsuDbLoggerFactory());
diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs
new file mode 100644
index 0000000000..ed5931dd2b
--- /dev/null
+++ b/osu.Game/Database/RealmContextFactory.cs
@@ -0,0 +1,208 @@
+// 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.Threading;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Logging;
+using osu.Framework.Platform;
+using osu.Framework.Statistics;
+using osu.Game.Input.Bindings;
+using Realms;
+
+namespace osu.Game.Database
+{
+ public class RealmContextFactory : Component, IRealmFactory
+ {
+ private readonly Storage storage;
+
+ private const string database_name = @"client";
+
+ private const int schema_version = 6;
+
+ ///
+ /// Lock object which is held for the duration of a write operation (via ).
+ ///
+ private readonly object writeLock = new object();
+
+ private static readonly GlobalStatistic reads = GlobalStatistics.Get("Realm", "Get (Read)");
+ private static readonly GlobalStatistic writes = GlobalStatistics.Get("Realm", "Get (Write)");
+ private static readonly GlobalStatistic refreshes = GlobalStatistics.Get("Realm", "Dirty Refreshes");
+ private static readonly GlobalStatistic contexts_created = GlobalStatistics.Get("Realm", "Contexts (Created)");
+ private static readonly GlobalStatistic pending_writes = GlobalStatistics.Get("Realm", "Pending writes");
+ private static readonly GlobalStatistic active_usages = GlobalStatistics.Get("Realm", "Active usages");
+
+ private readonly ManualResetEventSlim blockingResetEvent = new ManualResetEventSlim(true);
+
+ private Realm context;
+
+ public Realm Context
+ {
+ get
+ {
+ if (IsDisposed)
+ throw new InvalidOperationException($"Attempted to access {nameof(Context)} on a disposed context factory");
+
+ if (context == null)
+ {
+ context = createContext();
+ Logger.Log($"Opened realm \"{context.Config.DatabasePath}\" at version {context.Config.SchemaVersion}");
+ }
+
+ // creating a context will ensure our schema is up-to-date and migrated.
+
+ return context;
+ }
+ }
+
+ public RealmContextFactory(Storage storage)
+ {
+ this.storage = storage;
+ }
+
+ public RealmUsage GetForRead()
+ {
+ reads.Value++;
+ return new RealmUsage(this);
+ }
+
+ public RealmWriteUsage GetForWrite()
+ {
+ writes.Value++;
+ pending_writes.Value++;
+
+ Monitor.Enter(writeLock);
+
+ return new RealmWriteUsage(this);
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ if (context?.Refresh() == true)
+ refreshes.Value++;
+ }
+
+ private Realm createContext()
+ {
+ blockingResetEvent.Wait();
+
+ contexts_created.Value++;
+
+ return Realm.GetInstance(new RealmConfiguration(storage.GetFullPath($"{database_name}.realm", true))
+ {
+ SchemaVersion = schema_version,
+ MigrationCallback = onMigration,
+ });
+ }
+
+ private void onMigration(Migration migration, ulong lastSchemaVersion)
+ {
+ switch (lastSchemaVersion)
+ {
+ case 5:
+ // let's keep things simple. changing the type of the primary key is a bit involved.
+ migration.NewRealm.RemoveAll();
+ break;
+ }
+ }
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+
+ BlockAllOperations();
+ }
+
+ public IDisposable BlockAllOperations()
+ {
+ blockingResetEvent.Reset();
+ flushContexts();
+
+ return new InvokeOnDisposal(this, r => endBlockingSection());
+ }
+
+ private void endBlockingSection()
+ {
+ blockingResetEvent.Set();
+ }
+
+ private void flushContexts()
+ {
+ var previousContext = context;
+ context = null;
+
+ // wait for all threaded usages to finish
+ while (active_usages.Value > 0)
+ Thread.Sleep(50);
+
+ previousContext?.Dispose();
+ }
+
+ ///
+ /// A usage of realm from an arbitrary thread.
+ ///
+ public class RealmUsage : IDisposable
+ {
+ public readonly Realm Realm;
+
+ protected readonly RealmContextFactory Factory;
+
+ internal RealmUsage(RealmContextFactory factory)
+ {
+ active_usages.Value++;
+ Factory = factory;
+ Realm = factory.createContext();
+ }
+
+ ///
+ /// Disposes this instance, calling the initially captured action.
+ ///
+ public virtual void Dispose()
+ {
+ Realm?.Dispose();
+ active_usages.Value--;
+ }
+ }
+
+ ///
+ /// A transaction used for making changes to realm data.
+ ///
+ public class RealmWriteUsage : RealmUsage
+ {
+ private readonly Transaction transaction;
+
+ internal RealmWriteUsage(RealmContextFactory factory)
+ : base(factory)
+ {
+ transaction = Realm.BeginWrite();
+ }
+
+ ///
+ /// Commit all changes made in this transaction.
+ ///
+ public void Commit() => transaction.Commit();
+
+ ///
+ /// Revert all changes made in this transaction.
+ ///
+ public void Rollback() => transaction.Rollback();
+
+ ///
+ /// Disposes this instance, calling the initially captured action.
+ ///
+ public override void Dispose()
+ {
+ // rollback if not explicitly committed.
+ transaction?.Dispose();
+
+ base.Dispose();
+
+ Monitor.Exit(Factory.writeLock);
+ pending_writes.Value--;
+ }
+ }
+ }
+}
diff --git a/osu.Game/Database/RealmExtensions.cs b/osu.Game/Database/RealmExtensions.cs
new file mode 100644
index 0000000000..aee36e81c5
--- /dev/null
+++ b/osu.Game/Database/RealmExtensions.cs
@@ -0,0 +1,51 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using AutoMapper;
+using osu.Game.Input.Bindings;
+using Realms;
+
+namespace osu.Game.Database
+{
+ public static class RealmExtensions
+ {
+ private static readonly IMapper mapper = new MapperConfiguration(c =>
+ {
+ c.ShouldMapField = fi => false;
+ c.ShouldMapProperty = pi => pi.SetMethod != null && pi.SetMethod.IsPublic;
+
+ c.CreateMap();
+ }).CreateMapper();
+
+ ///
+ /// Create a detached copy of the each item in the collection.
+ ///
+ /// A list of managed s to detach.
+ /// The type of object.
+ /// A list containing non-managed copies of provided items.
+ public static List Detach(this IEnumerable items) where T : RealmObject
+ {
+ var list = new List();
+
+ foreach (var obj in items)
+ list.Add(obj.Detach());
+
+ return list;
+ }
+
+ ///
+ /// Create a detached copy of the item.
+ ///
+ /// The managed to detach.
+ /// The type of object.
+ /// A non-managed copy of provided item. Will return the provided item if already detached.
+ public static T Detach(this T item) where T : RealmObject
+ {
+ if (!item.IsManaged)
+ return item;
+
+ return mapper.Map(item);
+ }
+ }
+}
diff --git a/osu.Game/Extensions/LanguageExtensions.cs b/osu.Game/Extensions/LanguageExtensions.cs
new file mode 100644
index 0000000000..b67e7fb6fc
--- /dev/null
+++ b/osu.Game/Extensions/LanguageExtensions.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 System;
+using System.Globalization;
+using osu.Game.Localisation;
+
+namespace osu.Game.Extensions
+{
+ ///
+ /// Conversion utilities for the enum.
+ ///
+ public static class LanguageExtensions
+ {
+ ///
+ /// Returns the culture code of the that corresponds to the supplied .
+ ///
+ ///
+ /// This is required as enum member names are not allowed to contain hyphens.
+ ///
+ public static string ToCultureCode(this Language language)
+ => language.ToString().Replace("_", "-");
+
+ ///
+ /// Attempts to parse the supplied to a value.
+ ///
+ /// The code of the culture to parse.
+ /// The parsed . Valid only if the return value of the method is .
+ /// Whether the parsing succeeded.
+ public static bool TryParseCultureCode(string cultureCode, out Language language)
+ => Enum.TryParse(cultureCode.Replace("-", "_"), out language);
+ }
+}
diff --git a/osu.Game/Graphics/Containers/OsuHoverContainer.cs b/osu.Game/Graphics/Containers/OsuHoverContainer.cs
index 67af79c763..ac66fd658a 100644
--- a/osu.Game/Graphics/Containers/OsuHoverContainer.cs
+++ b/osu.Game/Graphics/Containers/OsuHoverContainer.cs
@@ -7,6 +7,7 @@ using osu.Framework.Graphics;
using osu.Framework.Input.Events;
using osuTK.Graphics;
using System.Collections.Generic;
+using osu.Game.Graphics.UserInterface;
namespace osu.Game.Graphics.Containers
{
@@ -20,7 +21,8 @@ namespace osu.Game.Graphics.Containers
protected virtual IEnumerable EffectTargets => new[] { Content };
- public OsuHoverContainer()
+ public OsuHoverContainer(HoverSampleSet sampleSet = HoverSampleSet.Default)
+ : base(sampleSet)
{
Enabled.ValueChanged += e =>
{
diff --git a/osu.Game/Graphics/UserInterface/HoverSampleSet.cs b/osu.Game/Graphics/UserInterface/HoverSampleSet.cs
index c74ac90a4c..b88f81a143 100644
--- a/osu.Game/Graphics/UserInterface/HoverSampleSet.cs
+++ b/osu.Game/Graphics/UserInterface/HoverSampleSet.cs
@@ -13,13 +13,13 @@ namespace osu.Game.Graphics.UserInterface
[Description("button")]
Button,
- [Description("softer")]
- Soft,
-
[Description("toolbar")]
Toolbar,
- [Description("songselect")]
- SongSelect
+ [Description("tabselect")]
+ TabSelect,
+
+ [Description("scrolltotop")]
+ ScrollToTop
}
}
diff --git a/osu.Game/Graphics/UserInterface/OsuDropdown.cs b/osu.Game/Graphics/UserInterface/OsuDropdown.cs
index 15fb00ccb0..b97f12df02 100644
--- a/osu.Game/Graphics/UserInterface/OsuDropdown.cs
+++ b/osu.Game/Graphics/UserInterface/OsuDropdown.cs
@@ -4,6 +4,8 @@
using System.Linq;
using osuTK.Graphics;
using osu.Framework.Allocation;
+using osu.Framework.Audio;
+using osu.Framework.Audio.Sample;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@@ -57,6 +59,9 @@ namespace osu.Game.Graphics.UserInterface
{
public override bool HandleNonPositionalInput => State == MenuState.Open;
+ private Sample sampleOpen;
+ private Sample sampleClose;
+
// todo: this uses the same styling as OsuMenu. hopefully we can just use OsuMenu in the future with some refactoring
public OsuDropdownMenu()
{
@@ -69,9 +74,30 @@ namespace osu.Game.Graphics.UserInterface
ItemsContainer.Padding = new MarginPadding(5);
}
+ [BackgroundDependencyLoader]
+ private void load(AudioManager audio)
+ {
+ sampleOpen = audio.Samples.Get(@"UI/dropdown-open");
+ sampleClose = audio.Samples.Get(@"UI/dropdown-close");
+ }
+
+ // todo: this shouldn't be required after https://github.com/ppy/osu-framework/issues/4519 is fixed.
+ private bool wasOpened;
+
// todo: this uses the same styling as OsuMenu. hopefully we can just use OsuMenu in the future with some refactoring
- protected override void AnimateOpen() => this.FadeIn(300, Easing.OutQuint);
- protected override void AnimateClose() => this.FadeOut(300, Easing.OutQuint);
+ protected override void AnimateOpen()
+ {
+ wasOpened = true;
+ this.FadeIn(300, Easing.OutQuint);
+ sampleOpen?.Play();
+ }
+
+ protected override void AnimateClose()
+ {
+ this.FadeOut(300, Easing.OutQuint);
+ if (wasOpened)
+ sampleClose?.Play();
+ }
// todo: this uses the same styling as OsuMenu. hopefully we can just use OsuMenu in the future with some refactoring
protected override void UpdateSize(Vector2 newSize)
@@ -155,7 +181,7 @@ namespace osu.Game.Graphics.UserInterface
nonAccentSelectedColour = Color4.Black.Opacity(0.5f);
updateColours();
- AddInternal(new HoverClickSounds(HoverSampleSet.Soft));
+ AddInternal(new HoverSounds());
}
protected override void UpdateForegroundColour()
@@ -262,7 +288,7 @@ namespace osu.Game.Graphics.UserInterface
},
};
- AddInternal(new HoverClickSounds());
+ AddInternal(new HoverSounds());
}
[BackgroundDependencyLoader]
diff --git a/osu.Game/Graphics/UserInterface/OsuTabControl.cs b/osu.Game/Graphics/UserInterface/OsuTabControl.cs
index dbcce9a84a..c447d7f609 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
@@ -172,7 +172,7 @@ namespace osu.Game.Graphics.UserInterface
Origin = Anchor.BottomLeft,
Anchor = Anchor.BottomLeft,
},
- new HoverClickSounds()
+ new HoverClickSounds(HoverSampleSet.TabSelect)
};
}
diff --git a/osu.Game/Graphics/UserInterface/OsuTabControlCheckbox.cs b/osu.Game/Graphics/UserInterface/OsuTabControlCheckbox.cs
index b66a4a58ce..c6121dcd17 100644
--- a/osu.Game/Graphics/UserInterface/OsuTabControlCheckbox.cs
+++ b/osu.Game/Graphics/UserInterface/OsuTabControlCheckbox.cs
@@ -4,6 +4,8 @@
using osuTK;
using osuTK.Graphics;
using osu.Framework.Allocation;
+using osu.Framework.Audio;
+using osu.Framework.Audio.Sample;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
@@ -43,6 +45,8 @@ namespace osu.Game.Graphics.UserInterface
}
private const float transition_length = 500;
+ private Sample sampleChecked;
+ private Sample sampleUnchecked;
public OsuTabControlCheckbox()
{
@@ -77,8 +81,7 @@ namespace osu.Game.Graphics.UserInterface
Colour = Color4.White,
Origin = Anchor.BottomLeft,
Anchor = Anchor.BottomLeft,
- },
- new HoverClickSounds()
+ }
};
Current.ValueChanged += selected =>
@@ -91,10 +94,13 @@ namespace osu.Game.Graphics.UserInterface
}
[BackgroundDependencyLoader]
- private void load(OsuColour colours)
+ private void load(OsuColour colours, AudioManager audio)
{
if (accentColour == null)
AccentColour = colours.Blue;
+
+ sampleChecked = audio.Samples.Get(@"UI/check-on");
+ sampleUnchecked = audio.Samples.Get(@"UI/check-off");
}
protected override bool OnHover(HoverEvent e)
@@ -111,6 +117,16 @@ namespace osu.Game.Graphics.UserInterface
base.OnHoverLost(e);
}
+ protected override void OnUserChange(bool value)
+ {
+ base.OnUserChange(value);
+
+ if (value)
+ sampleChecked?.Play();
+ else
+ sampleUnchecked?.Play();
+ }
+
private void updateFade()
{
box.FadeTo(Current.Value || IsHovered ? 1 : 0, transition_length, Easing.OutQuint);
diff --git a/osu.Game/Graphics/UserInterface/PageTabControl.cs b/osu.Game/Graphics/UserInterface/PageTabControl.cs
index d05a08108a..a218c7bf52 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
@@ -75,13 +76,13 @@ namespace osu.Game.Graphics.UserInterface
Origin = Anchor.BottomLeft,
Anchor = Anchor.BottomLeft,
},
- new HoverClickSounds()
+ new HoverClickSounds(HoverSampleSet.TabSelect)
};
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/IO/OsuStorage.cs b/osu.Game/IO/OsuStorage.cs
index 7df5d820ee..75130b0f9b 100644
--- a/osu.Game/IO/OsuStorage.cs
+++ b/osu.Game/IO/OsuStorage.cs
@@ -33,12 +33,18 @@ namespace osu.Game.IO
private readonly StorageConfigManager storageConfig;
private readonly Storage defaultStorage;
- public override string[] IgnoreDirectories => new[] { "cache" };
+ public override string[] IgnoreDirectories => new[]
+ {
+ "cache",
+ "client.realm.management"
+ };
public override string[] IgnoreFiles => new[]
{
"framework.ini",
- "storage.ini"
+ "storage.ini",
+ "client.realm.note",
+ "client.realm.lock",
};
public OsuStorage(GameHost host, Storage defaultStorage)
diff --git a/osu.Game/Input/Bindings/DatabasedKeyBinding.cs b/osu.Game/Input/Bindings/DatabasedKeyBinding.cs
index 8c0072c3da..ad3493d0fc 100644
--- a/osu.Game/Input/Bindings/DatabasedKeyBinding.cs
+++ b/osu.Game/Input/Bindings/DatabasedKeyBinding.cs
@@ -8,7 +8,7 @@ using osu.Game.Database;
namespace osu.Game.Input.Bindings
{
[Table("KeyBinding")]
- public class DatabasedKeyBinding : KeyBinding, IHasPrimaryKey
+ public class DatabasedKeyBinding : IKeyBinding, IHasPrimaryKey
{
public int ID { get; set; }
@@ -17,17 +17,23 @@ namespace osu.Game.Input.Bindings
public int? Variant { get; set; }
[Column("Keys")]
- public string KeysString
- {
- get => KeyCombination.ToString();
- private set => KeyCombination = value;
- }
+ public string KeysString { get; set; }
[Column("Action")]
- public int IntAction
+ public int IntAction { get; set; }
+
+ [NotMapped]
+ public KeyCombination KeyCombination
{
- get => (int)Action;
- set => Action = value;
+ get => KeysString;
+ set => KeysString = value.ToString();
+ }
+
+ [NotMapped]
+ public object Action
+ {
+ get => IntAction;
+ set => IntAction = (int)value;
}
}
}
diff --git a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs
index 23b09e8fb1..10376c1866 100644
--- a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs
+++ b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs
@@ -3,10 +3,12 @@
using System;
using System.Collections.Generic;
+using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Input.Bindings;
+using osu.Game.Database;
using osu.Game.Rulesets;
-using System.Linq;
+using Realms;
namespace osu.Game.Input.Bindings
{
@@ -21,7 +23,11 @@ namespace osu.Game.Input.Bindings
private readonly int? variant;
- private KeyBindingStore store;
+ private IDisposable realmSubscription;
+ private IQueryable realmKeyBindings;
+
+ [Resolved]
+ private RealmContextFactory realmFactory { get; set; }
public override IEnumerable DefaultKeyBindings => ruleset.CreateInstance().GetDefaultKeyBindings(variant ?? 0);
@@ -42,24 +48,34 @@ namespace osu.Game.Input.Bindings
throw new InvalidOperationException($"{nameof(variant)} can not be null when a non-null {nameof(ruleset)} is provided.");
}
- [BackgroundDependencyLoader]
- private void load(KeyBindingStore keyBindings)
- {
- store = keyBindings;
- }
-
protected override void LoadComplete()
{
+ if (ruleset == null || ruleset.ID.HasValue)
+ {
+ var rulesetId = ruleset?.ID;
+
+ realmKeyBindings = realmFactory.Context.All()
+ .Where(b => b.RulesetID == rulesetId && b.Variant == variant);
+
+ realmSubscription = realmKeyBindings
+ .SubscribeForNotifications((sender, changes, error) =>
+ {
+ // first subscription ignored as we are handling this in LoadComplete.
+ if (changes == null)
+ return;
+
+ ReloadMappings();
+ });
+ }
+
base.LoadComplete();
- store.KeyBindingChanged += ReloadMappings;
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
- if (store != null)
- store.KeyBindingChanged -= ReloadMappings;
+ realmSubscription?.Dispose();
}
protected override void ReloadMappings()
@@ -67,17 +83,17 @@ namespace osu.Game.Input.Bindings
var defaults = DefaultKeyBindings.ToList();
if (ruleset != null && !ruleset.ID.HasValue)
- // if the provided ruleset is not stored to the database, we have no way to retrieve custom bindings.
- // fallback to defaults instead.
+ // some tests instantiate a ruleset which is not present in the database.
+ // in these cases we still want key bindings to work, but matching to database instances would result in none being present,
+ // so let's populate the defaults directly.
KeyBindings = defaults;
else
{
- KeyBindings = store.Query(ruleset?.ID, variant)
- .OrderBy(b => defaults.FindIndex(d => (int)d.Action == b.IntAction))
- // this ordering is important to ensure that we read entries from the database in the order
- // enforced by DefaultKeyBindings. allow for song select to handle actions that may otherwise
- // have been eaten by the music controller due to query order.
- .ToList();
+ KeyBindings = realmKeyBindings.Detach()
+ // this ordering is important to ensure that we read entries from the database in the order
+ // enforced by DefaultKeyBindings. allow for song select to handle actions that may otherwise
+ // have been eaten by the music controller due to query order.
+ .OrderBy(b => defaults.FindIndex(d => (int)d.Action == b.ActionInt)).ToList();
}
}
}
diff --git a/osu.Game/Input/Bindings/RealmKeyBinding.cs b/osu.Game/Input/Bindings/RealmKeyBinding.cs
new file mode 100644
index 0000000000..334d2da427
--- /dev/null
+++ b/osu.Game/Input/Bindings/RealmKeyBinding.cs
@@ -0,0 +1,39 @@
+// 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.Input.Bindings;
+using osu.Game.Database;
+using Realms;
+
+namespace osu.Game.Input.Bindings
+{
+ [MapTo(nameof(KeyBinding))]
+ public class RealmKeyBinding : RealmObject, IHasGuidPrimaryKey, IKeyBinding
+ {
+ [PrimaryKey]
+ public Guid ID { get; set; } = Guid.NewGuid();
+
+ public int? RulesetID { get; set; }
+
+ public int? Variant { get; set; }
+
+ public KeyCombination KeyCombination
+ {
+ get => KeyCombinationString;
+ set => KeyCombinationString = value.ToString();
+ }
+
+ public object Action
+ {
+ get => ActionInt;
+ set => ActionInt = (int)value;
+ }
+
+ [MapTo(nameof(Action))]
+ public int ActionInt { get; set; }
+
+ [MapTo(nameof(KeyCombination))]
+ public string KeyCombinationString { get; set; }
+ }
+}
diff --git a/osu.Game/Input/KeyBindingStore.cs b/osu.Game/Input/KeyBindingStore.cs
deleted file mode 100644
index 3ef9923487..0000000000
--- a/osu.Game/Input/KeyBindingStore.cs
+++ /dev/null
@@ -1,133 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.Linq;
-using osu.Framework.Input.Bindings;
-using osu.Framework.Platform;
-using osu.Game.Database;
-using osu.Game.Input.Bindings;
-using osu.Game.Rulesets;
-
-namespace osu.Game.Input
-{
- public class KeyBindingStore : DatabaseBackedStore
- {
- public event Action KeyBindingChanged;
-
- ///
- /// Keys which should not be allowed for gameplay input purposes.
- ///
- private static readonly IEnumerable banned_keys = new[]
- {
- InputKey.MouseWheelDown,
- InputKey.MouseWheelLeft,
- InputKey.MouseWheelUp,
- InputKey.MouseWheelRight
- };
-
- public KeyBindingStore(DatabaseContextFactory contextFactory, RulesetStore rulesets, Storage storage = null)
- : base(contextFactory, storage)
- {
- using (ContextFactory.GetForWrite())
- {
- foreach (var info in rulesets.AvailableRulesets)
- {
- var ruleset = info.CreateInstance();
- foreach (var variant in ruleset.AvailableVariants)
- insertDefaults(ruleset.GetDefaultKeyBindings(variant), info.ID, variant);
- }
- }
- }
-
- public void Register(KeyBindingContainer manager) => insertDefaults(manager.DefaultKeyBindings);
-
- ///
- /// Retrieve all user-defined key combinations (in a format that can be displayed) for a specific action.
- ///
- /// The action to lookup.
- /// A set of display strings for all the user's key configuration for the action.
- public IEnumerable GetReadableKeyCombinationsFor(GlobalAction globalAction)
- {
- foreach (var action in Query().Where(b => (GlobalAction)b.Action == globalAction))
- {
- string str = action.KeyCombination.ReadableString();
-
- // even if found, the readable string may be empty for an unbound action.
- if (str.Length > 0)
- yield return str;
- }
- }
-
- private void insertDefaults(IEnumerable defaults, int? rulesetId = null, int? variant = null)
- {
- using (var usage = ContextFactory.GetForWrite())
- {
- // compare counts in database vs defaults
- foreach (var group in defaults.GroupBy(k => k.Action))
- {
- int count = Query(rulesetId, variant).Count(k => (int)k.Action == (int)group.Key);
- int aimCount = group.Count();
-
- if (aimCount <= count)
- continue;
-
- foreach (var insertable in group.Skip(count).Take(aimCount - count))
- {
- // insert any defaults which are missing.
- usage.Context.DatabasedKeyBinding.Add(new DatabasedKeyBinding
- {
- KeyCombination = insertable.KeyCombination,
- Action = insertable.Action,
- RulesetID = rulesetId,
- Variant = variant
- });
-
- // required to ensure stable insert order (https://github.com/dotnet/efcore/issues/11686)
- usage.Context.SaveChanges();
- }
- }
- }
- }
-
- ///
- /// Retrieve s for a specified ruleset/variant content.
- ///
- /// The ruleset's internal ID.
- /// An optional variant.
- public List Query(int? rulesetId = null, int? variant = null) =>
- ContextFactory.Get().DatabasedKeyBinding.Where(b => b.RulesetID == rulesetId && b.Variant == variant).ToList();
-
- public void Update(KeyBinding keyBinding)
- {
- using (ContextFactory.GetForWrite())
- {
- var dbKeyBinding = (DatabasedKeyBinding)keyBinding;
-
- Debug.Assert(dbKeyBinding.RulesetID == null || CheckValidForGameplay(keyBinding.KeyCombination));
-
- Refresh(ref dbKeyBinding);
-
- if (dbKeyBinding.KeyCombination.Equals(keyBinding.KeyCombination))
- return;
-
- dbKeyBinding.KeyCombination = keyBinding.KeyCombination;
- }
-
- KeyBindingChanged?.Invoke();
- }
-
- public static bool CheckValidForGameplay(KeyCombination combination)
- {
- foreach (var key in banned_keys)
- {
- if (combination.Keys.Contains(key))
- return false;
- }
-
- return true;
- }
- }
-}
diff --git a/osu.Game/Input/RealmKeyBindingStore.cs b/osu.Game/Input/RealmKeyBindingStore.cs
new file mode 100644
index 0000000000..9089169877
--- /dev/null
+++ b/osu.Game/Input/RealmKeyBindingStore.cs
@@ -0,0 +1,117 @@
+// 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.Input.Bindings;
+using osu.Game.Database;
+using osu.Game.Input.Bindings;
+using osu.Game.Rulesets;
+
+#nullable enable
+
+namespace osu.Game.Input
+{
+ public class RealmKeyBindingStore
+ {
+ private readonly RealmContextFactory realmFactory;
+
+ public RealmKeyBindingStore(RealmContextFactory realmFactory)
+ {
+ this.realmFactory = realmFactory;
+ }
+
+ ///
+ /// Retrieve all user-defined key combinations (in a format that can be displayed) for a specific action.
+ ///
+ /// The action to lookup.
+ /// A set of display strings for all the user's key configuration for the action.
+ public IReadOnlyList GetReadableKeyCombinationsFor(GlobalAction globalAction)
+ {
+ List combinations = new List();
+
+ using (var context = realmFactory.GetForRead())
+ {
+ foreach (var action in context.Realm.All().Where(b => b.RulesetID == null && (GlobalAction)b.ActionInt == globalAction))
+ {
+ string str = action.KeyCombination.ReadableString();
+
+ // even if found, the readable string may be empty for an unbound action.
+ if (str.Length > 0)
+ combinations.Add(str);
+ }
+ }
+
+ return combinations;
+ }
+
+ ///
+ /// Register a new type of , adding default bindings from .
+ ///
+ /// The container to populate defaults from.
+ public void Register(KeyBindingContainer container) => insertDefaults(container.DefaultKeyBindings);
+
+ ///
+ /// Register a ruleset, adding default bindings for each of its variants.
+ ///
+ /// The ruleset to populate defaults from.
+ public void Register(RulesetInfo ruleset)
+ {
+ var instance = ruleset.CreateInstance();
+
+ foreach (var variant in instance.AvailableVariants)
+ insertDefaults(instance.GetDefaultKeyBindings(variant), ruleset.ID, variant);
+ }
+
+ private void insertDefaults(IEnumerable defaults, int? rulesetId = null, int? variant = null)
+ {
+ using (var usage = realmFactory.GetForWrite())
+ {
+ // compare counts in database vs defaults
+ foreach (var defaultsForAction in defaults.GroupBy(k => k.Action))
+ {
+ int existingCount = usage.Realm.All().Count(k => k.RulesetID == rulesetId && k.Variant == variant && k.ActionInt == (int)defaultsForAction.Key);
+
+ if (defaultsForAction.Count() <= existingCount)
+ continue;
+
+ foreach (var k in defaultsForAction.Skip(existingCount))
+ {
+ // insert any defaults which are missing.
+ usage.Realm.Add(new RealmKeyBinding
+ {
+ KeyCombinationString = k.KeyCombination.ToString(),
+ ActionInt = (int)k.Action,
+ RulesetID = rulesetId,
+ Variant = variant
+ });
+ }
+ }
+
+ usage.Commit();
+ }
+ }
+
+ ///
+ /// Keys which should not be allowed for gameplay input purposes.
+ ///
+ private static readonly IEnumerable banned_keys = new[]
+ {
+ InputKey.MouseWheelDown,
+ InputKey.MouseWheelLeft,
+ InputKey.MouseWheelUp,
+ InputKey.MouseWheelRight
+ };
+
+ public static bool CheckValidForGameplay(KeyCombination combination)
+ {
+ foreach (var key in banned_keys)
+ {
+ if (combination.Keys.Contains(key))
+ return false;
+ }
+
+ return true;
+ }
+ }
+}
diff --git a/osu.Game/Localisation/Language.cs b/osu.Game/Localisation/Language.cs
index a3e845f229..3c66f31c58 100644
--- a/osu.Game/Localisation/Language.cs
+++ b/osu.Game/Localisation/Language.cs
@@ -10,7 +10,103 @@ namespace osu.Game.Localisation
[Description(@"English")]
en,
+ // TODO: Requires Arabic glyphs to be added to resources (and possibly also RTL support).
+ // [Description(@"اَلْعَرَبِيَّةُ")]
+ // ar,
+
+ [Description(@"Беларуская мова")]
+ be,
+
+ [Description(@"Български")]
+ bg,
+
+ [Description(@"Česky")]
+ cs,
+
+ [Description(@"Dansk")]
+ da,
+
+ [Description(@"Deutsch")]
+ de,
+
+ [Description(@"Ελληνικά")]
+ el,
+
+ [Description(@"español")]
+ es,
+
+ [Description(@"Suomi")]
+ fi,
+
+ [Description(@"français")]
+ fr,
+
+ [Description(@"Magyar")]
+ hu,
+
+ [Description(@"Bahasa Indonesia")]
+ id,
+
+ [Description(@"Italiano")]
+ it,
+
[Description(@"日本語")]
- ja
+ ja,
+
+ [Description(@"한국어")]
+ ko,
+
+ [Description(@"Nederlands")]
+ nl,
+
+ [Description(@"Norsk")]
+ no,
+
+ [Description(@"polski")]
+ pl,
+
+ [Description(@"Português")]
+ pt,
+
+ [Description(@"Português (Brasil)")]
+ pt_br,
+
+ [Description(@"Română")]
+ ro,
+
+ [Description(@"Русский")]
+ ru,
+
+ [Description(@"Slovenčina")]
+ sk,
+
+ [Description(@"Svenska")]
+ sv,
+
+ [Description(@"ไทย")]
+ th,
+
+ // Tagalog has no associated localisations yet, and is not supported on Xamarin platforms or Windows versions <10.
+ // Can be revisited if localisations ever arrive.
+ //[Description(@"Tagalog")]
+ //tl,
+
+ [Description(@"Türkçe")]
+ tr,
+
+ [Description(@"Українська мова")]
+ uk,
+
+ [Description(@"Tiếng Việt")]
+ vi,
+
+ [Description(@"简体中文")]
+ zh,
+
+ [Description(@"繁體中文(香港)")]
+ zh_hk,
+
+ [Description(@"繁體中文(台灣)")]
+ zh_tw
}
}
diff --git a/osu.Game/Online/Chat/MessageFormatter.cs b/osu.Game/Online/Chat/MessageFormatter.cs
index df14d7eb1c..faee08742b 100644
--- a/osu.Game/Online/Chat/MessageFormatter.cs
+++ b/osu.Game/Online/Chat/MessageFormatter.cs
@@ -154,6 +154,10 @@ namespace osu.Game.Online.Chat
case "beatmapsets":
case "d":
{
+ if (mainArg == "discussions")
+ // handle discussion links externally for now
+ return new LinkDetails(LinkAction.External, url);
+
if (args.Length > 4 && int.TryParse(args[4], out var id))
// https://osu.ppy.sh/beatmapsets/1154158#osu/2768184
return new LinkDetails(LinkAction.OpenBeatmap, id.ToString());
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/OsuGame.cs b/osu.Game/OsuGame.cs
index 02e724a451..32136b8789 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -50,8 +50,10 @@ using osu.Game.Updater;
using osu.Game.Utils;
using LogLevel = osu.Framework.Logging.LogLevel;
using osu.Game.Database;
+using osu.Game.Extensions;
using osu.Game.IO;
using osu.Game.Localisation;
+using osu.Game.Performance;
using osu.Game.Skinning.Editor;
namespace osu.Game
@@ -487,6 +489,8 @@ namespace osu.Game
protected virtual UpdateManager CreateUpdateManager() => new UpdateManager();
+ protected virtual HighPerformanceSession CreateHighPerformanceSession() => new HighPerformanceSession();
+
protected override Container CreateScalingContainer() => new ScalingContainer(ScalingMode.Everything);
#region Beatmap progression
@@ -580,8 +584,16 @@ namespace osu.Game
foreach (var language in Enum.GetValues(typeof(Language)).OfType())
{
- var cultureCode = language.ToString();
- Localisation.AddLanguage(cultureCode, new ResourceManagerLocalisationStore(cultureCode));
+ var cultureCode = language.ToCultureCode();
+
+ try
+ {
+ Localisation.AddLanguage(cultureCode, new ResourceManagerLocalisationStore(cultureCode));
+ }
+ catch (Exception ex)
+ {
+ Logger.Error(ex, $"Could not load localisations for language \"{cultureCode}\"");
+ }
}
// The next time this is updated is in UpdateAfterChildren, which occurs too late and results
@@ -604,9 +616,9 @@ namespace osu.Game
LocalConfig.LookupKeyBindings = l =>
{
- var combinations = KeyBindingStore.GetReadableKeyCombinationsFor(l).ToArray();
+ var combinations = KeyBindingStore.GetReadableKeyCombinationsFor(l);
- if (combinations.Length == 0)
+ if (combinations.Count == 0)
return "none";
return string.Join(" or ", combinations);
@@ -755,6 +767,8 @@ namespace osu.Game
loadComponentSingleFile(new AccountCreationOverlay(), topMostOverlayContent.Add, true);
loadComponentSingleFile(new DialogOverlay(), topMostOverlayContent.Add, true);
+ loadComponentSingleFile(CreateHighPerformanceSession(), Add);
+
chatOverlay.State.ValueChanged += state => channelManager.HighPollRate.Value = state.NewValue == Visibility.Visible;
Add(difficultyRecommender);
diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs
index 9c3adba342..3a08ef684f 100644
--- a/osu.Game/OsuGameBase.cs
+++ b/osu.Game/OsuGameBase.cs
@@ -95,7 +95,7 @@ namespace osu.Game
protected RulesetStore RulesetStore { get; private set; }
- protected KeyBindingStore KeyBindingStore { get; private set; }
+ protected RealmKeyBindingStore KeyBindingStore { get; private set; }
protected MenuCursorContainer MenuCursorContainer { get; private set; }
@@ -144,6 +144,8 @@ namespace osu.Game
private DatabaseContextFactory contextFactory;
+ private RealmContextFactory realmFactory;
+
protected override Container Content => content;
private Container content;
@@ -179,6 +181,9 @@ namespace osu.Game
dependencies.Cache(contextFactory = new DatabaseContextFactory(Storage));
+ dependencies.Cache(realmFactory = new RealmContextFactory(Storage));
+ AddInternal(realmFactory);
+
dependencies.CacheAs(Storage);
var largeStore = new LargeTextureStore(Host.CreateTextureLoaderStore(new NamespacedResourceStore(Resources, @"Textures")));
@@ -190,20 +195,29 @@ namespace osu.Game
AddFont(Resources, @"Fonts/osuFont");
- AddFont(Resources, @"Fonts/Torus-Regular");
- AddFont(Resources, @"Fonts/Torus-Light");
- AddFont(Resources, @"Fonts/Torus-SemiBold");
- AddFont(Resources, @"Fonts/Torus-Bold");
+ AddFont(Resources, @"Fonts/Torus/Torus-Regular");
+ AddFont(Resources, @"Fonts/Torus/Torus-Light");
+ AddFont(Resources, @"Fonts/Torus/Torus-SemiBold");
+ AddFont(Resources, @"Fonts/Torus/Torus-Bold");
- AddFont(Resources, @"Fonts/Noto-Basic");
- AddFont(Resources, @"Fonts/Noto-Hangul");
- AddFont(Resources, @"Fonts/Noto-CJK-Basic");
- AddFont(Resources, @"Fonts/Noto-CJK-Compatibility");
- AddFont(Resources, @"Fonts/Noto-Thai");
+ AddFont(Resources, @"Fonts/Inter/Inter-Regular");
+ AddFont(Resources, @"Fonts/Inter/Inter-RegularItalic");
+ AddFont(Resources, @"Fonts/Inter/Inter-Light");
+ AddFont(Resources, @"Fonts/Inter/Inter-LightItalic");
+ AddFont(Resources, @"Fonts/Inter/Inter-SemiBold");
+ AddFont(Resources, @"Fonts/Inter/Inter-SemiBoldItalic");
+ AddFont(Resources, @"Fonts/Inter/Inter-Bold");
+ AddFont(Resources, @"Fonts/Inter/Inter-BoldItalic");
- AddFont(Resources, @"Fonts/Venera-Light");
- AddFont(Resources, @"Fonts/Venera-Bold");
- AddFont(Resources, @"Fonts/Venera-Black");
+ AddFont(Resources, @"Fonts/Noto/Noto-Basic");
+ AddFont(Resources, @"Fonts/Noto/Noto-Hangul");
+ AddFont(Resources, @"Fonts/Noto/Noto-CJK-Basic");
+ AddFont(Resources, @"Fonts/Noto/Noto-CJK-Compatibility");
+ AddFont(Resources, @"Fonts/Noto/Noto-Thai");
+
+ AddFont(Resources, @"Fonts/Venera/Venera-Light");
+ AddFont(Resources, @"Fonts/Venera/Venera-Bold");
+ AddFont(Resources, @"Fonts/Venera/Venera-Black");
Audio.Samples.PlaybackConcurrency = SAMPLE_CONCURRENCY;
@@ -275,7 +289,8 @@ namespace osu.Game
dependencies.Cache(scorePerformanceManager);
AddInternal(scorePerformanceManager);
- dependencies.Cache(KeyBindingStore = new KeyBindingStore(contextFactory, RulesetStore));
+ migrateDataToRealm();
+
dependencies.Cache(settingsStore = new SettingsStore(contextFactory));
dependencies.Cache(rulesetConfigCache = new RulesetConfigCache(settingsStore));
@@ -323,7 +338,12 @@ namespace osu.Game
base.Content.Add(CreateScalingContainer().WithChildren(mainContent));
+ KeyBindingStore = new RealmKeyBindingStore(realmFactory);
KeyBindingStore.Register(globalBindings);
+
+ foreach (var r in RulesetStore.AvailableRulesets)
+ KeyBindingStore.Register(r);
+
dependencies.Cache(globalBindings);
PreviewTrackManager previewTrackManager;
@@ -378,8 +398,11 @@ namespace osu.Game
public void Migrate(string path)
{
- contextFactory.FlushConnections();
- (Storage as OsuStorage)?.Migrate(Host.GetStorage(path));
+ using (realmFactory.BlockAllOperations())
+ {
+ contextFactory.FlushConnections();
+ (Storage as OsuStorage)?.Migrate(Host.GetStorage(path));
+ }
}
protected override UserInputManager CreateUserInputManager() => new OsuUserInputManager();
@@ -390,6 +413,34 @@ namespace osu.Game
protected override Storage CreateStorage(GameHost host, Storage defaultStorage) => new OsuStorage(host, defaultStorage);
+ private void migrateDataToRealm()
+ {
+ using (var db = contextFactory.GetForWrite())
+ using (var usage = realmFactory.GetForWrite())
+ {
+ var existingBindings = db.Context.DatabasedKeyBinding;
+
+ // only migrate data if the realm database is empty.
+ if (!usage.Realm.All().Any())
+ {
+ foreach (var dkb in existingBindings)
+ {
+ usage.Realm.Add(new RealmKeyBinding
+ {
+ KeyCombinationString = dkb.KeyCombination.ToString(),
+ ActionInt = (int)dkb.Action,
+ RulesetID = dkb.RulesetID,
+ Variant = dkb.Variant
+ });
+ }
+ }
+
+ db.Context.RemoveRange(existingBindings);
+
+ usage.Commit();
+ }
+ }
+
private void onRulesetChanged(ValueChangedEvent r)
{
var dict = new Dictionary>();
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/Comments/CommentsHeader.cs b/osu.Game/Overlays/Comments/CommentsHeader.cs
index 0dd68bbd41..bf80655c3d 100644
--- a/osu.Game/Overlays/Comments/CommentsHeader.cs
+++ b/osu.Game/Overlays/Comments/CommentsHeader.cs
@@ -2,6 +2,8 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
+using osu.Framework.Audio;
+using osu.Framework.Audio.Sample;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics;
using osu.Framework.Bindables;
@@ -66,6 +68,8 @@ namespace osu.Game.Overlays.Comments
public readonly BindableBool Checked = new BindableBool();
private readonly SpriteIcon checkboxIcon;
+ private Sample sampleChecked;
+ private Sample sampleUnchecked;
public ShowDeletedButton()
{
@@ -93,6 +97,13 @@ namespace osu.Game.Overlays.Comments
});
}
+ [BackgroundDependencyLoader]
+ private void load(AudioManager audio)
+ {
+ sampleChecked = audio.Samples.Get(@"UI/check-on");
+ sampleUnchecked = audio.Samples.Get(@"UI/check-off");
+ }
+
protected override void LoadComplete()
{
Checked.BindValueChanged(isChecked => checkboxIcon.Icon = isChecked.NewValue ? FontAwesome.Solid.CheckSquare : FontAwesome.Regular.Square, true);
@@ -102,6 +113,12 @@ namespace osu.Game.Overlays.Comments
protected override bool OnClick(ClickEvent e)
{
Checked.Value = !Checked.Value;
+
+ if (Checked.Value)
+ sampleChecked?.Play();
+ else
+ sampleUnchecked?.Play();
+
return true;
}
}
diff --git a/osu.Game/Overlays/Comments/HeaderButton.cs b/osu.Game/Overlays/Comments/HeaderButton.cs
index fdc8db35ab..65172aa57c 100644
--- a/osu.Game/Overlays/Comments/HeaderButton.cs
+++ b/osu.Game/Overlays/Comments/HeaderButton.cs
@@ -6,7 +6,6 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
-using osu.Game.Graphics.UserInterface;
namespace osu.Game.Overlays.Comments
{
@@ -39,7 +38,6 @@ namespace osu.Game.Overlays.Comments
Origin = Anchor.Centre,
Margin = new MarginPadding { Horizontal = 10 }
},
- new HoverClickSounds(),
});
}
diff --git a/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs b/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs
index 0df3359c28..ef620df171 100644
--- a/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs
+++ b/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
@@ -13,6 +14,7 @@ using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
+using osu.Game.Database;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
@@ -27,7 +29,7 @@ namespace osu.Game.Overlays.KeyBinding
public class KeyBindingRow : Container, IFilterable
{
private readonly object action;
- private readonly IEnumerable bindings;
+ private readonly IEnumerable bindings;
private const float transition_time = 150;
@@ -62,7 +64,7 @@ namespace osu.Game.Overlays.KeyBinding
public IEnumerable FilterTerms => bindings.Select(b => b.KeyCombination.ReadableString()).Prepend(text.Text.ToString());
- public KeyBindingRow(object action, IEnumerable bindings)
+ public KeyBindingRow(object action, List bindings)
{
this.action = action;
this.bindings = bindings;
@@ -72,7 +74,7 @@ namespace osu.Game.Overlays.KeyBinding
}
[Resolved]
- private KeyBindingStore store { get; set; }
+ private RealmContextFactory realmFactory { get; set; }
[BackgroundDependencyLoader]
private void load(OsuColour colours)
@@ -153,7 +155,8 @@ namespace osu.Game.Overlays.KeyBinding
{
var button = buttons[i++];
button.UpdateKeyCombination(d);
- store.Update(button.KeyBinding);
+
+ updateStoreFromButton(button);
}
isDefault.Value = true;
@@ -314,7 +317,7 @@ namespace osu.Game.Overlays.KeyBinding
{
if (bindTarget != null)
{
- store.Update(bindTarget.KeyBinding);
+ updateStoreFromButton(bindTarget);
updateIsDefaultValue();
@@ -361,6 +364,17 @@ namespace osu.Game.Overlays.KeyBinding
if (bindTarget != null) bindTarget.IsBinding = true;
}
+ private void updateStoreFromButton(KeyButton button)
+ {
+ using (var usage = realmFactory.GetForWrite())
+ {
+ var binding = usage.Realm.Find(((IHasGuidPrimaryKey)button.KeyBinding).ID);
+ binding.KeyCombinationString = button.KeyBinding.KeyCombinationString;
+
+ usage.Commit();
+ }
+ }
+
private void updateIsDefaultValue()
{
isDefault.Value = bindings.Select(b => b.KeyCombination).SequenceEqual(Defaults);
@@ -386,7 +400,7 @@ namespace osu.Game.Overlays.KeyBinding
public class KeyButton : Container
{
- public readonly Framework.Input.Bindings.KeyBinding KeyBinding;
+ public readonly RealmKeyBinding KeyBinding;
private readonly Box box;
public readonly OsuSpriteText Text;
@@ -408,8 +422,11 @@ namespace osu.Game.Overlays.KeyBinding
}
}
- public KeyButton(Framework.Input.Bindings.KeyBinding keyBinding)
+ public KeyButton(RealmKeyBinding keyBinding)
{
+ if (keyBinding.IsManaged)
+ throw new ArgumentException("Key binding should not be attached as we make temporary changes", nameof(keyBinding));
+
KeyBinding = keyBinding;
Margin = new MarginPadding(padding);
@@ -478,7 +495,7 @@ namespace osu.Game.Overlays.KeyBinding
public void UpdateKeyCombination(KeyCombination newCombination)
{
- if ((KeyBinding as DatabasedKeyBinding)?.RulesetID != null && !KeyBindingStore.CheckValidForGameplay(newCombination))
+ if (KeyBinding.RulesetID != null && !RealmKeyBindingStore.CheckValidForGameplay(newCombination))
return;
KeyBinding.KeyCombination = newCombination;
diff --git a/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs b/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs
index 5e1f9d8f75..1fdc1b6574 100644
--- a/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs
+++ b/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs
@@ -6,8 +6,9 @@ using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
+using osu.Game.Database;
using osu.Game.Graphics.UserInterface;
-using osu.Game.Input;
+using osu.Game.Input.Bindings;
using osu.Game.Overlays.Settings;
using osu.Game.Rulesets;
using osuTK;
@@ -31,16 +32,21 @@ namespace osu.Game.Overlays.KeyBinding
}
[BackgroundDependencyLoader]
- private void load(KeyBindingStore store)
+ private void load(RealmContextFactory realmFactory)
{
- var bindings = store.Query(Ruleset?.ID, variant);
+ var rulesetId = Ruleset?.ID;
+
+ List bindings;
+
+ using (var usage = realmFactory.GetForRead())
+ bindings = usage.Realm.All().Where(b => b.RulesetID == rulesetId && b.Variant == variant).Detach();
foreach (var defaultGroup in Defaults.GroupBy(d => d.Action))
{
int intKey = (int)defaultGroup.Key;
// one row per valid action.
- Add(new KeyBindingRow(defaultGroup.Key, bindings.Where(b => ((int)b.Action).Equals(intKey)))
+ Add(new KeyBindingRow(defaultGroup.Key, bindings.Where(b => b.ActionInt.Equals(intKey)).ToList())
{
AllowMainMouseButtons = Ruleset != null,
Defaults = defaultGroup.Select(d => d.KeyCombination)
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/ModButton.cs b/osu.Game/Overlays/Mods/ModButton.cs
index 5e3733cd5e..70424101fd 100644
--- a/osu.Game/Overlays/Mods/ModButton.cs
+++ b/osu.Game/Overlays/Mods/ModButton.cs
@@ -302,7 +302,7 @@ namespace osu.Game.Overlays.Mods
Anchor = Anchor.TopCentre,
Font = OsuFont.GetFont(size: 18)
},
- new HoverClickSounds(buttons: new[] { MouseButton.Left, MouseButton.Right })
+ new HoverSounds()
};
Mod = 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/OverlayScrollContainer.cs b/osu.Game/Overlays/OverlayScrollContainer.cs
index 0004719b87..c5b4cc3645 100644
--- a/osu.Game/Overlays/OverlayScrollContainer.cs
+++ b/osu.Game/Overlays/OverlayScrollContainer.cs
@@ -11,6 +11,7 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Game.Graphics.Containers;
+using osu.Game.Graphics.UserInterface;
using osuTK;
using osuTK.Graphics;
@@ -84,6 +85,7 @@ namespace osu.Game.Overlays
private readonly Box background;
public ScrollToTopButton()
+ : base(HoverSampleSet.ScrollToTop)
{
Size = new Vector2(50);
Alpha = 0;
diff --git a/osu.Game/Overlays/OverlaySortTabControl.cs b/osu.Game/Overlays/OverlaySortTabControl.cs
index 0ebabd424f..d4dde0db3f 100644
--- a/osu.Game/Overlays/OverlaySortTabControl.cs
+++ b/osu.Game/Overlays/OverlaySortTabControl.cs
@@ -18,6 +18,7 @@ using JetBrains.Annotations;
using System;
using osu.Framework.Extensions;
using osu.Framework.Localisation;
+using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Overlays
{
@@ -54,7 +55,7 @@ namespace osu.Game.Overlays
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold),
- Text = @"Sort by"
+ Text = SortStrings.Default
},
CreateControl().With(c =>
{
@@ -143,10 +144,12 @@ 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()
}
}
});
+
+ AddInternal(new HoverClickSounds());
}
protected override void LoadComplete()
diff --git a/osu.Game/Overlays/OverlayTabControl.cs b/osu.Game/Overlays/OverlayTabControl.cs
index a1cbf2c1e7..578cd703c7 100644
--- a/osu.Game/Overlays/OverlayTabControl.cs
+++ b/osu.Game/Overlays/OverlayTabControl.cs
@@ -99,7 +99,7 @@ namespace osu.Game.Overlays
ExpandedSize = 5f,
CollapsedSize = 0
},
- new HoverClickSounds()
+ new HoverClickSounds(HoverSampleSet.TabSelect)
};
}
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/Settings/Sections/General/LanguageSettings.cs b/osu.Game/Overlays/Settings/Sections/General/LanguageSettings.cs
index c2767f61b4..dfcdb8e340 100644
--- a/osu.Game/Overlays/Settings/Sections/General/LanguageSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/General/LanguageSettings.cs
@@ -1,11 +1,11 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Configuration;
using osu.Framework.Graphics;
+using osu.Game.Extensions;
using osu.Game.Localisation;
namespace osu.Game.Overlays.Settings.Sections.General
@@ -35,11 +35,11 @@ namespace osu.Game.Overlays.Settings.Sections.General
},
};
- if (!Enum.TryParse(frameworkLocale.Value, out var locale))
+ if (!LanguageExtensions.TryParseCultureCode(frameworkLocale.Value, out var locale))
locale = Language.en;
languageSelection.Current.Value = locale;
- languageSelection.Current.BindValueChanged(val => frameworkLocale.Value = val.NewValue.ToString());
+ languageSelection.Current.BindValueChanged(val => frameworkLocale.Value = val.NewValue.ToCultureCode());
}
}
}
diff --git a/osu.Game/Overlays/Settings/SettingsEnumDropdown.cs b/osu.Game/Overlays/Settings/SettingsEnumDropdown.cs
index c77d14632b..9987a0c607 100644
--- a/osu.Game/Overlays/Settings/SettingsEnumDropdown.cs
+++ b/osu.Game/Overlays/Settings/SettingsEnumDropdown.cs
@@ -19,6 +19,8 @@ namespace osu.Game.Overlays.Settings
Margin = new MarginPadding { Top = 5 };
RelativeSizeAxes = Axes.X;
}
+
+ protected override DropdownMenu CreateMenu() => base.CreateMenu().With(m => m.MaxHeight = 200);
}
}
}
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/ToolbarButton.cs b/osu.Game/Overlays/Toolbar/ToolbarButton.cs
index 1933422dd9..432c52c2e9 100644
--- a/osu.Game/Overlays/Toolbar/ToolbarButton.cs
+++ b/osu.Game/Overlays/Toolbar/ToolbarButton.cs
@@ -1,8 +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.Linq;
using osu.Framework.Allocation;
-using osu.Framework.Caching;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics;
@@ -13,13 +13,13 @@ using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
+using osu.Game.Database;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Backgrounds;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
-using osu.Game.Input;
using osu.Game.Input.Bindings;
using osuTK;
using osuTK.Graphics;
@@ -76,7 +76,7 @@ namespace osu.Game.Overlays.Toolbar
protected FillFlowContainer Flow;
[Resolved]
- private KeyBindingStore keyBindings { get; set; }
+ private RealmContextFactory realmFactory { get; set; }
protected ToolbarButton()
: base(HoverSampleSet.Toolbar)
@@ -159,27 +159,28 @@ namespace osu.Game.Overlays.Toolbar
};
}
- private readonly Cached tooltipKeyBinding = new Cached();
+ private RealmKeyBinding realmKeyBinding;
- [BackgroundDependencyLoader]
- private void load()
+ protected override void LoadComplete()
{
- keyBindings.KeyBindingChanged += () => tooltipKeyBinding.Invalidate();
+ base.LoadComplete();
+
+ if (Hotkey == null) return;
+
+ realmKeyBinding = realmFactory.Context.All().FirstOrDefault(rkb => rkb.RulesetID == null && rkb.ActionInt == (int)Hotkey.Value);
+
+ if (realmKeyBinding != null)
+ {
+ realmKeyBinding.PropertyChanged += (sender, args) =>
+ {
+ if (args.PropertyName == nameof(realmKeyBinding.KeyCombinationString))
+ updateKeyBindingTooltip();
+ };
+ }
+
updateKeyBindingTooltip();
}
- private void updateKeyBindingTooltip()
- {
- if (tooltipKeyBinding.IsValid)
- return;
-
- var binding = keyBindings.Query().Find(b => (GlobalAction)b.Action == Hotkey);
- var keyBindingString = binding?.KeyCombination.ReadableString();
- keyBindingTooltip.Text = !string.IsNullOrEmpty(keyBindingString) ? $" ({keyBindingString})" : string.Empty;
-
- tooltipKeyBinding.Validate();
- }
-
protected override bool OnMouseDown(MouseDownEvent e) => true;
protected override bool OnClick(ClickEvent e)
@@ -218,6 +219,17 @@ namespace osu.Game.Overlays.Toolbar
public void OnReleased(GlobalAction action)
{
}
+
+ private void updateKeyBindingTooltip()
+ {
+ if (realmKeyBinding != null)
+ {
+ var keyBindingString = realmKeyBinding.KeyCombination.ReadableString();
+
+ if (!string.IsNullOrEmpty(keyBindingString))
+ keyBindingTooltip.Text = $" ({keyBindingString})";
+ }
+ }
}
public class OpaqueBackground : Container
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/Performance/HighPerformanceSession.cs b/osu.Game/Performance/HighPerformanceSession.cs
new file mode 100644
index 0000000000..661c1046f1
--- /dev/null
+++ b/osu.Game/Performance/HighPerformanceSession.cs
@@ -0,0 +1,41 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+
+namespace osu.Game.Performance
+{
+ public class HighPerformanceSession : Component
+ {
+ private readonly IBindable localUserPlaying = new Bindable();
+
+ [BackgroundDependencyLoader]
+ private void load(OsuGame game)
+ {
+ localUserPlaying.BindTo(game.LocalUserPlaying);
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ localUserPlaying.BindValueChanged(playing =>
+ {
+ if (playing.NewValue)
+ EnableHighPerformanceSession();
+ else
+ DisableHighPerformanceSession();
+ }, true);
+ }
+
+ protected virtual void EnableHighPerformanceSession()
+ {
+ }
+
+ protected virtual void DisableHighPerformanceSession()
+ {
+ }
+ }
+}
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/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
index 7fc35fc778..a0717ec38e 100644
--- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
+++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
@@ -156,10 +156,11 @@ namespace osu.Game.Rulesets.Objects.Drawables
/// If null, a hitobject is expected to be later applied via (or automatically via pooling).
///
protected DrawableHitObject([CanBeNull] HitObject initialHitObject = null)
- : base(initialHitObject != null ? new SyntheticHitObjectEntry(initialHitObject) : null)
{
- if (Entry != null)
- ensureEntryHasResult();
+ if (initialHitObject == null) return;
+
+ Entry = new SyntheticHitObjectEntry(initialHitObject);
+ ensureEntryHasResult();
}
[BackgroundDependencyLoader]
diff --git a/osu.Game/Rulesets/Objects/Pooling/PoolableDrawableWithLifetime.cs b/osu.Game/Rulesets/Objects/Pooling/PoolableDrawableWithLifetime.cs
index 4440ca8d21..9c6097a048 100644
--- a/osu.Game/Rulesets/Objects/Pooling/PoolableDrawableWithLifetime.cs
+++ b/osu.Game/Rulesets/Objects/Pooling/PoolableDrawableWithLifetime.cs
@@ -5,6 +5,7 @@
using System;
using System.Diagnostics;
+using osu.Framework.Graphics;
using osu.Framework.Graphics.Performance;
using osu.Framework.Graphics.Pooling;
@@ -16,14 +17,32 @@ namespace osu.Game.Rulesets.Objects.Pooling
/// The type storing state and controlling this drawable.
public abstract class PoolableDrawableWithLifetime : PoolableDrawable where TEntry : LifetimeEntry
{
+ private TEntry? entry;
+
///
/// The entry holding essential state of this .
///
- public TEntry? Entry { get; private set; }
+ ///
+ /// If a non-null value is set before loading is started, the entry is applied when the loading is completed.
+ /// It is not valid to set an entry while this is loading.
+ ///
+ public TEntry? Entry
+ {
+ get => entry;
+ set
+ {
+ if (LoadState == LoadState.NotLoaded)
+ entry = value;
+ else if (value != null)
+ Apply(value);
+ else if (HasEntryApplied)
+ free();
+ }
+ }
///
/// Whether is applied to this .
- /// When an initial entry is specified in the constructor, is set but not applied until loading is completed.
+ /// When an is set during initialization, it is not applied until loading is completed.
///
protected bool HasEntryApplied { get; private set; }
@@ -65,9 +84,9 @@ namespace osu.Game.Rulesets.Objects.Pooling
{
base.LoadAsyncComplete();
- // Apply the initial entry given in the constructor.
+ // Apply the initial entry.
if (Entry != null && !HasEntryApplied)
- Apply(Entry);
+ apply(Entry);
}
///
@@ -76,16 +95,10 @@ namespace osu.Game.Rulesets.Objects.Pooling
///
public void Apply(TEntry entry)
{
- if (HasEntryApplied)
- free();
+ if (LoadState == LoadState.Loading)
+ throw new InvalidOperationException($"Cannot apply a new {nameof(TEntry)} while currently loading.");
- Entry = entry;
- entry.LifetimeChanged += setLifetimeFromEntry;
- setLifetimeFromEntry(entry);
-
- OnApply(entry);
-
- HasEntryApplied = true;
+ apply(entry);
}
protected sealed override void FreeAfterUse()
@@ -111,6 +124,20 @@ namespace osu.Game.Rulesets.Objects.Pooling
{
}
+ private void apply(TEntry entry)
+ {
+ if (HasEntryApplied)
+ free();
+
+ this.entry = entry;
+ entry.LifetimeChanged += setLifetimeFromEntry;
+ setLifetimeFromEntry(entry);
+
+ OnApply(entry);
+
+ HasEntryApplied = true;
+ }
+
private void free()
{
Debug.Assert(Entry != null && HasEntryApplied);
@@ -118,7 +145,7 @@ namespace osu.Game.Rulesets.Objects.Pooling
OnFree(Entry);
Entry.LifetimeChanged -= setLifetimeFromEntry;
- Entry = null;
+ entry = null;
base.LifetimeStart = double.MinValue;
base.LifetimeEnd = double.MaxValue;
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/Rulesets/UI/RulesetInputManager.cs b/osu.Game/Rulesets/UI/RulesetInputManager.cs
index e3b9ad5641..e6cd2aa3dc 100644
--- a/osu.Game/Rulesets/UI/RulesetInputManager.cs
+++ b/osu.Game/Rulesets/UI/RulesetInputManager.cs
@@ -177,7 +177,7 @@ namespace osu.Game.Rulesets.UI
{
base.ReloadMappings();
- KeyBindings = KeyBindings.Where(b => KeyBindingStore.CheckValidForGameplay(b.KeyCombination)).ToList();
+ KeyBindings = KeyBindings.Where(b => RealmKeyBindingStore.CheckValidForGameplay(b.KeyCombination)).ToList();
}
}
}
diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs
index 94cc7ed095..3b15bc2cdf 100644
--- a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs
+++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs
@@ -69,7 +69,7 @@ namespace osu.Game.Rulesets.UI.Scrolling
///
public double TimeAtPosition(float localPosition, double currentTime)
{
- float scrollPosition = axisInverted ? scrollLength - localPosition : localPosition;
+ float scrollPosition = axisInverted ? -localPosition : localPosition;
return scrollingInfo.Algorithm.TimeAt(scrollPosition, currentTime, timeRange.Value, scrollLength);
}
@@ -81,8 +81,10 @@ namespace osu.Game.Rulesets.UI.Scrolling
///
public double TimeAtScreenSpacePosition(Vector2 screenSpacePosition)
{
- Vector2 localPosition = ToLocalSpace(screenSpacePosition);
- return TimeAtPosition(scrollingAxis == Direction.Horizontal ? localPosition.X : localPosition.Y, Time.Current);
+ Vector2 pos = ToLocalSpace(screenSpacePosition);
+ float localPosition = scrollingAxis == Direction.Horizontal ? pos.X : pos.Y;
+ localPosition -= axisInverted ? scrollLength : 0;
+ return TimeAtPosition(localPosition, Time.Current);
}
///
@@ -91,7 +93,7 @@ namespace osu.Game.Rulesets.UI.Scrolling
public float PositionAtTime(double time, double currentTime)
{
float scrollPosition = scrollingInfo.Algorithm.PositionAt(time, currentTime, timeRange.Value, scrollLength);
- return axisInverted ? scrollLength - scrollPosition : scrollPosition;
+ return axisInverted ? -scrollPosition : scrollPosition;
}
///
@@ -106,6 +108,7 @@ namespace osu.Game.Rulesets.UI.Scrolling
public Vector2 ScreenSpacePositionAtTime(double time)
{
float localPosition = PositionAtTime(time, Time.Current);
+ localPosition += axisInverted ? scrollLength : 0;
return scrollingAxis == Direction.Horizontal
? ToScreenSpace(new Vector2(localPosition, DrawHeight / 2))
: ToScreenSpace(new Vector2(DrawWidth / 2, localPosition));
@@ -236,14 +239,10 @@ namespace osu.Game.Rulesets.UI.Scrolling
{
float position = PositionAtTime(hitObject.HitObject.StartTime, currentTime);
- // The position returned from `PositionAtTime` is assuming the `TopLeft` anchor.
- // A correction is needed because the hit objects are using a different anchor for each direction (e.g. `BottomCentre` for `Bottom` direction).
- float anchorCorrection = axisInverted ? scrollLength : 0;
-
if (scrollingAxis == Direction.Horizontal)
- hitObject.X = position - anchorCorrection;
+ hitObject.X = position;
else
- hitObject.Y = position - anchorCorrection;
+ hitObject.Y = position;
}
}
}
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 fbcc7ea96f..efc5fcfbe5 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)
{
@@ -295,12 +289,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())
@@ -376,7 +370,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
@@ -508,19 +502,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;
@@ -543,7 +543,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.
@@ -551,14 +551,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();
}
@@ -622,98 +620,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;
@@ -911,13 +952,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.
@@ -980,7 +1014,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/Screens/Select/FooterButton.cs b/osu.Game/Screens/Select/FooterButton.cs
index afb3943a09..c3fbd767ff 100644
--- a/osu.Game/Screens/Select/FooterButton.cs
+++ b/osu.Game/Screens/Select/FooterButton.cs
@@ -66,7 +66,7 @@ namespace osu.Game.Screens.Select
private readonly Box light;
public FooterButton()
- : base(HoverSampleSet.SongSelect)
+ : base(HoverSampleSet.Button)
{
AutoSizeAxes = Axes.Both;
Shear = SHEAR;
diff --git a/osu.Game/Skinning/SkinnableTargetContainer.cs b/osu.Game/Skinning/SkinnableTargetContainer.cs
index 1338462dd6..53b142f09a 100644
--- a/osu.Game/Skinning/SkinnableTargetContainer.cs
+++ b/osu.Game/Skinning/SkinnableTargetContainer.cs
@@ -18,6 +18,8 @@ namespace osu.Game.Skinning
private readonly BindableList components = new BindableList();
+ public bool ComponentsLoaded { get; private set; }
+
public SkinnableTargetContainer(SkinnableTarget target)
{
Target = target;
@@ -30,6 +32,7 @@ namespace osu.Game.Skinning
{
ClearInternal();
components.Clear();
+ ComponentsLoaded = false;
content = CurrentSkin.GetDrawableComponent(new SkinnableTargetComponent(Target)) as SkinnableTargetComponentsContainer;
@@ -39,8 +42,11 @@ namespace osu.Game.Skinning
{
AddInternal(wrapper);
components.AddRange(wrapper.Children.OfType());
+ ComponentsLoaded = true;
});
}
+ else
+ ComponentsLoaded = true;
}
///
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/Tests/Visual/LegacySkinPlayerTestScene.cs b/osu.Game/Tests/Visual/LegacySkinPlayerTestScene.cs
index 14a928d3c1..f4f351d46c 100644
--- a/osu.Game/Tests/Visual/LegacySkinPlayerTestScene.cs
+++ b/osu.Game/Tests/Visual/LegacySkinPlayerTestScene.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions.IEnumerableExtensions;
@@ -43,6 +44,8 @@ namespace osu.Game.Tests.Visual
LegacySkin.ResetDrawableTarget(t);
t.Reload();
}));
+
+ AddUntilStep("wait for components to load", () => this.ChildrenOfType().All(t => t.ComponentsLoaded));
}
}
}
diff --git a/osu.Game/Tests/Visual/OsuTestScene.cs b/osu.Game/Tests/Visual/OsuTestScene.cs
index a4c78f24e3..98aad821ce 100644
--- a/osu.Game/Tests/Visual/OsuTestScene.cs
+++ b/osu.Game/Tests/Visual/OsuTestScene.cs
@@ -350,7 +350,7 @@ namespace osu.Game.Tests.Visual
if (CurrentTime >= Length)
{
Stop();
- RaiseCompleted();
+ // `RaiseCompleted` is not called here to prevent transitioning to the next song.
}
}
}
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 8eeaad1127..3c52405f8e 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -18,6 +18,7 @@
+
@@ -34,8 +35,9 @@
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
+
+
+
diff --git a/osu.iOS.props b/osu.iOS.props
index db442238ce..3689ce51f2 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -70,8 +70,8 @@
-
-
+
+
@@ -93,11 +93,12 @@
-
+
+