diff --git a/osu.Android.props b/osu.Android.props
index 3a9fdfebab..1c180a6b39 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -52,7 +52,7 @@
-
+
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchSkinConfiguration.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchSkinConfiguration.cs
index 83f28086e6..8ae2bcca0e 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchSkinConfiguration.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchSkinConfiguration.cs
@@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Catch.Tests
var skin = new TestSkin { FlipCatcherPlate = flip };
container.Child = new SkinProvidingContainer(skin)
{
- Child = catcher = new Catcher(new Container(), new DroppedObjectContainer())
+ Child = catcher = new Catcher(new DroppedObjectContainer())
{
Anchor = Anchor.Centre
}
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs
index b4282e6784..540f02580f 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs
@@ -31,8 +31,6 @@ namespace osu.Game.Rulesets.Catch.Tests
[Resolved]
private OsuConfigManager config { get; set; }
- private Container trailContainer;
-
private DroppedObjectContainer droppedObjectContainer;
private TestCatcher catcher;
@@ -45,7 +43,6 @@ namespace osu.Game.Rulesets.Catch.Tests
CircleSize = 0,
};
- trailContainer = new Container();
droppedObjectContainer = new DroppedObjectContainer();
Child = new Container
@@ -54,8 +51,7 @@ namespace osu.Game.Rulesets.Catch.Tests
Children = new Drawable[]
{
droppedObjectContainer,
- catcher = new TestCatcher(trailContainer, droppedObjectContainer, difficulty),
- trailContainer,
+ catcher = new TestCatcher(droppedObjectContainer, difficulty),
}
};
});
@@ -294,8 +290,8 @@ namespace osu.Game.Rulesets.Catch.Tests
{
public IEnumerable CaughtObjects => this.ChildrenOfType();
- public TestCatcher(Container trailsTarget, DroppedObjectContainer droppedObjectTarget, BeatmapDifficulty difficulty)
- : base(trailsTarget, droppedObjectTarget, difficulty)
+ public TestCatcher(DroppedObjectContainer droppedObjectTarget, BeatmapDifficulty difficulty)
+ : base(droppedObjectTarget, difficulty)
{
}
}
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs
index 6a518cf0ef..a3307c9224 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs
@@ -122,10 +122,9 @@ namespace osu.Game.Rulesets.Catch.Tests
public TestCatcherArea(BeatmapDifficulty beatmapDifficulty)
{
var droppedObjectContainer = new DroppedObjectContainer();
-
Add(droppedObjectContainer);
- Catcher = new Catcher(this, droppedObjectContainer, beatmapDifficulty)
+ Catcher = new Catcher(droppedObjectContainer, beatmapDifficulty)
{
X = CatchPlayfield.CENTER_X
};
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs
index 73797d0a6a..70b2c8c82a 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs
@@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Catch.Tests
}
[Test]
- public void TestCustomEndGlowColour()
+ public void TestCustomAfterImageColour()
{
var skin = new TestSkin
{
@@ -58,7 +58,7 @@ namespace osu.Game.Rulesets.Catch.Tests
}
[Test]
- public void TestCustomEndGlowColourPriority()
+ public void TestCustomAfterImageColourPriority()
{
var skin = new TestSkin
{
@@ -111,39 +111,37 @@ namespace osu.Game.Rulesets.Catch.Tests
checkHyperDashFruitColour(skin, skin.HyperDashColour);
}
- private void checkHyperDashCatcherColour(ISkin skin, Color4 expectedCatcherColour, Color4? expectedEndGlowColour = null)
+ private void checkHyperDashCatcherColour(ISkin skin, Color4 expectedCatcherColour, Color4? expectedAfterImageColour = null)
{
- Container trailsContainer = null;
- Catcher catcher = null;
CatcherTrailDisplay trails = null;
+ Catcher catcher = null;
AddStep("create hyper-dashing catcher", () =>
{
- trailsContainer = new Container();
+ CatcherArea catcherArea;
Child = setupSkinHierarchy(new Container
{
Anchor = Anchor.Centre,
- Children = new Drawable[]
+ Child = catcherArea = new CatcherArea
{
- catcher = new Catcher(trailsContainer, new DroppedObjectContainer())
+ Catcher = catcher = new Catcher(new DroppedObjectContainer())
{
Scale = new Vector2(4)
- },
- trailsContainer
+ }
}
}, skin);
+ trails = catcherArea.ChildrenOfType().Single();
});
- AddStep("get trails container", () =>
+ AddStep("start hyper-dash", () =>
{
- trails = trailsContainer.OfType().Single();
catcher.SetHyperDashState(2);
});
AddUntilStep("catcher colour is correct", () => catcher.Colour == expectedCatcherColour);
AddAssert("catcher trails colours are correct", () => trails.HyperDashTrailsColour == expectedCatcherColour);
- AddAssert("catcher end-glow colours are correct", () => trails.EndGlowSpritesColour == (expectedEndGlowColour ?? expectedCatcherColour));
+ AddAssert("catcher after-image colours are correct", () => trails.HyperDashAfterImageColour == (expectedAfterImageColour ?? expectedCatcherColour));
AddStep("finish hyper-dashing", () =>
{
diff --git a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs
index b43815a8bd..1e20643a08 100644
--- a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs
@@ -3,7 +3,6 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables;
@@ -45,14 +44,9 @@ namespace osu.Game.Rulesets.Catch.UI
[BackgroundDependencyLoader]
private void load()
{
- var trailContainer = new Container
- {
- Anchor = Anchor.BottomLeft,
- Origin = Anchor.TopLeft
- };
var droppedObjectContainer = new DroppedObjectContainer();
- Catcher = new Catcher(trailContainer, droppedObjectContainer, difficulty)
+ Catcher = new Catcher(droppedObjectContainer, difficulty)
{
X = CENTER_X
};
@@ -70,7 +64,6 @@ namespace osu.Game.Rulesets.Catch.UI
Origin = Anchor.TopLeft,
Catcher = Catcher,
},
- trailContainer,
HitObjectContainer,
});
diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs
index b724b84a56..9fd4610e6e 100644
--- a/osu.Game.Rulesets.Catch/UI/Catcher.cs
+++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs
@@ -36,8 +36,7 @@ namespace osu.Game.Rulesets.Catch.UI
public const float ALLOWED_CATCH_RANGE = 0.8f;
///
- /// The default colour used to tint hyper-dash fruit, along with the moving catcher, its trail
- /// and end glow/after-image during a hyper-dash.
+ /// The default colour used to tint hyper-dash fruit, along with the moving catcher, its trail and after-image during a hyper-dash.
///
public static readonly Color4 DEFAULT_HYPER_DASH_COLOUR = Color4.Red;
@@ -71,11 +70,6 @@ namespace osu.Game.Rulesets.Catch.UI
///
private const float caught_fruit_scale_adjust = 0.5f;
- [NotNull]
- private readonly Container trailsTarget;
-
- private CatcherTrailDisplay trails;
-
///
/// Contains caught objects on the plate.
///
@@ -88,30 +82,22 @@ namespace osu.Game.Rulesets.Catch.UI
public CatcherAnimationState CurrentState
{
- get => Body.AnimationState.Value;
- private set => Body.AnimationState.Value = value;
+ get => body.AnimationState.Value;
+ private set => body.AnimationState.Value = value;
}
- private bool dashing;
-
- public bool Dashing
- {
- get => dashing;
- set
- {
- if (value == dashing) return;
-
- dashing = value;
-
- updateTrailVisibility();
- }
- }
+ ///
+ /// Whether the catcher is currently dashing.
+ ///
+ public bool Dashing { get; set; }
///
/// The currently facing direction.
///
public Direction VisualDirection { get; set; } = Direction.Right;
+ public Vector2 BodyScale => Scale * body.Scale;
+
///
/// Whether the contents of the catcher plate should be visually flipped when the catcher direction is changed.
///
@@ -122,10 +108,9 @@ namespace osu.Game.Rulesets.Catch.UI
///
private readonly float catchWidth;
- internal readonly SkinnableCatcher Body;
+ private readonly SkinnableCatcher body;
private Color4 hyperDashColour = DEFAULT_HYPER_DASH_COLOUR;
- private Color4 hyperDashEndGlowColour = DEFAULT_HYPER_DASH_COLOUR;
private double hyperDashModifier = 1;
private int hyperDashDirection;
@@ -138,9 +123,8 @@ namespace osu.Game.Rulesets.Catch.UI
private readonly DrawablePool caughtBananaPool;
private readonly DrawablePool caughtDropletPool;
- public Catcher([NotNull] Container trailsTarget, [NotNull] DroppedObjectContainer droppedObjectTarget, BeatmapDifficulty difficulty = null)
+ public Catcher([NotNull] DroppedObjectContainer droppedObjectTarget, BeatmapDifficulty difficulty = null)
{
- this.trailsTarget = trailsTarget;
this.droppedObjectTarget = droppedObjectTarget;
Origin = Anchor.TopCentre;
@@ -164,7 +148,7 @@ namespace osu.Game.Rulesets.Catch.UI
// offset fruit vertically to better place "above" the plate.
Y = -5
},
- Body = new SkinnableCatcher(),
+ body = new SkinnableCatcher(),
hitExplosionContainer = new HitExplosionContainer
{
Anchor = Anchor.TopCentre,
@@ -177,15 +161,6 @@ namespace osu.Game.Rulesets.Catch.UI
private void load(OsuConfigManager config)
{
hitLighting = config.GetBindable(OsuSetting.HitLighting);
- trails = new CatcherTrailDisplay(this);
- }
-
- protected override void LoadComplete()
- {
- base.LoadComplete();
-
- // don't add in above load as we may potentially modify a parent in an unsafe manner.
- trailsTarget.Add(trails);
}
///
@@ -307,10 +282,7 @@ namespace osu.Game.Rulesets.Catch.UI
hyperDashTargetPosition = targetPosition;
if (!wasHyperDashing)
- {
- trails.DisplayEndGlow();
runHyperDashStateTransition(true);
- }
}
}
@@ -326,13 +298,9 @@ namespace osu.Game.Rulesets.Catch.UI
private void runHyperDashStateTransition(bool hyperDashing)
{
- updateTrailVisibility();
-
this.FadeColour(hyperDashing ? hyperDashColour : Color4.White, HYPER_DASH_TRANSITION_DURATION, Easing.OutQuint);
}
- private void updateTrailVisibility() => trails.DisplayTrail = Dashing || HyperDashing;
-
protected override void SkinChanged(ISkinSource skin)
{
base.SkinChanged(skin);
@@ -341,13 +309,6 @@ namespace osu.Game.Rulesets.Catch.UI
skin.GetConfig(CatchSkinColour.HyperDash)?.Value ??
DEFAULT_HYPER_DASH_COLOUR;
- hyperDashEndGlowColour =
- skin.GetConfig(CatchSkinColour.HyperDashAfterImage)?.Value ??
- hyperDashColour;
-
- trails.HyperDashTrailsColour = hyperDashColour;
- trails.EndGlowSpritesColour = hyperDashEndGlowColour;
-
flipCatcherPlate = skin.GetConfig(CatchSkinConfiguration.FlipCatcherPlate)?.Value ?? true;
runHyperDashStateTransition(HyperDashing);
@@ -358,7 +319,7 @@ namespace osu.Game.Rulesets.Catch.UI
base.Update();
var scaleFromDirection = new Vector2((int)VisualDirection, 1);
- Body.Scale = scaleFromDirection;
+ body.Scale = scaleFromDirection;
caughtObjectContainer.Scale = hitExplosionContainer.Scale = flipCatcherPlate ? scaleFromDirection : Vector2.One;
// Correct overshooting.
diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs
index de0ace9817..78f7e34649 100644
--- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs
@@ -25,17 +25,15 @@ namespace osu.Game.Rulesets.Catch.UI
public Catcher Catcher
{
get => catcher;
- set
- {
- if (catcher != null)
- Remove(catcher);
-
- Add(catcher = value);
- }
+ set => catcherContainer.Child = catcher = value;
}
+ private readonly Container catcherContainer;
+
private readonly CatchComboDisplay comboDisplay;
+ private readonly CatcherTrailDisplay catcherTrails;
+
private Catcher catcher;
///
@@ -45,20 +43,28 @@ namespace osu.Game.Rulesets.Catch.UI
///
private int currentDirection;
+ // TODO: support replay rewind
+ private bool lastHyperDashState;
+
///
/// must be set before loading.
///
public CatcherArea()
{
Size = new Vector2(CatchPlayfield.WIDTH, Catcher.BASE_SIZE);
- Child = comboDisplay = new CatchComboDisplay
+ Children = new Drawable[]
{
- RelativeSizeAxes = Axes.None,
- AutoSizeAxes = Axes.Both,
- Anchor = Anchor.TopLeft,
- Origin = Anchor.Centre,
- Margin = new MarginPadding { Bottom = 350f },
- X = CatchPlayfield.CENTER_X
+ catcherContainer = new Container { RelativeSizeAxes = Axes.Both },
+ catcherTrails = new CatcherTrailDisplay(),
+ comboDisplay = new CatchComboDisplay
+ {
+ RelativeSizeAxes = Axes.None,
+ AutoSizeAxes = Axes.Both,
+ Anchor = Anchor.TopLeft,
+ Origin = Anchor.Centre,
+ Margin = new MarginPadding { Bottom = 350f },
+ X = CatchPlayfield.CENTER_X
+ }
};
}
@@ -102,6 +108,19 @@ namespace osu.Game.Rulesets.Catch.UI
base.UpdateAfterChildren();
comboDisplay.X = Catcher.X;
+
+ if (!lastHyperDashState && Catcher.HyperDashing && Time.Elapsed > 0)
+ catcherTrails.DisplayHyperDashAfterImage(Catcher.CurrentState, Catcher.X, Catcher.BodyScale);
+
+ if (Catcher.Dashing || Catcher.HyperDashing)
+ {
+ double generationInterval = Catcher.HyperDashing ? 25 : 50;
+
+ if (Time.Current - catcherTrails.LastDashTrailTime >= generationInterval)
+ catcherTrails.DisplayDashTrail(Catcher.CurrentState, Catcher.X, Catcher.BodyScale, Catcher.HyperDashing);
+ }
+
+ lastHyperDashState = Catcher.HyperDashing;
}
public void SetCatcherPosition(float X)
diff --git a/osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs b/osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs
index b59fabcb70..abc76fc925 100644
--- a/osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs
@@ -2,10 +2,11 @@
// See the LICENCE file in the repository root for full licence text.
using System;
-using JetBrains.Annotations;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Pooling;
+using osu.Game.Rulesets.Catch.Skinning;
+using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics;
@@ -15,70 +16,27 @@ namespace osu.Game.Rulesets.Catch.UI
/// Represents a component responsible for displaying
/// the appropriate catcher trails when requested to.
///
- public class CatcherTrailDisplay : CompositeDrawable
+ public class CatcherTrailDisplay : SkinReloadableDrawable
{
- private readonly Catcher catcher;
+ ///
+ /// The most recent time a dash trail was added to this container.
+ /// Only alive (not faded out) trails are considered.
+ /// Returns if no dash trail is alive.
+ ///
+ public double LastDashTrailTime => getLastDashTrailTime();
+
+ public Color4 HyperDashTrailsColour => hyperDashTrails.Colour;
+
+ public Color4 HyperDashAfterImageColour => hyperDashAfterImages.Colour;
private readonly DrawablePool trailPool;
private readonly Container dashTrails;
private readonly Container hyperDashTrails;
- private readonly Container endGlowSprites;
+ private readonly Container hyperDashAfterImages;
- private Color4 hyperDashTrailsColour = Catcher.DEFAULT_HYPER_DASH_COLOUR;
-
- public Color4 HyperDashTrailsColour
+ public CatcherTrailDisplay()
{
- get => hyperDashTrailsColour;
- set
- {
- if (hyperDashTrailsColour == value)
- return;
-
- hyperDashTrailsColour = value;
- hyperDashTrails.Colour = hyperDashTrailsColour;
- }
- }
-
- private Color4 endGlowSpritesColour = Catcher.DEFAULT_HYPER_DASH_COLOUR;
-
- public Color4 EndGlowSpritesColour
- {
- get => endGlowSpritesColour;
- set
- {
- if (endGlowSpritesColour == value)
- return;
-
- endGlowSpritesColour = value;
- endGlowSprites.Colour = endGlowSpritesColour;
- }
- }
-
- private bool trail;
-
- ///
- /// Whether to start displaying trails following the catcher.
- ///
- public bool DisplayTrail
- {
- get => trail;
- set
- {
- if (trail == value)
- return;
-
- trail = value;
-
- if (trail)
- displayTrail();
- }
- }
-
- public CatcherTrailDisplay([NotNull] Catcher catcher)
- {
- this.catcher = catcher ?? throw new ArgumentNullException(nameof(catcher));
-
RelativeSizeAxes = Axes.Both;
InternalChildren = new Drawable[]
@@ -86,47 +44,68 @@ namespace osu.Game.Rulesets.Catch.UI
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 },
+ hyperDashAfterImages = new Container { RelativeSizeAxes = Axes.Both, Colour = Catcher.DEFAULT_HYPER_DASH_COLOUR },
};
}
+ protected override void SkinChanged(ISkinSource skin)
+ {
+ base.SkinChanged(skin);
+
+ hyperDashTrails.Colour = skin.GetConfig(CatchSkinColour.HyperDash)?.Value ?? Catcher.DEFAULT_HYPER_DASH_COLOUR;
+ hyperDashAfterImages.Colour = skin.GetConfig(CatchSkinColour.HyperDashAfterImage)?.Value ?? hyperDashTrails.Colour;
+ }
+
///
- /// Displays a single end-glow catcher sprite.
+ /// Displays a hyper-dash after-image of the catcher.
///
- public void DisplayEndGlow()
+ public void DisplayHyperDashAfterImage(CatcherAnimationState animationState, float x, Vector2 scale)
{
- var endGlow = createTrailSprite(endGlowSprites);
+ var trail = createTrail(animationState, x, scale);
- endGlow.MoveToOffset(new Vector2(0, -10), 1200, Easing.In);
- endGlow.ScaleTo(endGlow.Scale * 0.95f).ScaleTo(endGlow.Scale * 1.2f, 1200, Easing.In);
- endGlow.FadeOut(1200);
- endGlow.Expire(true);
+ hyperDashAfterImages.Add(trail);
+
+ trail.MoveToOffset(new Vector2(0, -10), 1200, Easing.In);
+ trail.ScaleTo(trail.Scale * 0.95f).ScaleTo(trail.Scale * 1.2f, 1200, Easing.In);
+ trail.FadeOut(1200);
+ trail.Expire(true);
}
- private void displayTrail()
+ public void DisplayDashTrail(CatcherAnimationState animationState, float x, Vector2 scale, bool hyperDashing)
{
- if (!DisplayTrail)
- return;
+ var trail = createTrail(animationState, x, scale);
- var sprite = createTrailSprite(catcher.HyperDashing ? hyperDashTrails : dashTrails);
+ if (hyperDashing)
+ hyperDashTrails.Add(trail);
+ else
+ dashTrails.Add(trail);
- sprite.FadeTo(0.4f).FadeOut(800, Easing.OutQuint);
- sprite.Expire(true);
-
- Scheduler.AddDelayed(displayTrail, catcher.HyperDashing ? 25 : 50);
+ trail.FadeTo(0.4f).FadeOut(800, Easing.OutQuint);
+ trail.Expire(true);
}
- private CatcherTrail createTrailSprite(Container target)
+ private CatcherTrail createTrail(CatcherAnimationState animationState, float x, Vector2 scale)
{
- CatcherTrail sprite = trailPool.Get();
+ CatcherTrail trail = trailPool.Get();
- sprite.AnimationState = catcher.CurrentState;
- sprite.Scale = catcher.Scale * catcher.Body.Scale;
- sprite.Position = catcher.Position;
+ trail.AnimationState = animationState;
+ trail.Scale = scale;
+ trail.Position = new Vector2(x, 0);
- target.Add(sprite);
+ return trail;
+ }
- return sprite;
+ private double getLastDashTrailTime()
+ {
+ double maxTime = double.NegativeInfinity;
+
+ foreach (var trail in dashTrails)
+ maxTime = Math.Max(maxTime, trail.LifetimeStart);
+
+ foreach (var trail in hyperDashTrails)
+ maxTime = Math.Max(maxTime, trail.LifetimeStart);
+
+ return maxTime;
}
}
}
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModMirror.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModMirror.cs
index cf404cc98e..f01884c97f 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModMirror.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModMirror.cs
@@ -10,13 +10,9 @@ using osu.Game.Rulesets.Mania.Beatmaps;
namespace osu.Game.Rulesets.Mania.Mods
{
- public class ManiaModMirror : Mod, IApplicableToBeatmap
+ public class ManiaModMirror : ModMirror, IApplicableToBeatmap
{
- public override string Name => "Mirror";
- public override string Acronym => "MR";
- public override ModType Type => ModType.Conversion;
public override string Description => "Notes are flipped horizontally.";
- public override double ScoreMultiplier => 1;
public void ApplyToBeatmap(IBeatmap beatmap)
{
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHardRock.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHardRock.cs
index 16c166257a..007820b016 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModHardRock.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModHardRock.cs
@@ -1,13 +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 System;
using System.Linq;
-using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Objects;
-using osu.Game.Rulesets.Osu.UI;
-using osuTK;
+using osu.Game.Rulesets.Osu.Utils;
namespace osu.Game.Rulesets.Osu.Mods
{
@@ -15,23 +14,13 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public override double ScoreMultiplier => 1.06;
+ public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModMirror)).ToArray();
+
public void ApplyToHitObject(HitObject hitObject)
{
var osuObject = (OsuHitObject)hitObject;
- osuObject.Position = new Vector2(osuObject.Position.X, OsuPlayfield.BASE_SIZE.Y - osuObject.Y);
-
- if (!(hitObject is Slider slider))
- return;
-
- slider.NestedHitObjects.OfType().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y));
- slider.NestedHitObjects.OfType().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y));
-
- var controlPoints = slider.Path.ControlPoints.Select(p => new PathControlPoint(p.Position.Value, p.Type.Value)).ToArray();
- foreach (var point in controlPoints)
- point.Position.Value = new Vector2(point.Position.Value.X, -point.Position.Value.Y);
-
- slider.Path = new SliderPath(controlPoints, slider.Path.ExpectedDistance.Value);
+ OsuHitObjectGenerationUtils.ReflectVertically(osuObject);
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModMirror.cs b/osu.Game.Rulesets.Osu/Mods/OsuModMirror.cs
new file mode 100644
index 0000000000..3faca0b01f
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModMirror.cs
@@ -0,0 +1,50 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using osu.Framework.Bindables;
+using osu.Game.Configuration;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.Utils;
+
+namespace osu.Game.Rulesets.Osu.Mods
+{
+ public class OsuModMirror : ModMirror, IApplicableToHitObject
+ {
+ public override string Description => "Flip objects on the chosen axes.";
+ public override Type[] IncompatibleMods => new[] { typeof(ModHardRock) };
+
+ [SettingSource("Mirrored axes", "Choose which axes objects are mirrored over.")]
+ public Bindable Reflection { get; } = new Bindable();
+
+ public void ApplyToHitObject(HitObject hitObject)
+ {
+ var osuObject = (OsuHitObject)hitObject;
+
+ switch (Reflection.Value)
+ {
+ case MirrorType.Horizontal:
+ OsuHitObjectGenerationUtils.ReflectHorizontally(osuObject);
+ break;
+
+ case MirrorType.Vertical:
+ OsuHitObjectGenerationUtils.ReflectVertically(osuObject);
+ break;
+
+ case MirrorType.Both:
+ OsuHitObjectGenerationUtils.ReflectHorizontally(osuObject);
+ OsuHitObjectGenerationUtils.ReflectVertically(osuObject);
+ break;
+ }
+ }
+
+ public enum MirrorType
+ {
+ Horizontal,
+ Vertical,
+ Both
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs
index e21d1da009..210d5e0403 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs
@@ -4,8 +4,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
-using osu.Framework.Allocation;
-using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
@@ -16,7 +14,6 @@ using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Timing;
using osu.Game.Configuration;
using osu.Game.Graphics;
-using osu.Game.Graphics.Containers;
using osu.Game.Overlays.Settings;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
@@ -29,7 +26,6 @@ using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Osu.Utils;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
-using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics;
@@ -67,11 +63,6 @@ namespace osu.Game.Rulesets.Osu.Mods
///
private const float distance_cap = 380f;
- // The distances from the hit objects to the borders of the playfield they start to "turn around" and curve towards the middle.
- // The closer the hit objects draw to the border, the sharper the turn
- private const byte border_distance_x = 192;
- private const byte border_distance_y = 144;
-
///
/// The extent of rotation towards playfield centre when a circle is near the edge
///
@@ -341,46 +332,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset)
{
- drawableRuleset.Overlays.Add(new TargetBeatContainer(drawableRuleset.Beatmap.HitObjects.First().StartTime));
- }
-
- public class TargetBeatContainer : BeatSyncedContainer
- {
- private readonly double firstHitTime;
-
- private PausableSkinnableSound sample;
-
- public TargetBeatContainer(double firstHitTime)
- {
- this.firstHitTime = firstHitTime;
- AllowMistimedEventFiring = false;
- Divisor = 1;
- }
-
- [BackgroundDependencyLoader]
- private void load()
- {
- InternalChildren = new Drawable[]
- {
- sample = new PausableSkinnableSound(new SampleInfo("Gameplay/catch-banana"))
- };
- }
-
- protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes)
- {
- base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes);
-
- if (!IsBeatSyncedWithTrack) return;
-
- int timeSignature = (int)timingPoint.TimeSignature;
-
- // play metronome from one measure before the first object.
- if (BeatSyncClock.CurrentTime < firstHitTime - timingPoint.BeatLength * timeSignature)
- return;
-
- sample.Frequency.Value = beatIndex % timeSignature == 0 ? 1 : 0.5f;
- sample.Play();
- }
+ drawableRuleset.Overlays.Add(new Metronome(drawableRuleset.Beatmap.HitObjects.First().StartTime));
}
#endregion
diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs
index 21a49f9495..b13cdff1ec 100644
--- a/osu.Game.Rulesets.Osu/OsuRuleset.cs
+++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs
@@ -166,6 +166,7 @@ namespace osu.Game.Rulesets.Osu
new OsuModDifficultyAdjust(),
new OsuModClassic(),
new OsuModRandom(),
+ new OsuModMirror(),
};
case ModType.Automation:
diff --git a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs
index 06b964a647..57ec51cf64 100644
--- a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs
+++ b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs
@@ -2,7 +2,11 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Linq;
+using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Game.Rulesets.Osu.UI;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Osu.Objects;
using osuTK;
namespace osu.Game.Rulesets.Osu.Utils
@@ -100,5 +104,47 @@ namespace osu.Game.Rulesets.Osu.Utils
initial.Length * MathF.Sin(finalAngleRad)
);
}
+
+ ///
+ /// Reflects the position of the in the playfield horizontally.
+ ///
+ /// The object to reflect.
+ public static void ReflectHorizontally(OsuHitObject osuObject)
+ {
+ osuObject.Position = new Vector2(OsuPlayfield.BASE_SIZE.X - osuObject.X, osuObject.Position.Y);
+
+ if (!(osuObject is Slider slider))
+ return;
+
+ slider.NestedHitObjects.OfType().ForEach(h => h.Position = new Vector2(OsuPlayfield.BASE_SIZE.X - h.Position.X, h.Position.Y));
+ slider.NestedHitObjects.OfType().ForEach(h => h.Position = new Vector2(OsuPlayfield.BASE_SIZE.X - h.Position.X, h.Position.Y));
+
+ var controlPoints = slider.Path.ControlPoints.Select(p => new PathControlPoint(p.Position.Value, p.Type.Value)).ToArray();
+ foreach (var point in controlPoints)
+ point.Position.Value = new Vector2(-point.Position.Value.X, point.Position.Value.Y);
+
+ slider.Path = new SliderPath(controlPoints, slider.Path.ExpectedDistance.Value);
+ }
+
+ ///
+ /// Reflects the position of the in the playfield vertically.
+ ///
+ /// The object to reflect.
+ public static void ReflectVertically(OsuHitObject osuObject)
+ {
+ osuObject.Position = new Vector2(osuObject.Position.X, OsuPlayfield.BASE_SIZE.Y - osuObject.Y);
+
+ if (!(osuObject is Slider slider))
+ return;
+
+ slider.NestedHitObjects.OfType().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y));
+ slider.NestedHitObjects.OfType().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y));
+
+ var controlPoints = slider.Path.ControlPoints.Select(p => new PathControlPoint(p.Position.Value, p.Type.Value)).ToArray();
+ foreach (var point in controlPoints)
+ point.Position.Value = new Vector2(point.Position.Value.X, -point.Position.Value.Y);
+
+ slider.Path = new SliderPath(controlPoints, slider.Path.ExpectedDistance.Value);
+ }
}
}
diff --git a/osu.Game/Online/API/APIRequest.cs b/osu.Game/Online/API/APIRequest.cs
index e6bfca166e..df0901fd93 100644
--- a/osu.Game/Online/API/APIRequest.cs
+++ b/osu.Game/Online/API/APIRequest.cs
@@ -171,19 +171,24 @@ namespace osu.Game.Online.API
WebRequest?.Abort();
- string responseString = WebRequest?.GetResponseString();
-
- if (!string.IsNullOrEmpty(responseString))
+ // in the case of a cancellation we don't care about whether there's an error in the response.
+ if (!(e is OperationCanceledException))
{
- try
- {
- // attempt to decode a displayable error string.
- var error = JsonConvert.DeserializeObject(responseString);
- if (error != null)
- e = new APIException(error.ErrorMessage, e);
- }
- catch
+ string responseString = WebRequest?.GetResponseString();
+
+ // naive check whether there's an error in the response to avoid unnecessary JSON deserialisation.
+ if (!string.IsNullOrEmpty(responseString) && responseString.Contains(@"""error"""))
{
+ try
+ {
+ // attempt to decode a displayable error string.
+ var error = JsonConvert.DeserializeObject(responseString);
+ if (error != null)
+ e = new APIException(error.ErrorMessage, e);
+ }
+ catch
+ {
+ }
}
}
diff --git a/osu.Game/Online/HubClientConnector.cs b/osu.Game/Online/HubClientConnector.cs
index 3839762e46..90049a6501 100644
--- a/osu.Game/Online/HubClientConnector.cs
+++ b/osu.Game/Online/HubClientConnector.cs
@@ -116,10 +116,7 @@ namespace osu.Game.Online
}
catch (Exception e)
{
- Logger.Log($"{clientName} connection error: {e}", LoggingTarget.Network);
-
- // retry on any failure.
- await Task.Delay(5000, cancellationToken).ConfigureAwait(false);
+ await handleErrorAndDelay(e, cancellationToken).ConfigureAwait(false);
}
}
}
@@ -129,6 +126,15 @@ namespace osu.Game.Online
}
}
+ ///
+ /// Handles an exception and delays an async flow.
+ ///
+ private async Task handleErrorAndDelay(Exception exception, CancellationToken cancellationToken)
+ {
+ Logger.Log($"{clientName} connection error: {exception}", LoggingTarget.Network);
+ await Task.Delay(5000, cancellationToken).ConfigureAwait(false);
+ }
+
private HubConnection buildConnection(CancellationToken cancellationToken)
{
var builder = new HubConnectionBuilder()
@@ -155,17 +161,18 @@ namespace osu.Game.Online
return newConnection;
}
- private Task onConnectionClosed(Exception? ex, CancellationToken cancellationToken)
+ private async Task onConnectionClosed(Exception? ex, CancellationToken cancellationToken)
{
isConnected.Value = false;
- Logger.Log(ex != null ? $"{clientName} lost connection: {ex}" : $"{clientName} disconnected", LoggingTarget.Network);
+ if (ex != null)
+ await handleErrorAndDelay(ex, cancellationToken).ConfigureAwait(false);
+ else
+ Logger.Log($"{clientName} disconnected", LoggingTarget.Network);
// make sure a disconnect wasn't triggered (and this is still the active connection).
if (!cancellationToken.IsCancellationRequested)
- Task.Run(connect, default);
-
- return Task.CompletedTask;
+ await Task.Run(connect, default).ConfigureAwait(false);
}
private async Task disconnect(bool takeLock)
diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs
index ddd1dfa6cd..fee0e62315 100644
--- a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs
+++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs
@@ -18,6 +18,8 @@ using osu.Game.Scoring;
using osu.Game.Users.Drawables;
using osuTK;
using osuTK.Graphics;
+using osu.Framework.Localisation;
+using osu.Framework.Extensions.LocalisationExtensions;
namespace osu.Game.Overlays.BeatmapSet.Scores
{
@@ -211,11 +213,11 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
return content.ToArray();
}
- protected override Drawable CreateHeader(int index, TableColumn column) => new HeaderText(column?.Header ?? string.Empty);
+ protected override Drawable CreateHeader(int index, TableColumn column) => new HeaderText(column?.Header ?? default);
private class HeaderText : OsuSpriteText
{
- public HeaderText(string text)
+ public HeaderText(LocalisableString text)
{
Text = text.ToUpper();
Font = OsuFont.GetFont(size: 10, weight: FontWeight.Bold);
diff --git a/osu.Game/Overlays/Rankings/Tables/RankingsTable.cs b/osu.Game/Overlays/Rankings/Tables/RankingsTable.cs
index 585b5c22aa..f568aae59c 100644
--- a/osu.Game/Overlays/Rankings/Tables/RankingsTable.cs
+++ b/osu.Game/Overlays/Rankings/Tables/RankingsTable.cs
@@ -13,6 +13,7 @@ using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Game.Users;
using osu.Game.Users.Drawables;
using osuTK;
+using osu.Framework.Localisation;
namespace osu.Game.Overlays.Rankings.Tables
{
@@ -74,7 +75,7 @@ namespace osu.Game.Overlays.Rankings.Tables
protected override Drawable CreateHeader(int index, TableColumn column)
{
- var title = column?.Header ?? string.Empty;
+ var title = column?.Header ?? default;
return new HeaderText(title, title == HighlightedColumn);
}
@@ -109,7 +110,7 @@ namespace osu.Game.Overlays.Rankings.Tables
{
private readonly bool isHighlighted;
- public HeaderText(string text, bool isHighlighted)
+ public HeaderText(LocalisableString text, bool isHighlighted)
{
this.isHighlighted = isHighlighted;
diff --git a/osu.Game/Overlays/Rankings/Tables/UserBasedTable.cs b/osu.Game/Overlays/Rankings/Tables/UserBasedTable.cs
index a6969f483f..56814244c0 100644
--- a/osu.Game/Overlays/Rankings/Tables/UserBasedTable.cs
+++ b/osu.Game/Overlays/Rankings/Tables/UserBasedTable.cs
@@ -9,6 +9,7 @@ using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Users;
using osu.Game.Scoring;
+using osu.Framework.Localisation;
namespace osu.Game.Overlays.Rankings.Tables
{
@@ -31,8 +32,8 @@ namespace osu.Game.Overlays.Rankings.Tables
protected override Drawable CreateHeader(int index, TableColumn column)
{
- var title = column?.Header ?? string.Empty;
- return new UserTableHeaderText(title, HighlightedColumn == title, GradeColumns.Contains(title));
+ var title = column?.Header ?? default;
+ return new UserTableHeaderText(title, HighlightedColumn == title, GradeColumns.Contains(title.ToString()));
}
protected sealed override Country GetCountry(UserStatistics item) => item.User.Country;
@@ -66,7 +67,7 @@ namespace osu.Game.Overlays.Rankings.Tables
private class UserTableHeaderText : HeaderText
{
- public UserTableHeaderText(string text, bool isHighlighted, bool isGrade)
+ public UserTableHeaderText(LocalisableString text, bool isHighlighted, bool isGrade)
: base(text, isHighlighted)
{
Margin = new MarginPadding
diff --git a/osu.Game/Rulesets/Mods/Metronome.cs b/osu.Game/Rulesets/Mods/Metronome.cs
new file mode 100644
index 0000000000..ee0a42b4bc
--- /dev/null
+++ b/osu.Game/Rulesets/Mods/Metronome.cs
@@ -0,0 +1,53 @@
+// 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.Audio.Track;
+using osu.Framework.Graphics;
+using osu.Game.Audio;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Graphics.Containers;
+using osu.Game.Skinning;
+
+namespace osu.Game.Rulesets.Mods
+{
+ public class Metronome : BeatSyncedContainer
+ {
+ private readonly double firstHitTime;
+
+ private PausableSkinnableSound sample;
+
+ /// Start time of the first hit object, used for providing a count down.
+ public Metronome(double firstHitTime)
+ {
+ this.firstHitTime = firstHitTime;
+ AllowMistimedEventFiring = false;
+ Divisor = 1;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ InternalChildren = new Drawable[]
+ {
+ sample = new PausableSkinnableSound(new SampleInfo("Gameplay/catch-banana"))
+ };
+ }
+
+ protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes)
+ {
+ base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes);
+
+ if (!IsBeatSyncedWithTrack) return;
+
+ int timeSignature = (int)timingPoint.TimeSignature;
+
+ // play metronome from one measure before the first object.
+ if (BeatSyncClock.CurrentTime < firstHitTime - timingPoint.BeatLength * timeSignature)
+ return;
+
+ sample.Frequency.Value = beatIndex % timeSignature == 0 ? 1 : 0.5f;
+ sample.Play();
+ }
+ }
+}
diff --git a/osu.Game/Rulesets/Mods/ModMirror.cs b/osu.Game/Rulesets/Mods/ModMirror.cs
new file mode 100644
index 0000000000..3c4b7d0c60
--- /dev/null
+++ b/osu.Game/Rulesets/Mods/ModMirror.cs
@@ -0,0 +1,13 @@
+// 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.Mods
+{
+ public abstract class ModMirror : Mod
+ {
+ public override string Name => "Mirror";
+ public override string Acronym => "MR";
+ public override ModType Type => ModType.Conversion;
+ public override double ScoreMultiplier => 1;
+ }
+}
diff --git a/osu.Game/Screens/Edit/EditorTable.cs b/osu.Game/Screens/Edit/EditorTable.cs
index 9578b96897..756339405c 100644
--- a/osu.Game/Screens/Edit/EditorTable.cs
+++ b/osu.Game/Screens/Edit/EditorTable.cs
@@ -2,10 +2,12 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
+using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
+using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
@@ -42,11 +44,11 @@ namespace osu.Game.Screens.Edit
});
}
- protected override Drawable CreateHeader(int index, TableColumn column) => new HeaderText(column?.Header ?? string.Empty);
+ protected override Drawable CreateHeader(int index, TableColumn column) => new HeaderText(column?.Header ?? default);
private class HeaderText : OsuSpriteText
{
- public HeaderText(string text)
+ public HeaderText(LocalisableString text)
{
Text = text.ToUpper();
Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold);
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index 4baaf89c67..2efcfeb278 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -36,7 +36,7 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
diff --git a/osu.iOS.props b/osu.iOS.props
index 66cae06713..2630614c03 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -70,7 +70,7 @@
-
+
@@ -93,7 +93,7 @@
-
+