diff --git a/osu.Android.props b/osu.Android.props
index 2902c74f0a..f89994cd56 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -51,7 +51,7 @@
-
+
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs b/osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs
index 2d92c925d7..d576ea3df8 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs
@@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Catch.Mods
public override double ScoreMultiplier => 1.12;
[SettingSource("Flashlight size", "Multiplier applied to the default flashlight size.")]
- public override BindableNumber SizeMultiplier { get; } = new BindableNumber
+ public override BindableFloat SizeMultiplier { get; } = new BindableFloat
{
MinValue = 0.5f,
MaxValue = 1.5f,
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs
index 1ee4ea12e3..8ef5bfd94c 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs
@@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Mania.Mods
public override Type[] IncompatibleMods => new[] { typeof(ModHidden) };
[SettingSource("Flashlight size", "Multiplier applied to the default flashlight size.")]
- public override BindableNumber SizeMultiplier { get; } = new BindableNumber
+ public override BindableFloat SizeMultiplier { get; } = new BindableFloat
{
MinValue = 0.5f,
MaxValue = 3f,
diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAimAssist.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAimAssist.cs
new file mode 100644
index 0000000000..b8310bc4e7
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAimAssist.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 NUnit.Framework;
+using osu.Game.Rulesets.Osu.Mods;
+
+namespace osu.Game.Rulesets.Osu.Tests.Mods
+{
+ public class TestSceneOsuModAimAssist : OsuModTestScene
+ {
+ [TestCase(0.1f)]
+ [TestCase(0.5f)]
+ [TestCase(1)]
+ public void TestAimAssist(float strength)
+ {
+ CreateModTest(new ModTestData
+ {
+ Mod = new OsuModAimAssist
+ {
+ AssistStrength = { Value = strength },
+ },
+ PassCondition = () => true,
+ Autoplay = false,
+ });
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAlternate.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAlternate.cs
new file mode 100644
index 0000000000..de1f61a0bd
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAlternate.cs
@@ -0,0 +1,154 @@
+// 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.Game.Beatmaps;
+using osu.Game.Beatmaps.Timing;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Osu.Mods;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.Replays;
+using osu.Game.Rulesets.Replays;
+using osuTK;
+
+namespace osu.Game.Rulesets.Osu.Tests.Mods
+{
+ public class TestSceneOsuModAlternate : OsuModTestScene
+ {
+ [Test]
+ public void TestInputAtIntro() => CreateModTest(new ModTestData
+ {
+ Mod = new OsuModAlternate(),
+ PassCondition = () => Player.ScoreProcessor.Combo.Value == 1,
+ Autoplay = false,
+ Beatmap = new Beatmap
+ {
+ HitObjects = new List
+ {
+ new HitCircle
+ {
+ StartTime = 1000,
+ Position = new Vector2(100),
+ },
+ },
+ },
+ ReplayFrames = new List
+ {
+ new OsuReplayFrame(500, new Vector2(200), OsuAction.LeftButton),
+ new OsuReplayFrame(501, new Vector2(200)),
+ new OsuReplayFrame(1000, new Vector2(100), OsuAction.LeftButton),
+ }
+ });
+
+ [Test]
+ public void TestInputAlternating() => CreateModTest(new ModTestData
+ {
+ Mod = new OsuModAlternate(),
+ PassCondition = () => Player.ScoreProcessor.Combo.Value == 4,
+ Autoplay = false,
+ Beatmap = new Beatmap
+ {
+ HitObjects = new List
+ {
+ new HitCircle
+ {
+ StartTime = 500,
+ Position = new Vector2(100),
+ },
+ new HitCircle
+ {
+ StartTime = 1000,
+ Position = new Vector2(200, 100),
+ },
+ new HitCircle
+ {
+ StartTime = 1500,
+ Position = new Vector2(300, 100),
+ },
+ new HitCircle
+ {
+ StartTime = 2000,
+ Position = new Vector2(400, 100),
+ },
+ },
+ },
+ ReplayFrames = new List
+ {
+ new OsuReplayFrame(500, new Vector2(100), OsuAction.LeftButton),
+ new OsuReplayFrame(501, new Vector2(100)),
+ new OsuReplayFrame(1000, new Vector2(200, 100), OsuAction.RightButton),
+ new OsuReplayFrame(1001, new Vector2(200, 100)),
+ new OsuReplayFrame(1500, new Vector2(300, 100), OsuAction.LeftButton),
+ new OsuReplayFrame(1501, new Vector2(300, 100)),
+ new OsuReplayFrame(2000, new Vector2(400, 100), OsuAction.RightButton),
+ new OsuReplayFrame(2001, new Vector2(400, 100)),
+ }
+ });
+
+ [Test]
+ public void TestInputSingular() => CreateModTest(new ModTestData
+ {
+ Mod = new OsuModAlternate(),
+ PassCondition = () => Player.ScoreProcessor.Combo.Value == 0 && Player.ScoreProcessor.HighestCombo.Value == 1,
+ Autoplay = false,
+ Beatmap = new Beatmap
+ {
+ HitObjects = new List
+ {
+ new HitCircle
+ {
+ StartTime = 500,
+ Position = new Vector2(100),
+ },
+ new HitCircle
+ {
+ StartTime = 1000,
+ Position = new Vector2(200, 100),
+ },
+ },
+ },
+ ReplayFrames = new List
+ {
+ new OsuReplayFrame(500, new Vector2(100), OsuAction.LeftButton),
+ new OsuReplayFrame(501, new Vector2(100)),
+ new OsuReplayFrame(1000, new Vector2(200, 100), OsuAction.LeftButton),
+ }
+ });
+
+ [Test]
+ public void TestInputSingularWithBreak() => CreateModTest(new ModTestData
+ {
+ Mod = new OsuModAlternate(),
+ PassCondition = () => Player.ScoreProcessor.Combo.Value == 2,
+ Autoplay = false,
+ Beatmap = new Beatmap
+ {
+ Breaks = new List
+ {
+ new BreakPeriod(500, 2250),
+ },
+ HitObjects = new List
+ {
+ new HitCircle
+ {
+ StartTime = 500,
+ Position = new Vector2(100),
+ },
+ new HitCircle
+ {
+ StartTime = 2500,
+ Position = new Vector2(100),
+ }
+ }
+ },
+ ReplayFrames = new List
+ {
+ new OsuReplayFrame(500, new Vector2(100), OsuAction.LeftButton),
+ new OsuReplayFrame(501, new Vector2(100)),
+ new OsuReplayFrame(2500, new Vector2(100), OsuAction.LeftButton),
+ new OsuReplayFrame(2501, new Vector2(100)),
+ }
+ });
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs
new file mode 100644
index 0000000000..ed4b139e00
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs
@@ -0,0 +1,83 @@
+// 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.Sprites;
+using osu.Framework.Utils;
+using osu.Game.Configuration;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
+using osu.Game.Rulesets.Osu.UI;
+using osu.Game.Rulesets.UI;
+using osuTK;
+
+namespace osu.Game.Rulesets.Osu.Mods
+{
+ internal class OsuModAimAssist : Mod, IUpdatableByPlayfield, IApplicableToDrawableRuleset
+ {
+ public override string Name => "Aim Assist";
+ public override string Acronym => "AA";
+ public override IconUsage? Icon => FontAwesome.Solid.MousePointer;
+ public override ModType Type => ModType.Fun;
+ public override string Description => "No need to chase the circle – the circle chases you!";
+ public override double ScoreMultiplier => 1;
+ public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay) };
+
+ private IFrameStableClock gameplayClock;
+
+ [SettingSource("Assist strength", "How much this mod will assist you.", 0)]
+ public BindableFloat AssistStrength { get; } = new BindableFloat(0.5f)
+ {
+ Precision = 0.05f,
+ MinValue = 0.05f,
+ MaxValue = 1.0f,
+ };
+
+ public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset)
+ {
+ gameplayClock = drawableRuleset.FrameStableClock;
+
+ // Hide judgment displays and follow points as they won't make any sense.
+ // Judgements can potentially be turned on in a future where they display at a position relative to their drawable counterpart.
+ drawableRuleset.Playfield.DisplayJudgements.Value = false;
+ (drawableRuleset.Playfield as OsuPlayfield)?.FollowPoints.Hide();
+ }
+
+ public void Update(Playfield playfield)
+ {
+ var cursorPos = playfield.Cursor.ActiveCursor.DrawPosition;
+
+ foreach (var drawable in playfield.HitObjectContainer.AliveObjects)
+ {
+ switch (drawable)
+ {
+ case DrawableHitCircle circle:
+ easeTo(circle, cursorPos);
+ break;
+
+ case DrawableSlider slider:
+
+ if (!slider.HeadCircle.Result.HasResult)
+ easeTo(slider, cursorPos);
+ else
+ easeTo(slider, cursorPos - slider.Ball.DrawPosition);
+
+ break;
+ }
+ }
+ }
+
+ private void easeTo(DrawableHitObject hitObject, Vector2 destination)
+ {
+ double dampLength = Interpolation.Lerp(3000, 40, AssistStrength.Value);
+
+ float x = (float)Interpolation.DampContinuously(hitObject.X, destination.X, dampLength, gameplayClock.ElapsedFrameTime);
+ float y = (float)Interpolation.DampContinuously(hitObject.Y, destination.Y, dampLength, gameplayClock.ElapsedFrameTime);
+
+ hitObject.Position = new Vector2(x, y);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs
new file mode 100644
index 0000000000..46b97dd23b
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs
@@ -0,0 +1,106 @@
+// 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.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Input.Bindings;
+using osu.Framework.Input.Events;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Rulesets.UI;
+using osu.Game.Screens.Play;
+
+namespace osu.Game.Rulesets.Osu.Mods
+{
+ public class OsuModAlternate : Mod, IApplicableToDrawableRuleset, IApplicableToPlayer
+ {
+ public override string Name => @"Alternate";
+ public override string Acronym => @"AL";
+ public override string Description => @"Don't use the same key twice in a row!";
+ public override double ScoreMultiplier => 1.0;
+ public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay) };
+ public override ModType Type => ModType.Conversion;
+ public override IconUsage? Icon => FontAwesome.Solid.Keyboard;
+
+ private double firstObjectValidJudgementTime;
+ private IBindable isBreakTime;
+ private const double flash_duration = 1000;
+ private OsuAction? lastActionPressed;
+ private DrawableRuleset ruleset;
+
+ private IFrameStableClock gameplayClock;
+
+ public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset)
+ {
+ ruleset = drawableRuleset;
+ drawableRuleset.KeyBindingInputManager.Add(new InputInterceptor(this));
+
+ var firstHitObject = ruleset.Objects.FirstOrDefault();
+ firstObjectValidJudgementTime = (firstHitObject?.StartTime ?? 0) - (firstHitObject?.HitWindows.WindowFor(HitResult.Meh) ?? 0);
+
+ gameplayClock = drawableRuleset.FrameStableClock;
+ }
+
+ public void ApplyToPlayer(Player player)
+ {
+ isBreakTime = player.IsBreakTime.GetBoundCopy();
+ isBreakTime.ValueChanged += e =>
+ {
+ if (e.NewValue)
+ lastActionPressed = null;
+ };
+ }
+
+ private bool checkCorrectAction(OsuAction action)
+ {
+ if (isBreakTime.Value)
+ return true;
+
+ if (gameplayClock.CurrentTime < firstObjectValidJudgementTime)
+ return true;
+
+ switch (action)
+ {
+ case OsuAction.LeftButton:
+ case OsuAction.RightButton:
+ break;
+
+ // Any action which is not left or right button should be ignored.
+ default:
+ return true;
+ }
+
+ if (lastActionPressed != action)
+ {
+ // User alternated correctly.
+ lastActionPressed = action;
+ return true;
+ }
+
+ ruleset.Cursor.FlashColour(Colour4.Red, flash_duration, Easing.OutQuint);
+ return false;
+ }
+
+ private class InputInterceptor : Component, IKeyBindingHandler
+ {
+ private readonly OsuModAlternate mod;
+
+ public InputInterceptor(OsuModAlternate mod)
+ {
+ this.mod = mod;
+ }
+
+ public bool OnPressed(KeyBindingPressEvent e)
+ // if the pressed action is incorrect, block it from reaching gameplay.
+ => !mod.checkCorrectAction(e.Action);
+
+ public void OnReleased(KeyBindingReleaseEvent e)
+ {
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs
index aac830801b..983964d639 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs
@@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override ModType Type => ModType.Automation;
public override string Description => @"Automatic cursor movement - just follow the rhythm.";
public override double ScoreMultiplier => 1;
- public override Type[] IncompatibleMods => new[] { typeof(OsuModSpunOut), typeof(ModRelax), typeof(ModFailCondition), typeof(ModNoFail), typeof(ModAutoplay) };
+ public override Type[] IncompatibleMods => new[] { typeof(OsuModSpunOut), typeof(ModRelax), typeof(ModFailCondition), typeof(ModNoFail), typeof(ModAutoplay), typeof(OsuModAimAssist) };
public bool PerformFail() => false;
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs
index 106edfb623..2668013321 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs
@@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModAutoplay : ModAutoplay
{
- public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModAutopilot)).Append(typeof(OsuModSpunOut)).ToArray();
+ public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAimAssist), typeof(OsuModAutopilot), typeof(OsuModSpunOut) }).ToArray();
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score
{
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModCinema.cs b/osu.Game.Rulesets.Osu/Mods/OsuModCinema.cs
index f478790134..ff31cfcd18 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModCinema.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModCinema.cs
@@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModCinema : ModCinema
{
- public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModAutopilot)).Append(typeof(OsuModSpunOut)).ToArray();
+ public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAimAssist), typeof(OsuModAutopilot), typeof(OsuModSpunOut) }).ToArray();
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score
{
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs b/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs
index b4eff57c55..38c84be295 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs
@@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Osu.Mods
};
[SettingSource("Flashlight size", "Multiplier applied to the default flashlight size.")]
- public override BindableNumber SizeMultiplier { get; } = new BindableNumber
+ public override BindableFloat SizeMultiplier { get; } = new BindableFloat
{
MinValue = 0.5f,
MaxValue = 2f,
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs
index 8122ab563e..28c3b069b6 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs
@@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override ModType Type => ModType.Fun;
public override string Description => "Everything rotates. EVERYTHING.";
public override double ScoreMultiplier => 1;
- public override Type[] IncompatibleMods => new[] { typeof(OsuModWiggle) };
+ public override Type[] IncompatibleMods => new[] { typeof(OsuModWiggle), typeof(OsuModAimAssist) };
private float theta;
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs b/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs
index ff6ba6e121..40a05400ea 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs
@@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override ModType Type => ModType.Fun;
public override string Description => "They just won't stay still...";
public override double ScoreMultiplier => 1;
- public override Type[] IncompatibleMods => new[] { typeof(OsuModTransform) };
+ public override Type[] IncompatibleMods => new[] { typeof(OsuModTransform), typeof(OsuModAimAssist) };
private const int wiggle_duration = 90; // (ms) Higher = fewer wiggles
private const int wiggle_strength = 10; // Higher = stronger wiggles
diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs
index 1122a869b7..428e7b9df5 100644
--- a/osu.Game.Rulesets.Osu/OsuRuleset.cs
+++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs
@@ -169,6 +169,7 @@ namespace osu.Game.Rulesets.Osu
new OsuModClassic(),
new OsuModRandom(),
new OsuModMirror(),
+ new OsuModAlternate(),
};
case ModType.Automation:
@@ -193,6 +194,7 @@ namespace osu.Game.Rulesets.Osu
new OsuModApproachDifferent(),
new OsuModMuted(),
new OsuModNoScope(),
+ new OsuModAimAssist(),
};
case ModType.System:
diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
index 2233a547b9..bc1e80cd12 100644
--- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
+++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
@@ -31,7 +31,8 @@ namespace osu.Game.Rulesets.Osu.UI
private readonly ProxyContainer approachCircles;
private readonly ProxyContainer spinnerProxies;
private readonly JudgementContainer judgementLayer;
- private readonly FollowPointRenderer followPoints;
+
+ public FollowPointRenderer FollowPoints { get; }
public static readonly Vector2 BASE_SIZE = new Vector2(512, 384);
@@ -50,7 +51,7 @@ namespace osu.Game.Rulesets.Osu.UI
{
playfieldBorder = new PlayfieldBorder { RelativeSizeAxes = Axes.Both },
spinnerProxies = new ProxyContainer { RelativeSizeAxes = Axes.Both },
- followPoints = new FollowPointRenderer { RelativeSizeAxes = Axes.Both },
+ FollowPoints = new FollowPointRenderer { RelativeSizeAxes = Axes.Both },
judgementLayer = new JudgementContainer { RelativeSizeAxes = Axes.Both },
HitObjectContainer,
judgementAboveHitObjectLayer = new Container { RelativeSizeAxes = Axes.Both },
@@ -131,13 +132,13 @@ namespace osu.Game.Rulesets.Osu.UI
protected override void OnHitObjectAdded(HitObject hitObject)
{
base.OnHitObjectAdded(hitObject);
- followPoints.AddFollowPoints((OsuHitObject)hitObject);
+ FollowPoints.AddFollowPoints((OsuHitObject)hitObject);
}
protected override void OnHitObjectRemoved(HitObject hitObject)
{
base.OnHitObjectRemoved(hitObject);
- followPoints.RemoveFollowPoints((OsuHitObject)hitObject);
+ FollowPoints.RemoveFollowPoints((OsuHitObject)hitObject);
}
private void onNewResult(DrawableHitObject judgedObject, JudgementResult result)
diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs
index fb07c687bb..beec785fe8 100644
--- a/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs
+++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs
@@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
public override double ScoreMultiplier => 1.12;
[SettingSource("Flashlight size", "Multiplier applied to the default flashlight size.")]
- public override BindableNumber SizeMultiplier { get; } = new BindableNumber
+ public override BindableFloat SizeMultiplier { get; } = new BindableFloat
{
MinValue = 0.5f,
MaxValue = 1.5f,
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs
index 9d14d80d07..869fb17317 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs
@@ -15,8 +15,12 @@ using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
+using osu.Game.Rulesets.Taiko;
+using osu.Game.Rulesets.Taiko.Mods;
+using osu.Game.Rulesets.UI;
using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
+using osu.Game.Screens.OnlinePlay.Multiplayer.Participants;
using osu.Game.Tests.Beatmaps;
using osu.Game.Tests.Resources;
@@ -77,6 +81,27 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("wait for join", () => RoomJoined);
}
+ [Test]
+ public void TestTaikoOnlyMod()
+ {
+ AddStep("add playlist item", () =>
+ {
+ SelectedRoom.Value.Playlist.Add(new PlaylistItem
+ {
+ Beatmap = { Value = new TestBeatmap(new TaikoRuleset().RulesetInfo).BeatmapInfo },
+ Ruleset = { Value = new TaikoRuleset().RulesetInfo },
+ AllowedMods = { new TaikoModSwap() }
+ });
+ });
+
+ ClickButtonWhenEnabled();
+
+ AddUntilStep("wait for join", () => RoomJoined);
+
+ AddStep("select swap mod", () => Client.ChangeUserMods(API.LocalUser.Value.OnlineID, new[] { new TaikoModSwap() }));
+ AddUntilStep("participant panel has mod", () => this.ChildrenOfType().Any(p => p.ChildrenOfType().Any(m => m.Mod is TaikoModSwap)));
+ }
+
[Test]
public void TestSettingValidity()
{
diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs
index ed484e03f6..e31377b96e 100644
--- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs
+++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs
@@ -128,6 +128,7 @@ namespace osu.Game.Tests.Visual.Navigation
AddStep("choose clear all scores", () => InputManager.Key(Key.Number4));
+ AddUntilStep("wait for dialog display", () => Game.Dependencies.Get().IsLoaded);
AddUntilStep("wait for dialog", () => Game.Dependencies.Get().CurrentDialog != null);
AddStep("confirm deletion", () => InputManager.Key(Key.Number1));
AddUntilStep("wait for dialog dismissed", () => Game.Dependencies.Get().CurrentDialog == null);
@@ -172,6 +173,7 @@ namespace osu.Game.Tests.Visual.Navigation
InputManager.Click(MouseButton.Left);
});
+ AddUntilStep("wait for dialog display", () => Game.Dependencies.Get().IsLoaded);
AddUntilStep("wait for dialog", () => Game.Dependencies.Get().CurrentDialog != null);
AddStep("confirm deletion", () => InputManager.Key(Key.Number1));
AddUntilStep("wait for dialog dismissed", () => Game.Dependencies.Get().CurrentDialog == null);
diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs
index a23bc620ec..4e46901e08 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs
@@ -601,7 +601,7 @@ namespace osu.Game.Tests.Visual.SongSelect
{
BeatmapSetInfo testMixed = null;
- createCarousel();
+ createCarousel(new List());
AddStep("add mixed ruleset beatmapset", () =>
{
@@ -765,22 +765,22 @@ namespace osu.Game.Tests.Visual.SongSelect
{
bool changed = false;
- createCarousel(c =>
+ if (beatmapSets == null)
+ {
+ beatmapSets = new List();
+
+ for (int i = 1; i <= (count ?? set_count); i++)
+ {
+ beatmapSets.Add(randomDifficulties
+ ? TestResources.CreateTestBeatmapSetInfo()
+ : TestResources.CreateTestBeatmapSetInfo(3));
+ }
+ }
+
+ createCarousel(beatmapSets, c =>
{
carouselAdjust?.Invoke(c);
- if (beatmapSets == null)
- {
- beatmapSets = new List();
-
- for (int i = 1; i <= (count ?? set_count); i++)
- {
- beatmapSets.Add(randomDifficulties
- ? TestResources.CreateTestBeatmapSetInfo()
- : TestResources.CreateTestBeatmapSetInfo(3));
- }
- }
-
carousel.Filter(initialCriteria?.Invoke() ?? new FilterCriteria());
carousel.BeatmapSetsChanged = () => changed = true;
carousel.BeatmapSets = beatmapSets;
@@ -789,7 +789,7 @@ namespace osu.Game.Tests.Visual.SongSelect
AddUntilStep("Wait for load", () => changed);
}
- private void createCarousel(Action carouselAdjust = null, Container target = null)
+ private void createCarousel(List beatmapSets, Action carouselAdjust = null, Container target = null)
{
AddStep("Create carousel", () =>
{
@@ -803,6 +803,8 @@ namespace osu.Game.Tests.Visual.SongSelect
carouselAdjust?.Invoke(carousel);
+ carousel.BeatmapSets = beatmapSets;
+
(target ?? this).Child = carousel;
});
}
diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs
index 48230ff9e9..667fd08084 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs
@@ -197,7 +197,24 @@ namespace osu.Game.Tests.Visual.SongSelect
Accuracy = 1,
MaxCombo = 244,
TotalScore = 1707827,
- //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
+ Mods = new Mod[]
+ {
+ new OsuModHidden(),
+ new OsuModHardRock(),
+ new OsuModFlashlight
+ {
+ FollowDelay = { Value = 200 },
+ SizeMultiplier = { Value = 5 },
+ },
+ new OsuModDifficultyAdjust
+ {
+ CircleSize = { Value = 11 },
+ ApproachRate = { Value = 10 },
+ OverallDifficulty = { Value = 10 },
+ DrainRate = { Value = 10 },
+ ExtendedLimits = { Value = true }
+ }
+ },
Ruleset = new OsuRuleset().RulesetInfo,
BeatmapInfo = beatmapInfo,
User = new APIUser
@@ -217,7 +234,7 @@ namespace osu.Game.Tests.Visual.SongSelect
Accuracy = 1,
MaxCombo = 244,
TotalScore = 1707827,
- //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
+ Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
BeatmapInfo = beatmapInfo,
Ruleset = new OsuRuleset().RulesetInfo,
User = new APIUser
@@ -237,7 +254,7 @@ namespace osu.Game.Tests.Visual.SongSelect
Accuracy = 1,
MaxCombo = 244,
TotalScore = 1707827,
- //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
+ Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
BeatmapInfo = beatmapInfo,
Ruleset = new OsuRuleset().RulesetInfo,
@@ -258,7 +275,7 @@ namespace osu.Game.Tests.Visual.SongSelect
Accuracy = 1,
MaxCombo = 244,
TotalScore = 1707827,
- //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
+ Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
BeatmapInfo = beatmapInfo,
Ruleset = new OsuRuleset().RulesetInfo,
@@ -279,7 +296,7 @@ namespace osu.Game.Tests.Visual.SongSelect
Accuracy = 1,
MaxCombo = 244,
TotalScore = 1707827,
- //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
+ Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
BeatmapInfo = beatmapInfo,
Ruleset = new OsuRuleset().RulesetInfo,
@@ -300,7 +317,7 @@ namespace osu.Game.Tests.Visual.SongSelect
Accuracy = 0.9826,
MaxCombo = 244,
TotalScore = 1707827,
- //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
+ Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
BeatmapInfo = beatmapInfo,
Ruleset = new OsuRuleset().RulesetInfo,
@@ -321,7 +338,7 @@ namespace osu.Game.Tests.Visual.SongSelect
Accuracy = 0.9654,
MaxCombo = 244,
TotalScore = 1707827,
- //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
+ Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
BeatmapInfo = beatmapInfo,
Ruleset = new OsuRuleset().RulesetInfo,
@@ -342,7 +359,7 @@ namespace osu.Game.Tests.Visual.SongSelect
Accuracy = 0.6025,
MaxCombo = 244,
TotalScore = 1707827,
- //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
+ Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
BeatmapInfo = beatmapInfo,
Ruleset = new OsuRuleset().RulesetInfo,
@@ -363,7 +380,7 @@ namespace osu.Game.Tests.Visual.SongSelect
Accuracy = 0.5140,
MaxCombo = 244,
TotalScore = 1707827,
- //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
+ Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
BeatmapInfo = beatmapInfo,
Ruleset = new OsuRuleset().RulesetInfo,
@@ -384,7 +401,7 @@ namespace osu.Game.Tests.Visual.SongSelect
Accuracy = 0.4222,
MaxCombo = 244,
TotalScore = 1707827,
- //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
+ Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
BeatmapInfo = beatmapInfo,
Ruleset = new OsuRuleset().RulesetInfo,
diff --git a/osu.Game/Beatmaps/Drawables/DifficultyIconTooltip.cs b/osu.Game/Beatmaps/Drawables/DifficultyIconTooltip.cs
index ec4bcbd65f..aba01a1294 100644
--- a/osu.Game/Beatmaps/Drawables/DifficultyIconTooltip.cs
+++ b/osu.Game/Beatmaps/Drawables/DifficultyIconTooltip.cs
@@ -30,6 +30,7 @@ namespace osu.Game.Beatmaps.Drawables
{
background = new Box
{
+ Alpha = 0.9f,
RelativeSizeAxes = Axes.Both
},
new FillFlowContainer
diff --git a/osu.Game/Graphics/Cursor/MenuCursor.cs b/osu.Game/Graphics/Cursor/MenuCursor.cs
index 8e272f637f..0cc751ea21 100644
--- a/osu.Game/Graphics/Cursor/MenuCursor.cs
+++ b/osu.Game/Graphics/Cursor/MenuCursor.cs
@@ -10,6 +10,8 @@ using osu.Framework.Graphics.Sprites;
using osu.Game.Configuration;
using System;
using JetBrains.Annotations;
+using osu.Framework.Audio;
+using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Textures;
using osu.Framework.Input.Events;
@@ -30,13 +32,17 @@ namespace osu.Game.Graphics.Cursor
private DragRotationState dragRotationState;
private Vector2 positionMouseDown;
+ private Sample tapSample;
+
[BackgroundDependencyLoader(true)]
- private void load([NotNull] OsuConfigManager config, [CanBeNull] ScreenshotManager screenshotManager)
+ private void load([NotNull] OsuConfigManager config, [CanBeNull] ScreenshotManager screenshotManager, AudioManager audio)
{
cursorRotate = config.GetBindable(OsuSetting.CursorRotation);
if (screenshotManager != null)
screenshotCursorVisibility.BindTo(screenshotManager.CursorVisibility);
+
+ tapSample = audio.Samples.Get(@"UI/cursor-tap");
}
protected override bool OnMouseMove(MouseMoveEvent e)
@@ -87,6 +93,8 @@ namespace osu.Game.Graphics.Cursor
dragRotationState = DragRotationState.DragStarted;
positionMouseDown = e.MousePosition;
}
+
+ playTapSample();
}
return base.OnMouseDown(e);
@@ -104,6 +112,9 @@ namespace osu.Game.Graphics.Cursor
activeCursor.RotateTo(0, 600 * (1 + Math.Abs(activeCursor.Rotation / 720)), Easing.OutElasticHalf);
dragRotationState = DragRotationState.NotDragging;
}
+
+ if (State.Value == Visibility.Visible)
+ playTapSample(0.8);
}
base.OnMouseUp(e);
@@ -121,6 +132,18 @@ namespace osu.Game.Graphics.Cursor
activeCursor.ScaleTo(0.6f, 250, Easing.In);
}
+ private void playTapSample(double baseFrequency = 1f)
+ {
+ const float random_range = 0.02f;
+ SampleChannel channel = tapSample.GetChannel();
+
+ // Scale to [-0.75, 0.75] so that the sample isn't fully panned left or right (sounds weird)
+ channel.Balance.Value = ((activeCursor.X / DrawWidth) * 2 - 1) * 0.75;
+ channel.Frequency.Value = baseFrequency - (random_range / 2f) + RNG.NextDouble(random_range);
+
+ channel.Play();
+ }
+
public class Cursor : Container
{
private Container cursorContainer;
diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs
index 8d91548149..c5302a393c 100644
--- a/osu.Game/Online/API/APIAccess.cs
+++ b/osu.Game/Online/API/APIAccess.cs
@@ -399,7 +399,10 @@ namespace osu.Game.Online.API
lock (queue)
{
if (state.Value == APIState.Offline)
+ {
+ request.Fail(new WebException(@"User not logged in"));
return;
+ }
queue.Enqueue(request);
}
@@ -416,7 +419,7 @@ namespace osu.Game.Online.API
if (failOldRequests)
{
foreach (var req in oldQueueRequests)
- req.Fail(new WebException(@"Disconnected from server"));
+ req.Fail(new WebException($@"Request failed from flush operation (state {state.Value})"));
}
}
}
diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs
index 906e09b8c1..c2393a5de5 100644
--- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs
+++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs
@@ -32,7 +32,7 @@ using osu.Game.Utils;
namespace osu.Game.Online.Leaderboards
{
- public class LeaderboardScore : OsuClickableContainer, IHasContextMenu
+ public class LeaderboardScore : OsuClickableContainer, IHasContextMenu, IHasCustomTooltip
{
public const float HEIGHT = 60;
@@ -70,6 +70,9 @@ namespace osu.Game.Online.Leaderboards
[Resolved]
private Storage storage { get; set; }
+ public ITooltip GetCustomTooltip() => new LeaderboardScoreTooltip();
+ public virtual ScoreInfo TooltipContent => Score;
+
public LeaderboardScore(ScoreInfo score, int? rank, bool isOnlineScope = true)
{
Score = score;
@@ -183,7 +186,6 @@ namespace osu.Game.Online.Leaderboards
Anchor = Anchor.BottomLeft,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
- Spacing = new Vector2(10f, 0f),
Margin = new MarginPadding { Left = edge_margin },
Children = statisticsLabels
},
@@ -228,7 +230,6 @@ namespace osu.Game.Online.Leaderboards
Origin = Anchor.BottomRight,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
- Spacing = new Vector2(1),
ChildrenEnumerable = Score.Mods.Select(mod => new ModIcon(mod) { Scale = new Vector2(0.375f) })
},
},
@@ -313,6 +314,7 @@ namespace osu.Game.Online.Leaderboards
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
+ Padding = new MarginPadding { Right = 10 },
Children = new Drawable[]
{
new Container
diff --git a/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs b/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs
new file mode 100644
index 0000000000..c26e9e6802
--- /dev/null
+++ b/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs
@@ -0,0 +1,219 @@
+// 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.Shapes;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Cursor;
+using osu.Game.Scoring;
+using osuTK;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Graphics;
+using osu.Framework.Allocation;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.UI;
+
+#nullable enable
+
+namespace osu.Game.Online.Leaderboards
+{
+ public class LeaderboardScoreTooltip : VisibilityContainer, ITooltip
+ {
+ private OsuSpriteText timestampLabel = null!;
+ private FillFlowContainer topScoreStatistics = null!;
+ private FillFlowContainer bottomScoreStatistics = null!;
+ private FillFlowContainer modStatistics = null!;
+
+ public LeaderboardScoreTooltip()
+ {
+ AutoSizeAxes = Axes.Both;
+ AutoSizeDuration = 200;
+ AutoSizeEasing = Easing.OutQuint;
+
+ Masking = true;
+ CornerRadius = 5;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colours)
+ {
+ InternalChildren = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Alpha = 0.9f,
+ Colour = colours.Gray3,
+ },
+ new FillFlowContainer
+ {
+ Margin = new MarginPadding(5),
+ Spacing = new Vector2(10),
+ AutoSizeAxes = Axes.Both,
+ Direction = FillDirection.Vertical,
+ Children = new Drawable[]
+ {
+ // Info row
+ timestampLabel = new OsuSpriteText
+ {
+ Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold),
+ },
+ // Mods row
+ modStatistics = new FillFlowContainer
+ {
+ AutoSizeAxes = Axes.Both,
+ Direction = FillDirection.Vertical,
+ Spacing = new Vector2(5, 0),
+ },
+ new FillFlowContainer
+ {
+ AutoSizeAxes = Axes.Both,
+ Direction = FillDirection.Vertical,
+ Children = new Drawable[]
+ {
+ // Actual stats rows
+ topScoreStatistics = new FillFlowContainer
+ {
+ AutoSizeAxes = Axes.Both,
+ Direction = FillDirection.Horizontal,
+ Spacing = new Vector2(10, 0),
+ },
+ bottomScoreStatistics = new FillFlowContainer
+ {
+ AutoSizeAxes = Axes.Both,
+ Direction = FillDirection.Horizontal,
+ Spacing = new Vector2(10, 0),
+ },
+ }
+ },
+ }
+ }
+ };
+ }
+
+ private ScoreInfo? displayedScore;
+
+ public void SetContent(ScoreInfo score)
+ {
+ if (displayedScore?.Equals(score) == true)
+ return;
+
+ displayedScore = score;
+
+ timestampLabel.Text = $"Played on {score.Date.ToLocalTime():d MMMM yyyy HH:mm}";
+
+ modStatistics.Clear();
+ topScoreStatistics.Clear();
+ bottomScoreStatistics.Clear();
+
+ foreach (var mod in score.Mods)
+ {
+ modStatistics.Add(new ModCell(mod));
+ }
+
+ foreach (var result in score.GetStatisticsForDisplay())
+ {
+ if (result.Result > HitResult.Perfect)
+ bottomScoreStatistics.Add(new HitResultCell(result));
+ else
+ topScoreStatistics.Add(new HitResultCell(result));
+ }
+ }
+
+ protected override void PopIn() => this.FadeIn(20, Easing.OutQuint);
+ protected override void PopOut() => this.FadeOut(80, Easing.OutQuint);
+
+ public void Move(Vector2 pos) => Position = pos;
+
+ private class HitResultCell : CompositeDrawable
+ {
+ private readonly string displayName;
+ private readonly HitResult result;
+ private readonly int count;
+
+ public HitResultCell(HitResultDisplayStatistic stat)
+ {
+ AutoSizeAxes = Axes.Both;
+
+ displayName = stat.DisplayName;
+ result = stat.Result;
+ count = stat.Count;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colours)
+ {
+ InternalChild = new FillFlowContainer
+ {
+ Height = 12,
+ AutoSizeAxes = Axes.X,
+ Direction = FillDirection.Horizontal,
+ Spacing = new Vector2(5f, 0f),
+ Children = new Drawable[]
+ {
+ new OsuSpriteText
+ {
+ Font = OsuFont.Torus.With(size: 12, weight: FontWeight.SemiBold),
+ Text = displayName.ToUpperInvariant(),
+ Colour = colours.ForHitResult(result),
+ },
+ new OsuSpriteText
+ {
+ Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold),
+ Text = count.ToString(),
+ },
+ }
+ };
+ }
+ }
+
+ private class ModCell : CompositeDrawable
+ {
+ private readonly Mod mod;
+
+ public ModCell(Mod mod)
+ {
+ AutoSizeAxes = Axes.Both;
+ this.mod = mod;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ FillFlowContainer container;
+ InternalChild = container = new FillFlowContainer
+ {
+ Height = 15,
+ AutoSizeAxes = Axes.X,
+ Direction = FillDirection.Horizontal,
+ Spacing = new Vector2(2f, 0f),
+ Children = new Drawable[]
+ {
+ new ModIcon(mod, showTooltip: false).With(icon =>
+ {
+ icon.Origin = Anchor.CentreLeft;
+ icon.Anchor = Anchor.CentreLeft;
+ icon.Scale = new Vector2(15f / icon.Height);
+ }),
+ }
+ };
+
+ string description = mod.SettingDescription;
+
+ if (!string.IsNullOrEmpty(description))
+ {
+ container.Add(new OsuSpriteText
+ {
+ RelativeSizeAxes = Axes.Y,
+ Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold),
+ Text = mod.SettingDescription,
+ Origin = Anchor.CentreLeft,
+ Anchor = Anchor.CentreLeft,
+ Margin = new MarginPadding { Top = 1 },
+ });
+ }
+ }
+ }
+ }
+}
diff --git a/osu.Game/Rulesets/Mods/DifficultyAdjustSettingsControl.cs b/osu.Game/Rulesets/Mods/DifficultyAdjustSettingsControl.cs
index 45873a321a..c8e7284f5d 100644
--- a/osu.Game/Rulesets/Mods/DifficultyAdjustSettingsControl.cs
+++ b/osu.Game/Rulesets/Mods/DifficultyAdjustSettingsControl.cs
@@ -105,6 +105,7 @@ namespace osu.Game.Rulesets.Mods
{
ShowsDefaultIndicator = false,
Current = currentNumber,
+ KeyboardStep = 0.1f,
}
};
diff --git a/osu.Game/Rulesets/Mods/ModFlashlight.cs b/osu.Game/Rulesets/Mods/ModFlashlight.cs
index e6487c6b29..b449f3f64d 100644
--- a/osu.Game/Rulesets/Mods/ModFlashlight.cs
+++ b/osu.Game/Rulesets/Mods/ModFlashlight.cs
@@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Mods
public override string Description => "Restricted view area.";
[SettingSource("Flashlight size", "Multiplier applied to the default flashlight size.")]
- public abstract BindableNumber SizeMultiplier { get; }
+ public abstract BindableFloat SizeMultiplier { get; }
[SettingSource("Change size based on combo", "Decrease the flashlight size as combo increases.")]
public abstract BindableBool ComboBasedSize { get; }
diff --git a/osu.Game/Rulesets/RulesetStore.cs b/osu.Game/Rulesets/RulesetStore.cs
index d017d54ed9..dd25005006 100644
--- a/osu.Game/Rulesets/RulesetStore.cs
+++ b/osu.Game/Rulesets/RulesetStore.cs
@@ -149,6 +149,10 @@ namespace osu.Game.Rulesets
var instanceInfo = (Activator.CreateInstance(resolvedType) as Ruleset)?.RulesetInfo
?? throw new RulesetLoadException(@"Instantiation failure");
+ // If a ruleset isn't up-to-date with the API, it could cause a crash at an arbitrary point of execution.
+ // To eagerly handle cases of missing implementations, enumerate all types here and mark as non-available on throw.
+ resolvedType.Assembly.GetTypes();
+
r.Name = instanceInfo.Name;
r.ShortName = instanceInfo.ShortName;
r.InstantiationInfo = instanceInfo.InstantiationInfo;
diff --git a/osu.Game/Screens/OnlinePlay/Match/Components/MatchLeaderboardScore.cs b/osu.Game/Screens/OnlinePlay/Match/Components/MatchLeaderboardScore.cs
index 799c44cc28..cf7e33fd63 100644
--- a/osu.Game/Screens/OnlinePlay/Match/Components/MatchLeaderboardScore.cs
+++ b/osu.Game/Screens/OnlinePlay/Match/Components/MatchLeaderboardScore.cs
@@ -14,6 +14,8 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components
{
private readonly APIUserScoreAggregate score;
+ public override ScoreInfo TooltipContent => null; // match aggregate scores can't show statistics that the custom tooltip displays.
+
public MatchLeaderboardScore(APIUserScoreAggregate score, int? rank, bool isOnlineScope = true)
: base(score.CreateScoreInfo(), rank, isOnlineScope)
{
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs
index 8fbaebadfe..96a665f33d 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
+using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
@@ -18,6 +19,7 @@ using osu.Game.Graphics.UserInterface;
using osu.Game.Online;
using osu.Game.Online.API;
using osu.Game.Online.Multiplayer;
+using osu.Game.Online.Rooms;
using osu.Game.Rulesets;
using osu.Game.Screens.Play.HUD;
using osu.Game.Users;
@@ -184,8 +186,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
const double fade_time = 50;
- // Todo: Should use the room's selected item to determine ruleset.
- var ruleset = rulesets.GetRuleset(0)?.CreateInstance();
+ var currentItem = Playlist.GetCurrentItem();
+ Debug.Assert(currentItem != null);
+
+ var ruleset = rulesets.GetRuleset(currentItem.RulesetID)?.CreateInstance();
int? currentModeRank = ruleset != null ? User.User?.RulesetsStatistics?.GetValueOrDefault(ruleset.ShortName)?.GlobalRank : null;
userRankText.Text = currentModeRank != null ? $"#{currentModeRank.Value:N0}" : string.Empty;
diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs
index fdb5d418f3..628452fbc8 100644
--- a/osu.Game/Screens/Play/HUDOverlay.cs
+++ b/osu.Game/Screens/Play/HUDOverlay.cs
@@ -17,6 +17,7 @@ using osu.Game.Input.Bindings;
using osu.Game.Overlays;
using osu.Game.Overlays.Notifications;
using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Play.HUD;
using osu.Game.Skinning;
@@ -83,10 +84,7 @@ namespace osu.Game.Screens.Play
Children = new Drawable[]
{
CreateFailingLayer(),
- mainComponents = new SkinnableTargetContainer(SkinnableTarget.MainHUDComponents)
- {
- RelativeSizeAxes = Axes.Both,
- },
+ mainComponents = new MainComponentsContainer(),
topRightElements = new FillFlowContainer
{
Anchor = Anchor.TopRight,
@@ -325,5 +323,29 @@ namespace osu.Game.Screens.Play
break;
}
}
+
+ private class MainComponentsContainer : SkinnableTargetContainer
+ {
+ private Bindable scoringMode;
+
+ [Resolved]
+ private OsuConfigManager config { get; set; }
+
+ public MainComponentsContainer()
+ : base(SkinnableTarget.MainHUDComponents)
+ {
+ RelativeSizeAxes = Axes.Both;
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ // When the scoring mode changes, relative positions of elements may change (see DefaultSkin.GetDrawableComponent).
+ // This is a best effort implementation for cases where users haven't customised layouts.
+ scoringMode = config.GetBindable(OsuSetting.ScoreDisplayMode);
+ scoringMode.BindValueChanged(val => Reload());
+ }
+ }
}
}
diff --git a/osu.Game/Tests/Visual/ModTestScene.cs b/osu.Game/Tests/Visual/ModTestScene.cs
index a71d008eb9..2505864d59 100644
--- a/osu.Game/Tests/Visual/ModTestScene.cs
+++ b/osu.Game/Tests/Visual/ModTestScene.cs
@@ -5,8 +5,12 @@ using System;
using System.Collections.Generic;
using JetBrains.Annotations;
using osu.Game.Beatmaps;
+using osu.Game.Online.API.Requests.Responses;
+using osu.Game.Replays;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Replays;
+using osu.Game.Scoring;
namespace osu.Game.Tests.Visual
{
@@ -50,18 +54,37 @@ namespace osu.Game.Tests.Visual
return CreateModPlayer(ruleset);
}
- protected virtual TestPlayer CreateModPlayer(Ruleset ruleset) => new ModTestPlayer(AllowFail);
+ protected virtual TestPlayer CreateModPlayer(Ruleset ruleset) => new ModTestPlayer(currentTestData, AllowFail);
protected class ModTestPlayer : TestPlayer
{
private readonly bool allowFail;
+ private readonly ModTestData currentTestData;
protected override bool CheckModsAllowFailure() => allowFail;
- public ModTestPlayer(bool allowFail)
+ public ModTestPlayer(ModTestData data, bool allowFail)
: base(false, false)
{
this.allowFail = allowFail;
+ currentTestData = data;
+ }
+
+ protected override void PrepareReplay()
+ {
+ if (currentTestData.Autoplay && currentTestData.ReplayFrames?.Count > 0)
+ throw new InvalidOperationException(@$"{nameof(ModTestData.Autoplay)} must be false when {nameof(ModTestData.ReplayFrames)} is specified.");
+
+ if (currentTestData.ReplayFrames != null)
+ {
+ DrawableRuleset?.SetReplayScore(new Score
+ {
+ Replay = new Replay { Frames = currentTestData.ReplayFrames },
+ ScoreInfo = new ScoreInfo { User = new APIUser { Username = @"Test" } },
+ });
+ }
+
+ base.PrepareReplay();
}
}
@@ -72,6 +95,12 @@ namespace osu.Game.Tests.Visual
///
public bool Autoplay = true;
+ ///
+ /// The frames to use for replay. must be set to false.
+ ///
+ [CanBeNull]
+ public List ReplayFrames;
+
///
/// The beatmap for this test case.
///
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index b74d51e8f1..50cef71b26 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -37,7 +37,7 @@
-
+
diff --git a/osu.iOS.props b/osu.iOS.props
index 90ffb9605f..9ec0f1c0a0 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -61,7 +61,7 @@
-
+