diff --git a/osu.Android.props b/osu.Android.props
index 7a0a542ee9..8de516240f 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -51,8 +51,8 @@
-
-
+
+
diff --git a/osu.Game.Rulesets.Catch.Tests/Mods/CatchModMirrorTest.cs b/osu.Game.Rulesets.Catch.Tests/Mods/CatchModMirrorTest.cs
new file mode 100644
index 0000000000..fbbfee6b60
--- /dev/null
+++ b/osu.Game.Rulesets.Catch.Tests/Mods/CatchModMirrorTest.cs
@@ -0,0 +1,120 @@
+// 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 NUnit.Framework;
+using osu.Framework.Utils;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Rulesets.Catch.Beatmaps;
+using osu.Game.Rulesets.Catch.Mods;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Catch.UI;
+using osu.Game.Rulesets.Objects;
+using osuTK;
+
+namespace osu.Game.Rulesets.Catch.Tests.Mods
+{
+ [TestFixture]
+ public class CatchModMirrorTest
+ {
+ [Test]
+ public void TestModMirror()
+ {
+ IBeatmap original = createBeatmap(false);
+ IBeatmap mirrored = createBeatmap(true);
+
+ assertEffectivePositionsMirrored(original, mirrored);
+ }
+
+ private static IBeatmap createBeatmap(bool withMirrorMod)
+ {
+ var beatmap = createRawBeatmap();
+ var mirrorMod = new CatchModMirror();
+
+ var beatmapProcessor = new CatchBeatmapProcessor(beatmap);
+ beatmapProcessor.PreProcess();
+
+ foreach (var hitObject in beatmap.HitObjects)
+ hitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
+
+ beatmapProcessor.PostProcess();
+
+ if (withMirrorMod)
+ mirrorMod.ApplyToBeatmap(beatmap);
+
+ return beatmap;
+ }
+
+ private static IBeatmap createRawBeatmap() => new Beatmap
+ {
+ HitObjects = new List
+ {
+ new Fruit
+ {
+ OriginalX = 150,
+ StartTime = 0
+ },
+ new Fruit
+ {
+ OriginalX = 450,
+ StartTime = 500
+ },
+ new JuiceStream
+ {
+ OriginalX = 250,
+ Path = new SliderPath
+ {
+ ControlPoints =
+ {
+ new PathControlPoint(new Vector2(-100, 1)),
+ new PathControlPoint(new Vector2(0, 2)),
+ new PathControlPoint(new Vector2(100, 3)),
+ new PathControlPoint(new Vector2(0, 4))
+ }
+ },
+ StartTime = 1000,
+ },
+ new BananaShower
+ {
+ StartTime = 5000,
+ Duration = 5000
+ }
+ }
+ };
+
+ private static void assertEffectivePositionsMirrored(IBeatmap original, IBeatmap mirrored)
+ {
+ if (original.HitObjects.Count != mirrored.HitObjects.Count)
+ Assert.Fail($"Top-level object count mismatch (original: {original.HitObjects.Count}, mirrored: {mirrored.HitObjects.Count})");
+
+ for (int i = 0; i < original.HitObjects.Count; ++i)
+ {
+ var originalObject = (CatchHitObject)original.HitObjects[i];
+ var mirroredObject = (CatchHitObject)mirrored.HitObjects[i];
+
+ // banana showers themselves are exempt, as we only really care about their nested bananas' positions.
+ if (!effectivePositionMirrored(originalObject, mirroredObject) && !(originalObject is BananaShower))
+ Assert.Fail($"{originalObject.GetType().Name} at time {originalObject.StartTime} is not mirrored ({printEffectivePositions(originalObject, mirroredObject)})");
+
+ if (originalObject.NestedHitObjects.Count != mirroredObject.NestedHitObjects.Count)
+ Assert.Fail($"{originalObject.GetType().Name} nested object count mismatch (original: {originalObject.NestedHitObjects.Count}, mirrored: {mirroredObject.NestedHitObjects.Count})");
+
+ for (int j = 0; j < originalObject.NestedHitObjects.Count; ++j)
+ {
+ var originalNested = (CatchHitObject)originalObject.NestedHitObjects[j];
+ var mirroredNested = (CatchHitObject)mirroredObject.NestedHitObjects[j];
+
+ if (!effectivePositionMirrored(originalNested, mirroredNested))
+ Assert.Fail($"{originalObject.GetType().Name}'s nested {originalNested.GetType().Name} at time {originalObject.StartTime} is not mirrored ({printEffectivePositions(originalNested, mirroredNested)})");
+ }
+ }
+ }
+
+ private static string printEffectivePositions(CatchHitObject original, CatchHitObject mirrored)
+ => $"original X: {original.EffectiveX}, mirrored X is: {mirrored.EffectiveX}, mirrored X should be: {CatchPlayfield.WIDTH - original.EffectiveX}";
+
+ private static bool effectivePositionMirrored(CatchHitObject original, CatchHitObject mirrored)
+ => Precision.AlmostEquals(original.EffectiveX, CatchPlayfield.WIDTH - mirrored.EffectiveX);
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs
index eafa1b9b9d..9fee6b2bc1 100644
--- a/osu.Game.Rulesets.Catch/CatchRuleset.cs
+++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs
@@ -117,6 +117,7 @@ namespace osu.Game.Rulesets.Catch
{
new CatchModDifficultyAdjust(),
new CatchModClassic(),
+ new CatchModMirror(),
};
case ModType.Automation:
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModMirror.cs b/osu.Game.Rulesets.Catch/Mods/CatchModMirror.cs
new file mode 100644
index 0000000000..932c8cad85
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModMirror.cs
@@ -0,0 +1,87 @@
+// 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.Game.Beatmaps;
+using osu.Game.Rulesets.Catch.Beatmaps;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Catch.UI;
+using osu.Game.Rulesets.Objects;
+using osuTK;
+
+namespace osu.Game.Rulesets.Catch.Mods
+{
+ public class CatchModMirror : ModMirror, IApplicableToBeatmap
+ {
+ public override string Description => "Fruits are flipped horizontally.";
+
+ ///
+ /// is used instead of ,
+ /// as applies offsets in .
+ /// runs after post-processing, while runs before it.
+ ///
+ public void ApplyToBeatmap(IBeatmap beatmap)
+ {
+ foreach (var hitObject in beatmap.HitObjects)
+ applyToHitObject(hitObject);
+ }
+
+ private void applyToHitObject(HitObject hitObject)
+ {
+ var catchObject = (CatchHitObject)hitObject;
+
+ switch (catchObject)
+ {
+ case Fruit fruit:
+ mirrorEffectiveX(fruit);
+ break;
+
+ case JuiceStream juiceStream:
+ mirrorEffectiveX(juiceStream);
+ mirrorJuiceStreamPath(juiceStream);
+ break;
+
+ case BananaShower bananaShower:
+ mirrorBananaShower(bananaShower);
+ break;
+ }
+ }
+
+ ///
+ /// Mirrors the effective X position of and its nested hit objects.
+ ///
+ private static void mirrorEffectiveX(CatchHitObject catchObject)
+ {
+ catchObject.OriginalX = CatchPlayfield.WIDTH - catchObject.OriginalX;
+ catchObject.XOffset = -catchObject.XOffset;
+
+ foreach (var nested in catchObject.NestedHitObjects.Cast())
+ {
+ nested.OriginalX = CatchPlayfield.WIDTH - nested.OriginalX;
+ nested.XOffset = -nested.XOffset;
+ }
+ }
+
+ ///
+ /// Mirrors the path of the .
+ ///
+ private static void mirrorJuiceStreamPath(JuiceStream juiceStream)
+ {
+ var controlPoints = juiceStream.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);
+
+ juiceStream.Path = new SliderPath(controlPoints, juiceStream.Path.ExpectedDistance.Value);
+ }
+
+ ///
+ /// Mirrors X positions of all bananas in the .
+ ///
+ private static void mirrorBananaShower(BananaShower bananaShower)
+ {
+ foreach (var banana in bananaShower.NestedHitObjects.OfType())
+ banana.XOffset = CatchPlayfield.WIDTH - banana.XOffset;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySliderBody.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySliderBody.cs
index 744ded37c9..1c8dfeac52 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySliderBody.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySliderBody.cs
@@ -3,9 +3,9 @@
using System;
using osu.Framework.Extensions.Color4Extensions;
-using osu.Framework.Utils;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Skinning.Default;
+using osu.Game.Utils;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Skinning.Legacy
@@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
// Roughly matches osu!stable's slider border portions.
=> base.CalculatedBorderPortion * 0.77f;
- public new Color4 AccentColour => new Color4(base.AccentColour.R, base.AccentColour.G, base.AccentColour.B, base.AccentColour.A * 0.70f);
+ public new Color4 AccentColour => new Color4(base.AccentColour.R, base.AccentColour.G, base.AccentColour.B, 0.7f);
protected override Color4 ColourAt(float position)
{
@@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
Color4 outerColour = AccentColour.Darken(0.1f);
Color4 innerColour = lighten(AccentColour, 0.5f);
- return Interpolation.ValueAt(position / realGradientPortion, outerColour, innerColour, 0, 1);
+ return LegacyUtils.InterpolateNonLinear(position / realGradientPortion, outerColour, innerColour, 0, 1);
}
///
diff --git a/osu.Game.Tests/Gameplay/TestSceneDrawableHitObject.cs b/osu.Game.Tests/Gameplay/TestSceneDrawableHitObject.cs
index 0ce71696bd..58f4c4c8db 100644
--- a/osu.Game.Tests/Gameplay/TestSceneDrawableHitObject.cs
+++ b/osu.Game.Tests/Gameplay/TestSceneDrawableHitObject.cs
@@ -2,11 +2,13 @@
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
+using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Scoring;
using osu.Game.Tests.Visual;
namespace osu.Game.Tests.Gameplay
@@ -121,6 +123,18 @@ namespace osu.Game.Tests.Gameplay
AddAssert("Drawable lifetime is restored", () => dho.LifetimeStart == 666 && dho.LifetimeEnd == 999);
}
+ [Test]
+ public void TestStateChangeBeforeLoadComplete()
+ {
+ TestDrawableHitObject dho = null;
+ AddStep("Add DHO and apply result", () =>
+ {
+ Child = dho = new TestDrawableHitObject(new HitObject { StartTime = Time.Current });
+ dho.MissForcefully();
+ });
+ AddAssert("DHO state is correct", () => dho.State.Value == ArmedState.Miss);
+ }
+
private class TestDrawableHitObject : DrawableHitObject
{
public const double INITIAL_LIFETIME_OFFSET = 100;
@@ -141,6 +155,19 @@ namespace osu.Game.Tests.Gameplay
if (SetLifetimeStartOnApply)
LifetimeStart = LIFETIME_ON_APPLY;
}
+
+ public void MissForcefully() => ApplyResult(r => r.Type = HitResult.Miss);
+
+ protected override void UpdateHitStateTransforms(ArmedState state)
+ {
+ if (state != ArmedState.Miss)
+ {
+ base.UpdateHitStateTransforms(state);
+ return;
+ }
+
+ this.FadeOut(1000);
+ }
}
private class TestLifetimeEntry : HitObjectLifetimeEntry
diff --git a/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs b/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs
index 0983b806e2..07ec86b0e7 100644
--- a/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs
+++ b/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs
@@ -24,6 +24,8 @@ namespace osu.Game.Tests.NonVisual.Multiplayer
AddRepeatStep("add some users", () => Client.AddUser(new User { Id = id++ }), 5);
checkPlayingUserCount(0);
+ AddAssert("playlist item is available", () => Client.CurrentMatchPlayingItem.Value != null);
+
changeState(3, MultiplayerUserState.WaitingForLoad);
checkPlayingUserCount(3);
@@ -41,6 +43,8 @@ namespace osu.Game.Tests.NonVisual.Multiplayer
AddStep("leave room", () => Client.LeaveRoom());
checkPlayingUserCount(0);
+
+ AddAssert("playlist item is null", () => Client.CurrentMatchPlayingItem.Value == null);
}
[Test]
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs
index dfb78a235b..93bdbb79f4 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs
@@ -14,7 +14,7 @@ using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Graphics.Containers;
using osu.Game.Online.Rooms;
-using osu.Game.Overlays;
+using osu.Game.Overlays.BeatmapListing.Panels;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
@@ -215,7 +215,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
assertDownloadButtonVisible(false);
void assertDownloadButtonVisible(bool visible) => AddUntilStep($"download button {(visible ? "shown" : "hidden")}",
- () => playlist.ChildrenOfType().Single().Alpha == (visible ? 1 : 0));
+ () => playlist.ChildrenOfType().Single().Alpha == (visible ? 1 : 0));
}
[Test]
@@ -229,7 +229,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
createPlaylist(byOnlineId, byChecksum);
- AddAssert("download buttons shown", () => playlist.ChildrenOfType().All(d => d.IsPresent));
+ AddAssert("download buttons shown", () => playlist.ChildrenOfType().All(d => d.IsPresent));
}
[Test]
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs
index 072e32370d..e9fae32335 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs
@@ -12,6 +12,7 @@ using osu.Game.Rulesets.UI;
using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate;
using osu.Game.Screens.Play;
using osu.Game.Tests.Beatmaps.IO;
+using osu.Game.Users;
namespace osu.Game.Tests.Visual.Multiplayer
{
@@ -47,8 +48,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
AddStep("start players silently", () =>
{
- Client.CurrentMatchPlayingUserIds.Add(PLAYER_1_ID);
- Client.CurrentMatchPlayingUserIds.Add(PLAYER_2_ID);
+ OnlinePlayDependencies.Client.AddUser(new User { Id = PLAYER_1_ID }, true);
+ OnlinePlayDependencies.Client.AddUser(new User { Id = PLAYER_2_ID }, true);
+
playingUserIds.Add(PLAYER_1_ID);
playingUserIds.Add(PLAYER_2_ID);
});
@@ -264,7 +266,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
foreach (int id in userIds)
{
- Client.CurrentMatchPlayingUserIds.Add(id);
+ OnlinePlayDependencies.Client.AddUser(new User { Id = id }, true);
+
SpectatorClient.StartPlay(id, beatmapId ?? importedBeatmapId);
playingUserIds.Add(id);
}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs
index 0e368b59dd..8121492a0b 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs
@@ -20,6 +20,7 @@ using osu.Game.Scoring;
using osu.Game.Screens.Play.HUD;
using osu.Game.Tests.Visual.OnlinePlay;
using osu.Game.Tests.Visual.Spectator;
+using osu.Game.Users;
namespace osu.Game.Tests.Visual.Multiplayer
{
@@ -53,10 +54,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
var playable = Beatmap.Value.GetPlayableBeatmap(Ruleset.Value);
foreach (var user in users)
+ {
SpectatorClient.StartPlay(user, Beatmap.Value.BeatmapInfo.OnlineBeatmapID ?? 0);
-
- // Todo: This is REALLY bad.
- Client.CurrentMatchPlayingUserIds.AddRange(users);
+ OnlinePlayDependencies.Client.AddUser(new User { Id = user }, true);
+ }
Children = new Drawable[]
{
diff --git a/osu.Game.Tests/Visual/TestSceneOsuGame.cs b/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs
similarity index 98%
rename from osu.Game.Tests/Visual/TestSceneOsuGame.cs
rename to osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs
index c52d846a68..26641214b1 100644
--- a/osu.Game.Tests/Visual/TestSceneOsuGame.cs
+++ b/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs
@@ -10,7 +10,6 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Textures;
using osu.Framework.Platform;
-using osu.Framework.Testing;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
@@ -30,10 +29,9 @@ using osu.Game.Skinning;
using osu.Game.Utils;
using osuTK.Graphics;
-namespace osu.Game.Tests.Visual
+namespace osu.Game.Tests.Visual.Navigation
{
[TestFixture]
- [HeadlessTest]
public class TestSceneOsuGame : OsuTestScene
{
private IReadOnlyList requiredGameDependencies => new[]
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneContextMenu.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneContextMenu.cs
index 53693d1b70..3b43f8485a 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneContextMenu.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneContextMenu.cs
@@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Collections.Generic;
+using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@@ -68,13 +70,40 @@ namespace osu.Game.Tests.Visual.UserInterface
);
}
- private class MyContextMenuContainer : Container, IHasContextMenu
+ private static MenuItem[] makeMenu()
{
- public MenuItem[] ContextMenuItems => new MenuItem[]
+ return new MenuItem[]
{
new OsuMenuItem(@"Some option"),
new OsuMenuItem(@"Highlighted option", MenuItemType.Highlighted),
new OsuMenuItem(@"Another option"),
+ new OsuMenuItem(@"Nested option >")
+ {
+ Items = new MenuItem[]
+ {
+ new OsuMenuItem(@"Sub-One"),
+ new OsuMenuItem(@"Sub-Two"),
+ new OsuMenuItem(@"Sub-Three"),
+ new OsuMenuItem(@"Sub-Nested option >")
+ {
+ Items = new MenuItem[]
+ {
+ new OsuMenuItem(@"Double Sub-One"),
+ new OsuMenuItem(@"Double Sub-Two"),
+ new OsuMenuItem(@"Double Sub-Three"),
+ new OsuMenuItem(@"Sub-Sub-Nested option >")
+ {
+ Items = new MenuItem[]
+ {
+ new OsuMenuItem(@"Too Deep One"),
+ new OsuMenuItem(@"Too Deep Two"),
+ new OsuMenuItem(@"Too Deep Three"),
+ }
+ }
+ }
+ }
+ }
+ },
new OsuMenuItem(@"Choose me please"),
new OsuMenuItem(@"And me too"),
new OsuMenuItem(@"Trying to fill"),
@@ -82,17 +111,29 @@ namespace osu.Game.Tests.Visual.UserInterface
};
}
+ private class MyContextMenuContainer : Container, IHasContextMenu
+ {
+ public MenuItem[] ContextMenuItems => makeMenu();
+ }
+
private class AnotherContextMenuContainer : Container, IHasContextMenu
{
- public MenuItem[] ContextMenuItems => new MenuItem[]
+ public MenuItem[] ContextMenuItems
{
- new OsuMenuItem(@"Simple option"),
- new OsuMenuItem(@"Simple very very long option"),
- new OsuMenuItem(@"Change width", MenuItemType.Highlighted, () => this.ResizeWidthTo(Width * 2, 100, Easing.OutQuint)),
- new OsuMenuItem(@"Change height", MenuItemType.Highlighted, () => this.ResizeHeightTo(Height * 2, 100, Easing.OutQuint)),
- new OsuMenuItem(@"Change width back", MenuItemType.Destructive, () => this.ResizeWidthTo(Width / 2, 100, Easing.OutQuint)),
- new OsuMenuItem(@"Change height back", MenuItemType.Destructive, () => this.ResizeHeightTo(Height / 2, 100, Easing.OutQuint)),
- };
+ get
+ {
+ List
-
-
+
+
@@ -93,7 +93,7 @@
-
+