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 @@ - +