diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj index 5eb5efa54c..3dd6be7307 100644 --- a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj +++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj @@ -12,7 +12,7 @@ - + diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj index d7c116411a..0c4bfe0ed7 100644 --- a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj +++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj @@ -12,7 +12,7 @@ - + diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj index 89b551286b..bb0a487274 100644 --- a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj +++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj @@ -12,7 +12,7 @@ - + diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj index d7c116411a..0c4bfe0ed7 100644 --- a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj +++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj @@ -12,7 +12,7 @@ - + diff --git a/osu.Android.props b/osu.Android.props index c845d7f276..591e00fb27 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,10 +52,10 @@ - + - + diff --git a/osu.Desktop/Updater/SquirrelUpdateManager.cs b/osu.Desktop/Updater/SquirrelUpdateManager.cs index 47cd39dc5a..58d67c11d9 100644 --- a/osu.Desktop/Updater/SquirrelUpdateManager.cs +++ b/osu.Desktop/Updater/SquirrelUpdateManager.cs @@ -116,7 +116,7 @@ namespace osu.Desktop.Updater if (scheduleRecheck) { // check again in 30 minutes. - Scheduler.AddDelayed(async () => await checkForUpdateAsync().ConfigureAwait(false), 60000 * 30); + Scheduler.AddDelayed(() => Task.Run(async () => await checkForUpdateAsync().ConfigureAwait(false)), 60000 * 30); } } @@ -141,7 +141,7 @@ namespace osu.Desktop.Updater Activated = () => { updateManager.PrepareUpdateAsync() - .ContinueWith(_ => updateManager.Schedule(() => game.GracefullyExit())); + .ContinueWith(_ => updateManager.Schedule(() => game?.GracefullyExit())); return true; }; } diff --git a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj index 7a74563b2b..da8a0540f4 100644 --- a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj +++ b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj @@ -9,7 +9,7 @@ - + diff --git a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj index 83d0744588..484da8e22e 100644 --- a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj +++ b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj @@ -4,7 +4,7 @@ - + diff --git a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj index b2a0912d19..6df555617b 100644 --- a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj +++ b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj @@ -4,7 +4,7 @@ - + diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs index 1d500dcc14..3252e6d912 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs @@ -37,11 +37,13 @@ namespace osu.Game.Rulesets.Osu.Tests private readonly BindableBool snakingIn = new BindableBool(); private readonly BindableBool snakingOut = new BindableBool(); + private IBeatmap beatmap; + private const double duration_of_span = 3605; private const double fade_in_modifier = -1200; protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) - => new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager); + => new ClockBackedTestWorkingBeatmap(this.beatmap = beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager); [BackgroundDependencyLoader] private void load(RulesetConfigCache configCache) @@ -51,8 +53,16 @@ namespace osu.Game.Rulesets.Osu.Tests config.BindWith(OsuRulesetSetting.SnakingOutSliders, snakingOut); } + private Slider slider; private DrawableSlider drawableSlider; + [SetUp] + public void Setup() => Schedule(() => + { + slider = null; + drawableSlider = null; + }); + [SetUpSteps] public override void SetUpSteps() { @@ -67,21 +77,19 @@ namespace osu.Game.Rulesets.Osu.Tests base.SetUpSteps(); AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning); - double startTime = hitObjects[sliderIndex].StartTime; - addSeekStep(startTime); - retrieveDrawableSlider((Slider)hitObjects[sliderIndex]); + retrieveSlider(sliderIndex); setSnaking(true); - ensureSnakingIn(startTime + fade_in_modifier); + addEnsureSnakingInSteps(() => slider.StartTime + fade_in_modifier); for (int i = 0; i < sliderIndex; i++) { // non-final repeats should not snake out - ensureNoSnakingOut(startTime, i); + addEnsureNoSnakingOutStep(() => slider.StartTime, i); } // final repeat should snake out - ensureSnakingOut(startTime, sliderIndex); + addEnsureSnakingOutSteps(() => slider.StartTime, sliderIndex); } [TestCase(0)] @@ -93,17 +101,15 @@ namespace osu.Game.Rulesets.Osu.Tests base.SetUpSteps(); AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning); - double startTime = hitObjects[sliderIndex].StartTime; - addSeekStep(startTime); - retrieveDrawableSlider((Slider)hitObjects[sliderIndex]); + retrieveSlider(sliderIndex); setSnaking(false); - ensureNoSnakingIn(startTime + fade_in_modifier); + addEnsureNoSnakingInSteps(() => slider.StartTime + fade_in_modifier); for (int i = 0; i <= sliderIndex; i++) { // no snaking out ever, including final repeat - ensureNoSnakingOut(startTime, i); + addEnsureNoSnakingOutStep(() => slider.StartTime, i); } } @@ -116,7 +122,7 @@ namespace osu.Game.Rulesets.Osu.Tests // repeat might have a chance to update its position depending on where in the frame its hit, // so some leniency is allowed here instead of checking strict equality - checkPositionChange(16600, sliderRepeat, positionAlmostSame); + addCheckPositionChangeSteps(() => 16600, getSliderRepeat, positionAlmostSame); } [Test] @@ -126,38 +132,41 @@ namespace osu.Game.Rulesets.Osu.Tests setSnaking(true); base.SetUpSteps(); - checkPositionChange(16600, sliderRepeat, positionDecreased); + addCheckPositionChangeSteps(() => 16600, getSliderRepeat, positionDecreased); } - private void retrieveDrawableSlider(Slider slider) => AddUntilStep($"retrieve slider @ {slider.StartTime}", () => - (drawableSlider = (DrawableSlider)Player.DrawableRuleset.Playfield.AllHitObjects.SingleOrDefault(d => d.HitObject == slider)) != null); - - private void ensureSnakingIn(double startTime) => checkPositionChange(startTime, sliderEnd, positionIncreased); - private void ensureNoSnakingIn(double startTime) => checkPositionChange(startTime, sliderEnd, positionRemainsSame); - - private void ensureSnakingOut(double startTime, int repeatIndex) + private void retrieveSlider(int index) { - var repeatTime = timeAtRepeat(startTime, repeatIndex); + AddStep("retrieve slider at index", () => slider = (Slider)beatmap.HitObjects[index]); + addSeekStep(() => slider); + AddUntilStep("retrieve drawable slider", () => + (drawableSlider = (DrawableSlider)Player.DrawableRuleset.Playfield.AllHitObjects.SingleOrDefault(d => d.HitObject == slider)) != null); + } + private void addEnsureSnakingInSteps(Func startTime) => addCheckPositionChangeSteps(startTime, getSliderEnd, positionIncreased); + private void addEnsureNoSnakingInSteps(Func startTime) => addCheckPositionChangeSteps(startTime, getSliderEnd, positionRemainsSame); + + private void addEnsureSnakingOutSteps(Func startTime, int repeatIndex) + { if (repeatIndex % 2 == 0) - checkPositionChange(repeatTime, sliderStart, positionIncreased); + addCheckPositionChangeSteps(timeAtRepeat(startTime, repeatIndex), getSliderStart, positionIncreased); else - checkPositionChange(repeatTime, sliderEnd, positionDecreased); + addCheckPositionChangeSteps(timeAtRepeat(startTime, repeatIndex), getSliderEnd, positionDecreased); } - private void ensureNoSnakingOut(double startTime, int repeatIndex) => - checkPositionChange(timeAtRepeat(startTime, repeatIndex), positionAtRepeat(repeatIndex), positionRemainsSame); + private void addEnsureNoSnakingOutStep(Func startTime, int repeatIndex) + => addCheckPositionChangeSteps(timeAtRepeat(startTime, repeatIndex), positionAtRepeat(repeatIndex), positionRemainsSame); - private double timeAtRepeat(double startTime, int repeatIndex) => startTime + 100 + duration_of_span * repeatIndex; - private Func positionAtRepeat(int repeatIndex) => repeatIndex % 2 == 0 ? (Func)sliderStart : sliderEnd; + private Func timeAtRepeat(Func startTime, int repeatIndex) => () => startTime() + 100 + duration_of_span * repeatIndex; + private Func positionAtRepeat(int repeatIndex) => repeatIndex % 2 == 0 ? (Func)getSliderStart : getSliderEnd; - private List sliderCurve => ((PlaySliderBody)drawableSlider.Body.Drawable).CurrentCurve; - private Vector2 sliderStart() => sliderCurve.First(); - private Vector2 sliderEnd() => sliderCurve.Last(); + private List getSliderCurve() => ((PlaySliderBody)drawableSlider.Body.Drawable).CurrentCurve; + private Vector2 getSliderStart() => getSliderCurve().First(); + private Vector2 getSliderEnd() => getSliderCurve().Last(); - private Vector2 sliderRepeat() + private Vector2 getSliderRepeat() { - var drawable = Player.DrawableRuleset.Playfield.AllHitObjects.SingleOrDefault(d => d.HitObject == hitObjects[1]); + var drawable = Player.DrawableRuleset.Playfield.AllHitObjects.SingleOrDefault(d => d.HitObject == beatmap.HitObjects[1]); var repeat = drawable.ChildrenOfType>().First().Children.First(); return repeat.Position; } @@ -167,7 +176,7 @@ namespace osu.Game.Rulesets.Osu.Tests private bool positionDecreased(Vector2 previous, Vector2 current) => current.X < previous.X && current.Y < previous.Y; private bool positionAlmostSame(Vector2 previous, Vector2 current) => Precision.AlmostEquals(previous, current, 1); - private void checkPositionChange(double startTime, Func positionToCheck, Func positionAssertion) + private void addCheckPositionChangeSteps(Func startTime, Func positionToCheck, Func positionAssertion) { Vector2 previousPosition = Vector2.Zero; @@ -176,7 +185,7 @@ namespace osu.Game.Rulesets.Osu.Tests addSeekStep(startTime); AddStep($"save {positionDescription} position", () => previousPosition = positionToCheck.Invoke()); - addSeekStep(startTime + 100); + addSeekStep(() => startTime() + 100); AddAssert($"{positionDescription} {assertionDescription}", () => { var currentPosition = positionToCheck.Invoke(); @@ -193,19 +202,21 @@ namespace osu.Game.Rulesets.Osu.Tests }); } - private void addSeekStep(double time) + private void addSeekStep(Func slider) { - AddStep($"seek to {time}", () => Player.GameplayClockContainer.Seek(time)); - - AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100)); + AddStep("seek to slider", () => Player.GameplayClockContainer.Seek(slider().StartTime)); + AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(slider().StartTime, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100)); } - protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new Beatmap + private void addSeekStep(Func time) { - HitObjects = hitObjects - }; + AddStep("seek to time", () => Player.GameplayClockContainer.Seek(time())); + AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time(), Player.DrawableRuleset.FrameStableClock.CurrentTime, 100)); + } - private readonly List hitObjects = new List + protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new Beatmap { HitObjects = createHitObjects() }; + + private static List createHitObjects() => new List { new Slider { diff --git a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj index 1efd19f49d..68be34d153 100644 --- a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj +++ b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj @@ -5,7 +5,7 @@ - + diff --git a/osu.Game.Rulesets.Osu/Mods/IHidesApproachCircles.cs b/osu.Game.Rulesets.Osu/Mods/IHidesApproachCircles.cs new file mode 100644 index 0000000000..4a3b187e83 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Mods/IHidesApproachCircles.cs @@ -0,0 +1,16 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Rulesets.Osu.Mods +{ + /// + /// Marker interface for any mod which completely hides the approach circles. + /// Used for incompatibility with . + /// + /// + /// Note that this is only a marker interface for incompatibility purposes, it does not change any gameplay behaviour. + /// + public interface IHidesApproachCircles + { + } +} diff --git a/osu.Game.Rulesets.Osu/Mods/IMutateApproachCircles.cs b/osu.Game.Rulesets.Osu/Mods/IMutateApproachCircles.cs deleted file mode 100644 index 60a5825241..0000000000 --- a/osu.Game.Rulesets.Osu/Mods/IMutateApproachCircles.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -namespace osu.Game.Rulesets.Osu.Mods -{ - /// - /// Any mod which affects the animation or visibility of approach circles. Should be used for incompatibility purposes. - /// - public interface IMutateApproachCircles - { - } -} diff --git a/osu.Game.Rulesets.Osu/Mods/IRequiresApproachCircles.cs b/osu.Game.Rulesets.Osu/Mods/IRequiresApproachCircles.cs new file mode 100644 index 0000000000..1458abfe05 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Mods/IRequiresApproachCircles.cs @@ -0,0 +1,16 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Rulesets.Osu.Mods +{ + /// + /// Marker interface for any mod which requires the approach circles to be visible. + /// Used for incompatibility with . + /// + /// + /// Note that this is only a marker interface for incompatibility purposes, it does not change any gameplay behaviour. + /// + public interface IRequiresApproachCircles + { + } +} diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModApproachDifferent.cs b/osu.Game.Rulesets.Osu/Mods/OsuModApproachDifferent.cs index 526e29ad53..d832411104 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModApproachDifferent.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModApproachDifferent.cs @@ -12,7 +12,7 @@ using osu.Game.Rulesets.Osu.Objects.Drawables; namespace osu.Game.Rulesets.Osu.Mods { - public class OsuModApproachDifferent : Mod, IApplicableToDrawableHitObject, IMutateApproachCircles + public class OsuModApproachDifferent : Mod, IApplicableToDrawableHitObject, IRequiresApproachCircles { public override string Name => "Approach Different"; public override string Acronym => "AD"; @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override double ScoreMultiplier => 1; public override IconUsage? Icon { get; } = FontAwesome.Regular.Circle; - public override Type[] IncompatibleMods => new[] { typeof(IMutateApproachCircles) }; + public override Type[] IncompatibleMods => new[] { typeof(IHidesApproachCircles) }; [SettingSource("Initial size", "Change the initial size of the approach circle, relative to hit circles.", 0)] public BindableFloat Scale { get; } = new BindableFloat(4) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs index 16b38cd0b1..9c7784a00a 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs @@ -15,12 +15,12 @@ using osu.Game.Rulesets.Osu.Skinning; namespace osu.Game.Rulesets.Osu.Mods { - public class OsuModHidden : ModHidden, IMutateApproachCircles + public class OsuModHidden : ModHidden, IHidesApproachCircles { public override string Description => @"Play with no approach circles and fading circles/sliders."; public override double ScoreMultiplier => 1.06; - public override Type[] IncompatibleMods => new[] { typeof(IMutateApproachCircles) }; + public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles), typeof(OsuModSpinIn) }; private const double fade_in_duration_multiplier = 0.4; private const double fade_out_duration_multiplier = 0.3; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModObjectScaleTween.cs b/osu.Game.Rulesets.Osu/Mods/OsuModObjectScaleTween.cs index 6dfabed0df..778447e444 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModObjectScaleTween.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModObjectScaleTween.cs @@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Osu.Mods /// /// Adjusts the size of hit objects during their fade in animation. /// - public abstract class OsuModObjectScaleTween : ModWithVisibilityAdjustment, IMutateApproachCircles + public abstract class OsuModObjectScaleTween : ModWithVisibilityAdjustment, IHidesApproachCircles { public override ModType Type => ModType.Fun; @@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Mods protected virtual float EndScale => 1; - public override Type[] IncompatibleMods => new[] { typeof(IMutateApproachCircles) }; + public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles), typeof(OsuModSpinIn) }; protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state) { diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSpinIn.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpinIn.cs index d3ca2973f0..56c246953e 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSpinIn.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpinIn.cs @@ -12,7 +12,7 @@ using osuTK; namespace osu.Game.Rulesets.Osu.Mods { - public class OsuModSpinIn : ModWithVisibilityAdjustment, IMutateApproachCircles + public class OsuModSpinIn : ModWithVisibilityAdjustment, IHidesApproachCircles { public override string Name => "Spin In"; public override string Acronym => "SI"; @@ -21,8 +21,9 @@ namespace osu.Game.Rulesets.Osu.Mods public override string Description => "Circles spin in. No approach circles."; public override double ScoreMultiplier => 1; - // todo: this mod should be able to be compatible with hidden with a bit of further implementation. - public override Type[] IncompatibleMods => new[] { typeof(IMutateApproachCircles) }; + // todo: this mod needs to be incompatible with "hidden" due to forcing the circle to remain opaque, + // further implementation will be required for supporting that. + public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles), typeof(OsuModObjectScaleTween), typeof(OsuModHidden) }; private const int rotate_offset = 360; private const float rotate_starting_width = 2; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs index 84263221a7..a05e4dea03 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs @@ -11,7 +11,7 @@ using osu.Game.Rulesets.Osu.Skinning.Default; namespace osu.Game.Rulesets.Osu.Mods { - public class OsuModTraceable : ModWithVisibilityAdjustment, IMutateApproachCircles + public class OsuModTraceable : ModWithVisibilityAdjustment, IRequiresApproachCircles { public override string Name => "Traceable"; public override string Acronym => "TC"; @@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override string Description => "Put your faith in the approach circles..."; public override double ScoreMultiplier => 1; - public override Type[] IncompatibleMods => new[] { typeof(IMutateApproachCircles) }; + public override Type[] IncompatibleMods => new[] { typeof(IHidesApproachCircles) }; protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state) { diff --git a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj index 8fb167ba10..532fdc5cb0 100644 --- a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj +++ b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj @@ -4,7 +4,7 @@ - + diff --git a/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs b/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs index adc1d6aede..0983b806e2 100644 --- a/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs +++ b/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs @@ -7,6 +7,7 @@ using NUnit.Framework; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Testing; using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; using osu.Game.Tests.Visual.Multiplayer; using osu.Game.Users; @@ -50,7 +51,10 @@ namespace osu.Game.Tests.NonVisual.Multiplayer AddStep("create room initially in gameplay", () => { - Room.RoomID.Value = null; + var newRoom = new Room(); + newRoom.CopyFrom(SelectedRoom.Value); + + newRoom.RoomID.Value = null; Client.RoomSetupAction = room => { room.State = MultiplayerRoomState.Playing; @@ -61,7 +65,7 @@ namespace osu.Game.Tests.NonVisual.Multiplayer }); }; - RoomManager.CreateRoom(Room); + RoomManager.CreateRoom(newRoom); }); AddUntilStep("wait for room join", () => Client.Room != null); diff --git a/osu.Game.Tests/OnlinePlay/TestSceneCatchUpSyncManager.cs b/osu.Game.Tests/OnlinePlay/TestSceneCatchUpSyncManager.cs index d4e591cf09..6851df3832 100644 --- a/osu.Game.Tests/OnlinePlay/TestSceneCatchUpSyncManager.cs +++ b/osu.Game.Tests/OnlinePlay/TestSceneCatchUpSyncManager.cs @@ -31,32 +31,24 @@ namespace osu.Game.Tests.OnlinePlay } [Test] - public void TestMasterClockStartsWhenAllPlayerClocksHaveFrames() + public void TestPlayerClocksStartWhenAllHaveFrames() { setWaiting(() => player1, false); - assertMasterState(false); assertPlayerClockState(() => player1, false); assertPlayerClockState(() => player2, false); setWaiting(() => player2, false); - assertMasterState(true); assertPlayerClockState(() => player1, true); assertPlayerClockState(() => player2, true); } [Test] - public void TestMasterClockDoesNotStartWhenNoneReadyForMaximumDelayTime() - { - AddWaitStep($"wait {CatchUpSyncManager.MAXIMUM_START_DELAY} milliseconds", (int)Math.Ceiling(CatchUpSyncManager.MAXIMUM_START_DELAY / TimePerAction)); - assertMasterState(false); - } - - [Test] - public void TestMasterClockStartsWhenAnyReadyForMaximumDelayTime() + public void TestReadyPlayersStartWhenReadyForMaximumDelayTime() { setWaiting(() => player1, false); AddWaitStep($"wait {CatchUpSyncManager.MAXIMUM_START_DELAY} milliseconds", (int)Math.Ceiling(CatchUpSyncManager.MAXIMUM_START_DELAY / TimePerAction)); - assertMasterState(true); + assertPlayerClockState(() => player1, true); + assertPlayerClockState(() => player2, false); } [Test] @@ -153,9 +145,6 @@ namespace osu.Game.Tests.OnlinePlay private void setPlayerClockTime(Func playerClock, double offsetFromMaster) => AddStep($"set player clock {playerClock().Id} = master - {offsetFromMaster}", () => playerClock().Seek(master.CurrentTime - offsetFromMaster)); - private void assertMasterState(bool running) - => AddAssert($"master clock {(running ? "is" : "is not")} running", () => master.IsRunning == running); - private void assertCatchingUp(Func playerClock, bool catchingUp) => AddAssert($"player clock {playerClock().Id} {(catchingUp ? "is" : "is not")} catching up", () => playerClock().IsCatchingUp == catchingUp); @@ -201,6 +190,11 @@ namespace osu.Game.Tests.OnlinePlay private class TestManualClock : ManualClock, IAdjustableClock { + public TestManualClock() + { + IsRunning = true; + } + public void Start() => IsRunning = true; public void Stop() => IsRunning = false; diff --git a/osu.Game.Tests/Rulesets/TestSceneRulesetSkinProvidingContainer.cs b/osu.Game.Tests/Rulesets/TestSceneRulesetSkinProvidingContainer.cs index 25619de323..28ad7ed6a7 100644 --- a/osu.Game.Tests/Rulesets/TestSceneRulesetSkinProvidingContainer.cs +++ b/osu.Game.Tests/Rulesets/TestSceneRulesetSkinProvidingContainer.cs @@ -2,14 +2,15 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Textures; +using osu.Framework.Testing; using osu.Game.Audio; using osu.Game.Rulesets; using osu.Game.Skinning; @@ -18,14 +19,21 @@ using osu.Game.Tests.Visual; namespace osu.Game.Tests.Rulesets { + [HeadlessTest] public class TestSceneRulesetSkinProvidingContainer : OsuTestScene { private SkinRequester requester; protected override Ruleset CreateRuleset() => new TestSceneRulesetDependencies.TestRuleset(); - [Cached(typeof(ISkinSource))] - private readonly ISkinSource testSource = new TestSkinProvider(); + [Test] + public void TestRulesetResources() + { + setupProviderStep(); + + AddAssert("ruleset texture retrieved via skin", () => requester.GetTexture("test-image") != null); + AddAssert("ruleset sample retrieved via skin", () => requester.GetSample(new SampleInfo("test-sample")) != null); + } [Test] public void TestEarlyAddedSkinRequester() @@ -38,7 +46,7 @@ namespace osu.Game.Tests.Rulesets rulesetSkinProvider.Add(requester = new SkinRequester()); - requester.OnLoadAsync += () => textureOnLoad = requester.GetTexture(TestSkinProvider.TEXTURE_NAME); + requester.OnLoadAsync += () => textureOnLoad = requester.GetTexture("test-image"); Child = rulesetSkinProvider; }); @@ -46,6 +54,15 @@ namespace osu.Game.Tests.Rulesets AddAssert("requester got correct initial texture", () => textureOnLoad != null); } + private void setupProviderStep() + { + AddStep("setup provider", () => + { + Child = new RulesetSkinProvidingContainer(Ruleset.Value.CreateInstance(), Beatmap.Value.Beatmap, Beatmap.Value.Skin) + .WithChild(requester = new SkinRequester()); + }); + } + private class SkinRequester : Drawable, ISkin { private ISkinSource skin; @@ -68,28 +85,5 @@ namespace osu.Game.Tests.Rulesets public IBindable GetConfig(TLookup lookup) => skin.GetConfig(lookup); } - - private class TestSkinProvider : ISkinSource - { - public const string TEXTURE_NAME = "some-texture"; - - public Drawable GetDrawableComponent(ISkinComponent component) => throw new NotImplementedException(); - - public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => componentName == TEXTURE_NAME ? Texture.WhitePixel : null; - - public ISample GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException(); - - public IBindable GetConfig(TLookup lookup) => throw new NotImplementedException(); - - public event Action SourceChanged - { - add { } - remove { } - } - - public ISkin FindProvider(Func lookupFunction) => lookupFunction(this) ? this : null; - - public IEnumerable AllSources => new[] { this }; - } } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs new file mode 100644 index 0000000000..d80fbfe309 --- /dev/null +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs @@ -0,0 +1,121 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Screens; +using osu.Game.Online.API; +using osu.Game.Online.Rooms; +using osu.Game.Online.Solo; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Objects; +using osu.Game.Screens.Ranking; + +namespace osu.Game.Tests.Visual.Gameplay +{ + public class TestScenePlayerScoreSubmission : OsuPlayerTestScene + { + protected override bool AllowFail => allowFail; + + private bool allowFail; + + private DummyAPIAccess dummyAPI => (DummyAPIAccess)API; + + protected override bool HasCustomSteps => true; + + protected override TestPlayer CreatePlayer(Ruleset ruleset) => new TestPlayer(false); + + [Test] + public void TestNoSubmissionOnResultsWithNoToken() + { + prepareTokenResponse(false); + + CreateTest(() => allowFail = false); + + AddUntilStep("wait for token request", () => Player.TokenCreationRequested); + + AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning); + AddStep("seek to completion", () => Player.GameplayClockContainer.Seek(Player.DrawableRuleset.Objects.Last().GetEndTime())); + + AddUntilStep("results displayed", () => Player.GetChildScreen() is ResultsScreen); + + AddAssert("ensure no submission", () => Player.SubmittedScore == null); + } + + [Test] + public void TestSubmissionOnResults() + { + prepareTokenResponse(true); + + CreateTest(() => allowFail = false); + + AddUntilStep("wait for token request", () => Player.TokenCreationRequested); + + AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning); + AddStep("seek to completion", () => Player.GameplayClockContainer.Seek(Player.DrawableRuleset.Objects.Last().GetEndTime())); + + AddUntilStep("results displayed", () => Player.GetChildScreen() is ResultsScreen); + AddAssert("ensure passing submission", () => Player.SubmittedScore?.ScoreInfo.Passed == true); + } + + [Test] + public void TestNoSubmissionOnExitWithNoToken() + { + prepareTokenResponse(false); + + CreateTest(() => allowFail = false); + + AddUntilStep("wait for token request", () => Player.TokenCreationRequested); + + AddStep("exit", () => Player.Exit()); + AddAssert("ensure no submission", () => Player.SubmittedScore == null); + } + + [Test] + public void TestSubmissionOnFail() + { + prepareTokenResponse(true); + + CreateTest(() => allowFail = true); + + AddUntilStep("wait for token request", () => Player.TokenCreationRequested); + AddUntilStep("wait for fail", () => Player.HasFailed); + AddStep("exit", () => Player.Exit()); + + AddAssert("ensure failing submission", () => Player.SubmittedScore?.ScoreInfo.Passed == false); + } + + [Test] + public void TestSubmissionOnExit() + { + prepareTokenResponse(true); + + CreateTest(() => allowFail = false); + + AddUntilStep("wait for token request", () => Player.TokenCreationRequested); + AddStep("exit", () => Player.Exit()); + AddAssert("ensure failing submission", () => Player.SubmittedScore?.ScoreInfo.Passed == false); + } + + private void prepareTokenResponse(bool validToken) + { + AddStep("Prepare test API", () => + { + dummyAPI.HandleRequest = request => + { + switch (request) + { + case CreateSoloScoreRequest tokenRequest: + if (validToken) + tokenRequest.TriggerSuccess(new APIScoreToken { ID = 1234 }); + else + tokenRequest.TriggerFailure(new APIException("something went wrong!", null)); + return true; + } + + return false; + }; + }); + } + } +} diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index 6eeb3596a8..7584d67afe 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -2,8 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System.Linq; -using System.Threading; -using System.Threading.Tasks; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -41,8 +39,6 @@ namespace osu.Game.Tests.Visual.Gameplay [Resolved] private OsuGameBase game { get; set; } - private int nextFrame; - private BeatmapSetInfo importedBeatmap; private int importedBeatmapId; @@ -51,8 +47,6 @@ namespace osu.Game.Tests.Visual.Gameplay { base.SetUpSteps(); - AddStep("reset sent frames", () => nextFrame = 0); - AddStep("import beatmap", () => { importedBeatmap = ImportBeatmapTest.LoadOszIntoOsu(game, virtualTrack: true).Result; @@ -105,7 +99,8 @@ namespace osu.Game.Tests.Visual.Gameplay waitForPlayer(); checkPaused(true); - sendFrames(1000); // send enough frames to ensure play won't be paused + // send enough frames to ensure play won't be paused + sendFrames(100); checkPaused(false); } @@ -114,12 +109,12 @@ namespace osu.Game.Tests.Visual.Gameplay public void TestSpectatingDuringGameplay() { start(); + sendFrames(300); loadSpectatingScreen(); waitForPlayer(); - AddStep("advance frame count", () => nextFrame = 300); - sendFrames(); + sendFrames(300); AddUntilStep("playing from correct point in time", () => player.ChildrenOfType().First().FrameStableClock.CurrentTime > 30000); } @@ -220,11 +215,7 @@ namespace osu.Game.Tests.Visual.Gameplay private void sendFrames(int count = 10) { - AddStep("send frames", () => - { - testSpectatorClient.SendFrames(streamingUser.Id, nextFrame, count); - nextFrame += count; - }); + AddStep("send frames", () => testSpectatorClient.SendFrames(streamingUser.Id, count)); } private void loadSpectatingScreen() @@ -232,14 +223,5 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("load screen", () => LoadScreen(spectatorScreen = new SoloSpectator(streamingUser))); AddUntilStep("wait for screen load", () => spectatorScreen.LoadState == LoadState.Loaded); } - - internal class TestUserLookupCache : UserLookupCache - { - protected override Task ComputeValueAsync(int lookup, CancellationToken token = default) => Task.FromResult(new User - { - Id = lookup, - Username = $"User {lookup}" - }); - } } } diff --git a/osu.Game.Tests/Visual/Multiplayer/RoomManagerTestScene.cs b/osu.Game.Tests/Visual/Multiplayer/RoomManagerTestScene.cs deleted file mode 100644 index c665a57452..0000000000 --- a/osu.Game.Tests/Visual/Multiplayer/RoomManagerTestScene.cs +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using osu.Framework.Allocation; -using osu.Game.Beatmaps; -using osu.Game.Online.Rooms; -using osu.Game.Rulesets; -using osu.Game.Screens.OnlinePlay; -using osu.Game.Users; - -namespace osu.Game.Tests.Visual.Multiplayer -{ - public abstract class RoomManagerTestScene : RoomTestScene - { - [Cached(Type = typeof(IRoomManager))] - protected TestRoomManager RoomManager { get; } = new TestRoomManager(); - - public override void SetUpSteps() - { - base.SetUpSteps(); - - AddStep("clear rooms", () => RoomManager.Rooms.Clear()); - } - - protected void AddRooms(int count, RulesetInfo ruleset = null) - { - AddStep("add rooms", () => - { - for (int i = 0; i < count; i++) - { - var room = new Room - { - RoomID = { Value = i }, - Name = { Value = $"Room {i}" }, - Host = { Value = new User { Username = "Host" } }, - EndDate = { Value = DateTimeOffset.Now + TimeSpan.FromSeconds(10) }, - Category = { Value = i % 2 == 0 ? RoomCategory.Spotlight : RoomCategory.Normal } - }; - - if (ruleset != null) - { - room.Playlist.Add(new PlaylistItem - { - Ruleset = { Value = ruleset }, - Beatmap = - { - Value = new BeatmapInfo - { - Metadata = new BeatmapMetadata() - } - } - }); - } - - RoomManager.Rooms.Add(room); - } - }); - } - } -} diff --git a/osu.Game.Tests/Visual/Multiplayer/TestRoomManager.cs b/osu.Game.Tests/Visual/Multiplayer/TestRoomManager.cs deleted file mode 100644 index 1785c99784..0000000000 --- a/osu.Game.Tests/Visual/Multiplayer/TestRoomManager.cs +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using osu.Framework.Bindables; -using osu.Game.Online.Rooms; -using osu.Game.Screens.OnlinePlay; - -namespace osu.Game.Tests.Visual.Multiplayer -{ - public class TestRoomManager : IRoomManager - { - public event Action RoomsUpdated - { - add { } - remove { } - } - - public readonly BindableList Rooms = new BindableList(); - - public IBindable InitialRoomsReceived { get; } = new Bindable(true); - - IBindableList IRoomManager.Rooms => Rooms; - - public void CreateRoom(Room room, Action onSuccess = null, Action onError = null) => Rooms.Add(room); - - public void JoinRoom(Room room, Action onSuccess = null, Action onError = null) - { - } - - public void PartRoom() - { - } - } -} diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomInfo.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomInfo.cs index 9f24347ae9..471d0b6c98 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomInfo.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomInfo.cs @@ -4,17 +4,21 @@ using System; using NUnit.Framework; using osu.Framework.Graphics; +using osu.Game.Online.Rooms; using osu.Game.Online.Rooms.RoomStatuses; using osu.Game.Screens.OnlinePlay.Lounge.Components; +using osu.Game.Tests.Visual.OnlinePlay; using osu.Game.Users; namespace osu.Game.Tests.Visual.Multiplayer { - public class TestSceneLoungeRoomInfo : RoomTestScene + public class TestSceneLoungeRoomInfo : OnlinePlayTestScene { [SetUp] public new void Setup() => Schedule(() => { + SelectedRoom.Value = new Room(); + Child = new RoomInfo { Anchor = Anchor.Centre, @@ -23,15 +27,10 @@ namespace osu.Game.Tests.Visual.Multiplayer }; }); - public override void SetUpSteps() - { - // Todo: Temp - } - [Test] public void TestNonSelectedRoom() { - AddStep("set null room", () => Room.RoomID.Value = null); + AddStep("set null room", () => SelectedRoom.Value.RoomID.Value = null); } [Test] @@ -39,11 +38,11 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("set open room", () => { - Room.RoomID.Value = 0; - Room.Name.Value = "Room 0"; - Room.Host.Value = new User { Username = "peppy", Id = 2 }; - Room.EndDate.Value = DateTimeOffset.Now.AddMonths(1); - Room.Status.Value = new RoomStatusOpen(); + SelectedRoom.Value.RoomID.Value = 0; + SelectedRoom.Value.Name.Value = "Room 0"; + SelectedRoom.Value.Host.Value = new User { Username = "peppy", Id = 2 }; + SelectedRoom.Value.EndDate.Value = DateTimeOffset.Now.AddMonths(1); + SelectedRoom.Value.Status.Value = new RoomStatusOpen(); }); } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs index 5682fd5c3c..75cc687ee8 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs @@ -3,24 +3,26 @@ using System.Linq; using NUnit.Framework; -using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Graphics; using osu.Game.Online.Rooms; using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Osu; using osu.Game.Screens.OnlinePlay.Lounge.Components; +using osu.Game.Tests.Visual.OnlinePlay; using osuTK.Graphics; using osuTK.Input; namespace osu.Game.Tests.Visual.Multiplayer { - public class TestSceneLoungeRoomsContainer : RoomManagerTestScene + public class TestSceneLoungeRoomsContainer : OnlinePlayTestScene { + protected new BasicTestRoomManager RoomManager => (BasicTestRoomManager)base.RoomManager; + private RoomsContainer container; - [BackgroundDependencyLoader] - private void load() + [SetUp] + public new void Setup() => Schedule(() => { Child = container = new RoomsContainer { @@ -29,12 +31,12 @@ namespace osu.Game.Tests.Visual.Multiplayer Width = 0.5f, JoinRequested = joinRequested }; - } + }); [Test] public void TestBasicListChanges() { - AddRooms(3); + AddStep("add rooms", () => RoomManager.AddRooms(3)); AddAssert("has 3 rooms", () => container.Rooms.Count == 3); AddStep("remove first room", () => RoomManager.Rooms.Remove(RoomManager.Rooms.FirstOrDefault())); @@ -51,7 +53,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestKeyboardNavigation() { - AddRooms(3); + AddStep("add rooms", () => RoomManager.AddRooms(3)); AddAssert("no selection", () => checkRoomSelected(null)); @@ -72,7 +74,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestClickDeselection() { - AddRooms(1); + AddStep("add room", () => RoomManager.AddRooms(1)); AddAssert("no selection", () => checkRoomSelected(null)); @@ -91,7 +93,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestStringFiltering() { - AddRooms(4); + AddStep("add rooms", () => RoomManager.AddRooms(4)); AddUntilStep("4 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 4); @@ -107,21 +109,21 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestRulesetFiltering() { - AddRooms(2, new OsuRuleset().RulesetInfo); - AddRooms(3, new CatchRuleset().RulesetInfo); + AddStep("add rooms", () => RoomManager.AddRooms(2, new OsuRuleset().RulesetInfo)); + AddStep("add rooms", () => RoomManager.AddRooms(3, new CatchRuleset().RulesetInfo)); + // Todo: What even is this case...? + AddStep("set empty filter criteria", () => container.Filter(null)); AddUntilStep("5 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 5); AddStep("filter osu! rooms", () => container.Filter(new FilterCriteria { Ruleset = new OsuRuleset().RulesetInfo })); - AddUntilStep("2 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 2); AddStep("filter catch rooms", () => container.Filter(new FilterCriteria { Ruleset = new CatchRuleset().RulesetInfo })); - AddUntilStep("3 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 3); } - private bool checkRoomSelected(Room room) => Room == room; + private bool checkRoomSelected(Room room) => SelectedRoom.Value == room; private void joinRequested(Room room) => room.Status.Value = new JoinedRoomStatus(); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs index 9ad9f2c883..d66603a448 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs @@ -11,11 +11,12 @@ using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Tests.Beatmaps; +using osu.Game.Tests.Visual.OnlinePlay; using osuTK; namespace osu.Game.Tests.Visual.Multiplayer { - public class TestSceneMatchBeatmapDetailArea : RoomTestScene + public class TestSceneMatchBeatmapDetailArea : OnlinePlayTestScene { [Resolved] private BeatmapManager beatmapManager { get; set; } @@ -26,6 +27,8 @@ namespace osu.Game.Tests.Visual.Multiplayer [SetUp] public new void Setup() => Schedule(() => { + SelectedRoom.Value = new Room(); + Child = new MatchBeatmapDetailArea { Anchor = Anchor.Centre, @@ -37,9 +40,9 @@ namespace osu.Game.Tests.Visual.Multiplayer private void createNewItem() { - Room.Playlist.Add(new PlaylistItem + SelectedRoom.Value.Playlist.Add(new PlaylistItem { - ID = Room.Playlist.Count, + ID = SelectedRoom.Value.Playlist.Count, Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo }, Ruleset = { Value = new OsuRuleset().RulesetInfo }, RequiredMods = diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchHeader.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchHeader.cs index 7cdc6b1a7d..71ba5db481 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchHeader.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchHeader.cs @@ -7,46 +7,49 @@ using osu.Game.Online.Rooms; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Screens.OnlinePlay.Match.Components; +using osu.Game.Tests.Visual.OnlinePlay; using osu.Game.Users; namespace osu.Game.Tests.Visual.Multiplayer { - public class TestSceneMatchHeader : RoomTestScene + public class TestSceneMatchHeader : OnlinePlayTestScene { - public TestSceneMatchHeader() - { - Child = new Header(); - } - [SetUp] public new void Setup() => Schedule(() => { - Room.Playlist.Add(new PlaylistItem + SelectedRoom.Value = new Room { - Beatmap = + Name = { Value = "A very awesome room" }, + Host = { Value = new User { Id = 2, Username = "peppy" } }, + Playlist = { - Value = new BeatmapInfo + new PlaylistItem { - Metadata = new BeatmapMetadata + Beatmap = { - Title = "Title", - Artist = "Artist", - AuthorString = "Author", + Value = new BeatmapInfo + { + Metadata = new BeatmapMetadata + { + Title = "Title", + Artist = "Artist", + AuthorString = "Author", + }, + Version = "Version", + Ruleset = new OsuRuleset().RulesetInfo + } }, - Version = "Version", - Ruleset = new OsuRuleset().RulesetInfo + RequiredMods = + { + new OsuModDoubleTime(), + new OsuModNoFail(), + new OsuModRelax(), + } } - }, - RequiredMods = - { - new OsuModDoubleTime(), - new OsuModNoFail(), - new OsuModRelax(), } - }); + }; - Room.Name.Value = "A very awesome room"; - Room.Host.Value = new User { Id = 2, Username = "peppy" }; + Child = new Header(); }); } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboard.cs index 64eaf0556b..a7a5f3af39 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboard.cs @@ -2,72 +2,74 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; -using Newtonsoft.Json; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Online.API; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Match.Components; +using osu.Game.Tests.Visual.OnlinePlay; using osu.Game.Users; using osuTK; namespace osu.Game.Tests.Visual.Multiplayer { - public class TestSceneMatchLeaderboard : RoomTestScene + public class TestSceneMatchLeaderboard : OnlinePlayTestScene { - protected override bool UseOnlineAPI => true; - - public TestSceneMatchLeaderboard() - { - Add(new MatchLeaderboard - { - Origin = Anchor.Centre, - Anchor = Anchor.Centre, - Size = new Vector2(550f, 450f), - Scope = MatchLeaderboardScope.Overall, - }); - } - [BackgroundDependencyLoader] - private void load(IAPIProvider api) + private void load() { - var req = new GetRoomScoresRequest(); - req.Success += v => { }; - req.Failure += _ => { }; + ((DummyAPIAccess)API).HandleRequest = r => + { + switch (r) + { + case GetRoomLeaderboardRequest leaderboardRequest: + leaderboardRequest.TriggerSuccess(new APILeaderboard + { + Leaderboard = new List + { + new APIUserScoreAggregate + { + UserID = 2, + User = new User { Id = 2, Username = "peppy" }, + TotalScore = 995533, + RoomID = 3, + CompletedBeatmaps = 1, + TotalAttempts = 6, + Accuracy = 0.9851 + }, + new APIUserScoreAggregate + { + UserID = 1040328, + User = new User { Id = 1040328, Username = "smoogipoo" }, + TotalScore = 981100, + RoomID = 3, + CompletedBeatmaps = 1, + TotalAttempts = 9, + Accuracy = 0.937 + } + } + }); + return true; + } - api.Queue(req); + return false; + }; } [SetUp] public new void Setup() => Schedule(() => { - Room.RoomID.Value = 3; + SelectedRoom.Value = new Room { RoomID = { Value = 3 } }; + + Child = new MatchLeaderboard + { + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Size = new Vector2(550f, 450f), + Scope = MatchLeaderboardScope.Overall, + }; }); - - private class GetRoomScoresRequest : APIRequest> - { - protected override string Target => "rooms/3/leaderboard"; - } - - private class RoomScore - { - [JsonProperty("user")] - public User User { get; set; } - - [JsonProperty("accuracy")] - public double Accuracy { get; set; } - - [JsonProperty("total_score")] - public int TotalScore { get; set; } - - [JsonProperty("pp")] - public double PP { get; set; } - - [JsonProperty("attempts")] - public int TotalAttempts { get; set; } - - [JsonProperty("completed")] - public int CompletedAttempts { get; set; } - } } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs index 5ad35be0ec..e14df62af1 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs @@ -3,74 +3,40 @@ using System.Collections.Generic; using System.Linq; -using System.Threading; -using System.Threading.Tasks; using NUnit.Framework; -using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Framework.Testing; using osu.Framework.Timing; -using osu.Game.Database; -using osu.Game.Online.Spectator; using osu.Game.Rulesets.Osu.Scoring; using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate; using osu.Game.Screens.Play.HUD; -using osu.Game.Tests.Visual.Spectator; -using osu.Game.Users; namespace osu.Game.Tests.Visual.Multiplayer { public class TestSceneMultiSpectatorLeaderboard : MultiplayerTestScene { - [Cached(typeof(SpectatorClient))] - private TestSpectatorClient spectatorClient = new TestSpectatorClient(); - - [Cached(typeof(UserLookupCache))] - private UserLookupCache lookupCache = new TestUserLookupCache(); - - protected override Container Content => content; - private readonly Container content; - - private readonly Dictionary clocks = new Dictionary - { - { PLAYER_1_ID, new ManualClock() }, - { PLAYER_2_ID, new ManualClock() } - }; - - public TestSceneMultiSpectatorLeaderboard() - { - base.Content.AddRange(new Drawable[] - { - spectatorClient, - lookupCache, - content = new Container { RelativeSizeAxes = Axes.Both } - }); - } + private Dictionary clocks; + private MultiSpectatorLeaderboard leaderboard; [SetUpSteps] public new void SetUpSteps() { - MultiSpectatorLeaderboard leaderboard = null; - AddStep("reset", () => { Clear(); - foreach (var (userId, clock) in clocks) + clocks = new Dictionary { - spectatorClient.EndPlay(userId); - clock.CurrentTime = 0; - } + { PLAYER_1_ID, new ManualClock() }, + { PLAYER_2_ID, new ManualClock() } + }; + + foreach (var (userId, _) in clocks) + SpectatorClient.StartPlay(userId, 0); }); AddStep("create leaderboard", () => { - foreach (var (userId, _) in clocks) - spectatorClient.StartPlay(userId, 0); - Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value); - var playable = Beatmap.Value.GetPlayableBeatmap(Ruleset.Value); var scoreProcessor = new OsuScoreProcessor(); scoreProcessor.ApplyBeatmap(playable); @@ -96,10 +62,10 @@ namespace osu.Game.Tests.Visual.Multiplayer // For player 2, send frames in sets of 10. for (int i = 0; i < 100; i++) { - spectatorClient.SendFrames(PLAYER_1_ID, i, 1); + SpectatorClient.SendFrames(PLAYER_1_ID, 1); if (i % 10 == 0) - spectatorClient.SendFrames(PLAYER_2_ID, i, 10); + SpectatorClient.SendFrames(PLAYER_2_ID, 10); } }); @@ -145,17 +111,5 @@ namespace osu.Game.Tests.Visual.Multiplayer private void assertCombo(int userId, int expectedCombo) => AddUntilStep($"player {userId} has {expectedCombo} combo", () => this.ChildrenOfType().Single(s => s.User?.Id == userId).Combo.Value == expectedCombo); - - private class TestUserLookupCache : UserLookupCache - { - protected override Task ComputeValueAsync(int lookup, CancellationToken token = default) - { - return Task.FromResult(new User - { - Id = lookup, - Username = $"User {lookup}" - }); - } - } } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs index b91391c409..b8db4067fb 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs @@ -3,31 +3,20 @@ using System.Collections.Generic; using System.Linq; -using System.Threading; -using System.Threading.Tasks; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Testing; using osu.Game.Beatmaps; -using osu.Game.Database; -using osu.Game.Online.Spectator; +using osu.Game.Rulesets.UI; using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate; using osu.Game.Screens.Play; using osu.Game.Tests.Beatmaps.IO; -using osu.Game.Tests.Visual.Spectator; -using osu.Game.Users; namespace osu.Game.Tests.Visual.Multiplayer { public class TestSceneMultiSpectatorScreen : MultiplayerTestScene { - [Cached(typeof(SpectatorClient))] - private TestSpectatorClient spectatorClient = new TestSpectatorClient(); - - [Cached(typeof(UserLookupCache))] - private UserLookupCache lookupCache = new TestUserLookupCache(); - [Resolved] private OsuGameBase game { get; set; } @@ -37,7 +26,6 @@ namespace osu.Game.Tests.Visual.Multiplayer private MultiSpectatorScreen spectatorScreen; private readonly List playingUserIds = new List(); - private readonly Dictionary nextFrame = new Dictionary(); private BeatmapSetInfo importedSet; private BeatmapInfo importedBeatmap; @@ -51,25 +39,8 @@ namespace osu.Game.Tests.Visual.Multiplayer importedBeatmapId = importedBeatmap.OnlineBeatmapID ?? -1; } - public override void SetUpSteps() - { - base.SetUpSteps(); - - AddStep("reset sent frames", () => nextFrame.Clear()); - - AddStep("add streaming client", () => - { - Remove(spectatorClient); - Add(spectatorClient); - }); - - AddStep("finish previous gameplay", () => - { - foreach (var id in playingUserIds) - spectatorClient.EndPlay(id); - playingUserIds.Clear(); - }); - } + [SetUp] + public new void Setup() => Schedule(() => playingUserIds.Clear()); [Test] public void TestDelayedStart() @@ -80,18 +51,16 @@ namespace osu.Game.Tests.Visual.Multiplayer Client.CurrentMatchPlayingUserIds.Add(PLAYER_2_ID); playingUserIds.Add(PLAYER_1_ID); playingUserIds.Add(PLAYER_2_ID); - nextFrame[PLAYER_1_ID] = 0; - nextFrame[PLAYER_2_ID] = 0; }); loadSpectateScreen(false); AddWaitStep("wait a bit", 10); - AddStep("load player first_player_id", () => spectatorClient.StartPlay(PLAYER_1_ID, importedBeatmapId)); + AddStep("load player first_player_id", () => SpectatorClient.StartPlay(PLAYER_1_ID, importedBeatmapId)); AddUntilStep("one player added", () => spectatorScreen.ChildrenOfType().Count() == 1); AddWaitStep("wait a bit", 10); - AddStep("load player second_player_id", () => spectatorClient.StartPlay(PLAYER_2_ID, importedBeatmapId)); + AddStep("load player second_player_id", () => SpectatorClient.StartPlay(PLAYER_2_ID, importedBeatmapId)); AddUntilStep("two players added", () => spectatorScreen.ChildrenOfType().Count() == 2); } @@ -107,6 +76,23 @@ namespace osu.Game.Tests.Visual.Multiplayer AddWaitStep("wait a bit", 20); } + [Test] + public void TestTimeDoesNotProgressWhileAllPlayersPaused() + { + start(new[] { PLAYER_1_ID, PLAYER_2_ID }); + loadSpectateScreen(); + + sendFrames(PLAYER_1_ID, 40); + sendFrames(PLAYER_2_ID, 20); + + checkPaused(PLAYER_2_ID, true); + checkPausedInstant(PLAYER_1_ID, false); + AddAssert("master clock still running", () => this.ChildrenOfType().Single().IsRunning); + + checkPaused(PLAYER_1_ID, true); + AddUntilStep("master clock paused", () => !this.ChildrenOfType().Single().IsRunning); + } + [Test] public void TestPlayersMustStartSimultaneously() { @@ -182,7 +168,7 @@ namespace osu.Game.Tests.Visual.Multiplayer // Send initial frames for both players. A few more for player 1. sendFrames(PLAYER_1_ID, 1000); - sendFrames(PLAYER_2_ID, 10); + sendFrames(PLAYER_2_ID, 30); checkPausedInstant(PLAYER_1_ID, false); checkPausedInstant(PLAYER_2_ID, false); @@ -210,8 +196,8 @@ namespace osu.Game.Tests.Visual.Multiplayer sendFrames(PLAYER_1_ID, 10); sendFrames(PLAYER_2_ID, 20); - assertMuted(PLAYER_1_ID, false); - assertMuted(PLAYER_2_ID, true); + checkPaused(PLAYER_1_ID, false); + assertOneNotMuted(); checkPaused(PLAYER_1_ID, true); assertMuted(PLAYER_1_ID, true); @@ -229,6 +215,36 @@ namespace osu.Game.Tests.Visual.Multiplayer assertMuted(PLAYER_2_ID, true); } + [Test] + public void TestSpectatingDuringGameplay() + { + var players = new[] { PLAYER_1_ID, PLAYER_2_ID }; + + start(players); + sendFrames(players, 300); + + loadSpectateScreen(); + sendFrames(players, 300); + + AddUntilStep("playing from correct point in time", () => this.ChildrenOfType().All(r => r.FrameStableClock.CurrentTime > 30000)); + } + + [Test] + public void TestSpectatingDuringGameplayWithLateFrames() + { + start(new[] { PLAYER_1_ID, PLAYER_2_ID }); + sendFrames(new[] { PLAYER_1_ID, PLAYER_2_ID }, 300); + + loadSpectateScreen(); + sendFrames(PLAYER_1_ID, 300); + + AddWaitStep("wait maximum start delay seconds", (int)(CatchUpSyncManager.MAXIMUM_START_DELAY / TimePerAction)); + checkPaused(PLAYER_1_ID, false); + + sendFrames(PLAYER_2_ID, 300); + AddUntilStep("player 2 playing from correct point in time", () => getPlayer(PLAYER_2_ID).ChildrenOfType().Single().FrameStableClock.CurrentTime > 30000); + } + private void loadSpectateScreen(bool waitForPlayerLoad = true) { AddStep("load screen", () => @@ -242,8 +258,6 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("wait for screen load", () => spectatorScreen.LoadState == LoadState.Loaded && (!waitForPlayerLoad || spectatorScreen.AllPlayersLoaded)); } - private void start(int userId, int? beatmapId = null) => start(new[] { userId }, beatmapId); - private void start(int[] userIds, int? beatmapId = null) { AddStep("start play", () => @@ -251,23 +265,12 @@ namespace osu.Game.Tests.Visual.Multiplayer foreach (int id in userIds) { Client.CurrentMatchPlayingUserIds.Add(id); - spectatorClient.StartPlay(id, beatmapId ?? importedBeatmapId); + SpectatorClient.StartPlay(id, beatmapId ?? importedBeatmapId); playingUserIds.Add(id); - nextFrame[id] = 0; } }); } - private void finish(int userId) - { - AddStep("end play", () => - { - spectatorClient.EndPlay(userId); - playingUserIds.Remove(userId); - nextFrame.Remove(userId); - }); - } - private void sendFrames(int userId, int count = 10) => sendFrames(new[] { userId }, count); private void sendFrames(int[] userIds, int count = 10) @@ -275,10 +278,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("send frames", () => { foreach (int id in userIds) - { - spectatorClient.SendFrames(id, nextFrame[id], count); - nextFrame[id] += count; - } + SpectatorClient.SendFrames(id, count); }); } @@ -286,7 +286,14 @@ namespace osu.Game.Tests.Visual.Multiplayer => AddUntilStep($"{userId} is {(state ? "paused" : "playing")}", () => getPlayer(userId).ChildrenOfType().First().GameplayClock.IsRunning != state); private void checkPausedInstant(int userId, bool state) - => AddAssert($"{userId} is {(state ? "paused" : "playing")}", () => getPlayer(userId).ChildrenOfType().First().GameplayClock.IsRunning != state); + { + checkPaused(userId, state); + + // Todo: The following should work, but is broken because SpectatorScreen retrieves the WorkingBeatmap via the BeatmapManager, bypassing the test scene clock and running real-time. + // AddAssert($"{userId} is {(state ? "paused" : "playing")}", () => getPlayer(userId).ChildrenOfType().First().GameplayClock.IsRunning != state); + } + + private void assertOneNotMuted() => AddAssert("one player not muted", () => spectatorScreen.ChildrenOfType().Count(p => !p.Mute) == 1); private void assertMuted(int userId, bool muted) => AddAssert($"{userId} {(muted ? "is" : "is not")} muted", () => getInstance(userId).Mute == muted); @@ -297,17 +304,5 @@ namespace osu.Game.Tests.Visual.Multiplayer private Player getPlayer(int userId) => getInstance(userId).ChildrenOfType().Single(); private PlayerArea getInstance(int userId) => spectatorScreen.ChildrenOfType().Single(p => p.UserId == userId); - - internal class TestUserLookupCache : UserLookupCache - { - protected override Task ComputeValueAsync(int lookup, CancellationToken token = default) - { - return Task.FromResult(new User - { - Id = lookup, - Username = $"User {lookup}" - }); - } - } } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index 599dfb082b..c93640e7b5 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -18,6 +18,7 @@ using osu.Game.Overlays.Mods; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Screens; using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Match.Components; using osu.Game.Screens.OnlinePlay.Multiplayer; @@ -30,14 +31,13 @@ namespace osu.Game.Tests.Visual.Multiplayer { public class TestSceneMultiplayer : ScreenTestScene { - private TestMultiplayer multiplayerScreen; - private BeatmapManager beatmaps; private RulesetStore rulesets; private BeatmapSetInfo importedSet; - private TestMultiplayerClient client => multiplayerScreen.Client; - private Room room => client.APIRoom; + private DependenciesScreen dependenciesScreen; + private TestMultiplayer multiplayerScreen; + private TestMultiplayerClient client; public TestSceneMultiplayer() { @@ -229,30 +229,43 @@ namespace osu.Game.Tests.Visual.Multiplayer private void loadMultiplayer() { - AddStep("show", () => + AddStep("create multiplayer screen", () => multiplayerScreen = new TestMultiplayer()); + + AddStep("load dependencies", () => { - multiplayerScreen = new TestMultiplayer(); + client = new TestMultiplayerClient(multiplayerScreen.RoomManager); - // Needs to be added at a higher level since the multiplayer screen becomes non-current. - Child = multiplayerScreen.Client; + // The screen gets suspended so it stops receiving updates. + Child = client; - LoadScreen(multiplayerScreen); + LoadScreen(dependenciesScreen = new DependenciesScreen(client)); }); - AddUntilStep("wait for loaded", () => multiplayerScreen.IsLoaded); + AddUntilStep("wait for dependencies to load", () => dependenciesScreen.IsLoaded); + + AddStep("load multiplayer", () => LoadScreen(multiplayerScreen)); + AddUntilStep("wait for multiplayer to load", () => multiplayerScreen.IsLoaded); } - private class TestMultiplayer : Screens.OnlinePlay.Multiplayer.Multiplayer + /// + /// Used for the sole purpose of adding as a resolvable dependency. + /// + private class DependenciesScreen : OsuScreen { [Cached(typeof(MultiplayerClient))] public readonly TestMultiplayerClient Client; - public TestMultiplayer() + public DependenciesScreen(TestMultiplayerClient client) { - Client = new TestMultiplayerClient((TestMultiplayerRoomManager)RoomManager); + Client = client; } + } - protected override RoomManager CreateRoomManager() => new TestMultiplayerRoomManager(); + private class TestMultiplayer : Screens.OnlinePlay.Multiplayer.Multiplayer + { + public new TestMultiplayerRoomManager RoomManager { get; private set; } + + protected override RoomManager CreateRoomManager() => RoomManager = new TestMultiplayerRoomManager(); } } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs index af2f6fa5fe..0e368b59dd 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs @@ -6,12 +6,11 @@ using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Configuration; -using osu.Game.Database; using osu.Game.Online.API; using osu.Game.Online.Spectator; using osu.Game.Replays.Legacy; @@ -19,37 +18,20 @@ using osu.Game.Rulesets.Osu.Scoring; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Screens.Play.HUD; -using osu.Game.Tests.Visual.Online; +using osu.Game.Tests.Visual.OnlinePlay; using osu.Game.Tests.Visual.Spectator; namespace osu.Game.Tests.Visual.Multiplayer { public class TestSceneMultiplayerGameplayLeaderboard : MultiplayerTestScene { - private const int users = 16; + private static IEnumerable users => Enumerable.Range(0, 16); - [Cached(typeof(SpectatorClient))] - private TestMultiplayerSpectatorClient spectatorClient = new TestMultiplayerSpectatorClient(); - - [Cached(typeof(UserLookupCache))] - private UserLookupCache lookupCache = new TestSceneCurrentlyPlayingDisplay.TestUserLookupCache(); + public new TestMultiplayerSpectatorClient SpectatorClient => (TestMultiplayerSpectatorClient)OnlinePlayDependencies?.SpectatorClient; private MultiplayerGameplayLeaderboard leaderboard; - - protected override Container Content { get; } = new Container { RelativeSizeAxes = Axes.Both }; - private OsuConfigManager config; - public TestSceneMultiplayerGameplayLeaderboard() - { - base.Content.Children = new Drawable[] - { - spectatorClient, - lookupCache, - Content - }; - } - [BackgroundDependencyLoader] private void load() { @@ -59,7 +41,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [SetUpSteps] public override void SetUpSteps() { - AddStep("set local user", () => ((DummyAPIAccess)API).LocalUser.Value = lookupCache.GetUserAsync(1).Result); + AddStep("set local user", () => ((DummyAPIAccess)API).LocalUser.Value = LookupCache.GetUserAsync(1).Result); AddStep("create leaderboard", () => { @@ -70,14 +52,11 @@ namespace osu.Game.Tests.Visual.Multiplayer var playable = Beatmap.Value.GetPlayableBeatmap(Ruleset.Value); - for (int i = 0; i < users; i++) - spectatorClient.StartPlay(i, Beatmap.Value.BeatmapInfo.OnlineBeatmapID ?? 0); + foreach (var user in users) + SpectatorClient.StartPlay(user, Beatmap.Value.BeatmapInfo.OnlineBeatmapID ?? 0); - spectatorClient.Schedule(() => - { - Client.CurrentMatchPlayingUserIds.Clear(); - Client.CurrentMatchPlayingUserIds.AddRange(spectatorClient.PlayingUsers); - }); + // Todo: This is REALLY bad. + Client.CurrentMatchPlayingUserIds.AddRange(users); Children = new Drawable[] { @@ -86,7 +65,7 @@ namespace osu.Game.Tests.Visual.Multiplayer scoreProcessor.ApplyBeatmap(playable); - LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(scoreProcessor, spectatorClient.PlayingUsers.ToArray()) + LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(scoreProcessor, users.ToArray()) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -100,24 +79,32 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestScoreUpdates() { - AddRepeatStep("update state", () => spectatorClient.RandomlyUpdateState(), 100); + AddRepeatStep("update state", () => SpectatorClient.RandomlyUpdateState(), 100); AddToggleStep("switch compact mode", expanded => leaderboard.Expanded.Value = expanded); } [Test] public void TestUserQuit() { - AddRepeatStep("mark user quit", () => Client.CurrentMatchPlayingUserIds.RemoveAt(0), users); + foreach (var user in users) + AddStep($"mark user {user} quit", () => Client.RemoveUser(LookupCache.GetUserAsync(user).Result.AsNonNull())); } [Test] public void TestChangeScoringMode() { - AddRepeatStep("update state", () => spectatorClient.RandomlyUpdateState(), 5); + AddRepeatStep("update state", () => SpectatorClient.RandomlyUpdateState(), 5); AddStep("change to classic", () => config.SetValue(OsuSetting.ScoreDisplayMode, ScoringMode.Classic)); AddStep("change to standardised", () => config.SetValue(OsuSetting.ScoreDisplayMode, ScoringMode.Standardised)); } + protected override OnlinePlayTestSceneDependencies CreateOnlinePlayDependencies() => new TestDependencies(); + + protected class TestDependencies : MultiplayerTestSceneDependencies + { + protected override TestSpectatorClient CreateSpectatorClient() => new TestMultiplayerSpectatorClient(); + } + public class TestMultiplayerSpectatorClient : TestSpectatorClient { private readonly Dictionary lastHeaders = new Dictionary(); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchFooter.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchFooter.cs index 6b03b53b4b..4e08ffef17 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchFooter.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchFooter.cs @@ -1,7 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Framework.Allocation; +using NUnit.Framework; using osu.Framework.Graphics; using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Multiplayer.Match; @@ -10,18 +10,17 @@ namespace osu.Game.Tests.Visual.Multiplayer { public class TestSceneMultiplayerMatchFooter : MultiplayerTestScene { - [Cached] - private readonly OnlinePlayBeatmapAvailabilityTracker availablilityTracker = new OnlinePlayBeatmapAvailabilityTracker(); - - [BackgroundDependencyLoader] - private void load() + [SetUp] + public new void Setup() => Schedule(() => { + SelectedRoom.Value = new Room(); + Child = new MultiplayerMatchFooter { Anchor = Anchor.Centre, Origin = Anchor.Centre, Height = 50 }; - } + }); } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs index 5b059c06f5..8bcb9cebbc 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs @@ -29,7 +29,7 @@ using osu.Game.Screens.Select; namespace osu.Game.Tests.Visual.Multiplayer { - public class TestSceneMultiplayerMatchSongSelect : RoomTestScene + public class TestSceneMultiplayerMatchSongSelect : MultiplayerTestScene { private BeatmapManager manager; private RulesetStore rulesets; diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs index e8ebc0c426..955be6ca21 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs @@ -49,13 +49,13 @@ namespace osu.Game.Tests.Visual.Multiplayer [SetUp] public new void Setup() => Schedule(() => { - Room.Name.Value = "Test Room"; + SelectedRoom.Value = new Room { Name = { Value = "Test Room" } }; }); [SetUpSteps] public void SetupSteps() { - AddStep("load match", () => LoadScreen(screen = new MultiplayerMatchSubScreen(Room))); + AddStep("load match", () => LoadScreen(screen = new MultiplayerMatchSubScreen(SelectedRoom.Value))); AddUntilStep("wait for load", () => screen.IsCurrentScreen()); } @@ -66,7 +66,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("set playlist", () => { - Room.Playlist.Add(new PlaylistItem + SelectedRoom.Value.Playlist.Add(new PlaylistItem { Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo }, Ruleset = { Value = new OsuRuleset().RulesetInfo }, @@ -81,7 +81,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("set playlist", () => { - Room.Playlist.Add(new PlaylistItem + SelectedRoom.Value.Playlist.Add(new PlaylistItem { Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo }, Ruleset = { Value = new OsuRuleset().RulesetInfo }, @@ -102,7 +102,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("set playlist", () => { - Room.Playlist.Add(new PlaylistItem + SelectedRoom.Value.Playlist.Add(new PlaylistItem { Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First()).BeatmapInfo }, Ruleset = { Value = new OsuRuleset().RulesetInfo }, diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs index 7f8f04b718..6526f7eea7 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs @@ -22,8 +22,11 @@ namespace osu.Game.Tests.Visual.Multiplayer { public class TestSceneMultiplayerParticipantsList : MultiplayerTestScene { - [SetUp] - public new void Setup() => Schedule(createNewParticipantsList); + [SetUpSteps] + public void SetupSteps() + { + createNewParticipantsList(); + } [Test] public void TestAddUser() @@ -88,7 +91,7 @@ namespace osu.Game.Tests.Visual.Multiplayer public void TestCorrectInitialState() { AddStep("set to downloading map", () => Client.ChangeBeatmapAvailability(BeatmapAvailability.Downloading(0))); - AddStep("recreate list", createNewParticipantsList); + createNewParticipantsList(); checkProgressBarVisibility(true); } @@ -233,7 +236,17 @@ namespace osu.Game.Tests.Visual.Multiplayer private void createNewParticipantsList() { - Child = new ParticipantsList { Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Y, Size = new Vector2(380, 0.7f) }; + ParticipantsList participantsList = null; + + AddStep("create new list", () => Child = participantsList = new ParticipantsList + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Y, + Size = new Vector2(380, 0.7f) + }); + + AddUntilStep("wait for list to load", () => participantsList.IsLoaded); } private void checkProgressBarVisibility(bool visible) => diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs index 929cd6ca80..0e036e8868 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs @@ -3,6 +3,7 @@ using System; using System.Linq; +using System.Threading.Tasks; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; @@ -27,7 +28,6 @@ namespace osu.Game.Tests.Visual.Multiplayer public class TestSceneMultiplayerReadyButton : MultiplayerTestScene { private MultiplayerReadyButton button; - private OnlinePlayBeatmapAvailabilityTracker beatmapTracker; private BeatmapSetInfo importedSet; private readonly Bindable selectedItem = new Bindable(); @@ -43,18 +43,13 @@ namespace osu.Game.Tests.Visual.Multiplayer Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, Beatmap.Default)); beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).Wait(); - - Add(beatmapTracker = new OnlinePlayBeatmapAvailabilityTracker - { - SelectedItem = { BindTarget = selectedItem } - }); - - Dependencies.Cache(beatmapTracker); } [SetUp] public new void Setup() => Schedule(() => { + AvailabilityTracker.SelectedItem.BindTo(selectedItem); + importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First(); Beatmap.Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First()); selectedItem.Value = new PlaylistItem @@ -71,18 +66,22 @@ namespace osu.Game.Tests.Visual.Multiplayer Anchor = Anchor.Centre, Origin = Anchor.Centre, Size = new Vector2(200, 50), - OnReadyClick = async () => + OnReadyClick = () => { readyClickOperation = OngoingOperationTracker.BeginOperation(); - if (Client.IsHost && Client.LocalUser?.State == MultiplayerUserState.Ready) + Task.Run(async () => { - await Client.StartMatch(); - return; - } + if (Client.IsHost && Client.LocalUser?.State == MultiplayerUserState.Ready) + { + await Client.StartMatch(); + return; + } - await Client.ToggleReady(); - readyClickOperation.Dispose(); + await Client.ToggleReady(); + + readyClickOperation.Dispose(); + }); } }); }); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerRoomManager.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerRoomManager.cs index c008771fd9..b17427a30b 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerRoomManager.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerRoomManager.cs @@ -3,37 +3,38 @@ using System; using NUnit.Framework; -using osu.Framework.Graphics; using osu.Framework.Testing; using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Tests.Beatmaps; +using osu.Game.Tests.Visual.OnlinePlay; namespace osu.Game.Tests.Visual.Multiplayer { [HeadlessTest] - public class TestSceneMultiplayerRoomManager : RoomTestScene + public class TestSceneMultiplayerRoomManager : MultiplayerTestScene { - private TestMultiplayerRoomContainer roomContainer; - private TestMultiplayerRoomManager roomManager => roomContainer.RoomManager; + protected override OnlinePlayTestSceneDependencies CreateOnlinePlayDependencies() => new TestDependencies(); + + public TestSceneMultiplayerRoomManager() + : base(false) + { + } [Test] public void TestPollsInitially() { AddStep("create room manager with a few rooms", () => { - createRoomManager().With(d => d.OnLoadComplete += _ => - { - roomManager.CreateRoom(createRoom(r => r.Name.Value = "1")); - roomManager.PartRoom(); - roomManager.CreateRoom(createRoom(r => r.Name.Value = "2")); - roomManager.PartRoom(); - roomManager.ClearRooms(); - }); + RoomManager.CreateRoom(createRoom(r => r.Name.Value = "1")); + RoomManager.PartRoom(); + RoomManager.CreateRoom(createRoom(r => r.Name.Value = "2")); + RoomManager.PartRoom(); + RoomManager.ClearRooms(); }); - AddAssert("manager polled for rooms", () => ((RoomManager)roomManager).Rooms.Count == 2); - AddAssert("initial rooms received", () => roomManager.InitialRoomsReceived.Value); + AddAssert("manager polled for rooms", () => ((RoomManager)RoomManager).Rooms.Count == 2); + AddAssert("initial rooms received", () => RoomManager.InitialRoomsReceived.Value); } [Test] @@ -41,19 +42,16 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("create room manager with a few rooms", () => { - createRoomManager().With(d => d.OnLoadComplete += _ => - { - roomManager.CreateRoom(createRoom()); - roomManager.PartRoom(); - roomManager.CreateRoom(createRoom()); - roomManager.PartRoom(); - }); + RoomManager.CreateRoom(createRoom()); + RoomManager.PartRoom(); + RoomManager.CreateRoom(createRoom()); + RoomManager.PartRoom(); }); - AddStep("disconnect", () => roomContainer.Client.Disconnect()); + AddStep("disconnect", () => Client.Disconnect()); - AddAssert("rooms cleared", () => ((RoomManager)roomManager).Rooms.Count == 0); - AddAssert("initial rooms not received", () => !roomManager.InitialRoomsReceived.Value); + AddAssert("rooms cleared", () => ((RoomManager)RoomManager).Rooms.Count == 0); + AddAssert("initial rooms not received", () => !RoomManager.InitialRoomsReceived.Value); } [Test] @@ -61,20 +59,17 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("create room manager with a few rooms", () => { - createRoomManager().With(d => d.OnLoadComplete += _ => - { - roomManager.CreateRoom(createRoom()); - roomManager.PartRoom(); - roomManager.CreateRoom(createRoom()); - roomManager.PartRoom(); - }); + RoomManager.CreateRoom(createRoom()); + RoomManager.PartRoom(); + RoomManager.CreateRoom(createRoom()); + RoomManager.PartRoom(); }); - AddStep("disconnect", () => roomContainer.Client.Disconnect()); - AddStep("connect", () => roomContainer.Client.Connect()); + AddStep("disconnect", () => Client.Disconnect()); + AddStep("connect", () => Client.Connect()); - AddAssert("manager polled for rooms", () => ((RoomManager)roomManager).Rooms.Count == 2); - AddAssert("initial rooms received", () => roomManager.InitialRoomsReceived.Value); + AddAssert("manager polled for rooms", () => ((RoomManager)RoomManager).Rooms.Count == 2); + AddAssert("initial rooms received", () => RoomManager.InitialRoomsReceived.Value); } [Test] @@ -82,15 +77,12 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("create room manager with a room", () => { - createRoomManager().With(d => d.OnLoadComplete += _ => - { - roomManager.CreateRoom(createRoom()); - roomManager.ClearRooms(); - }); + RoomManager.CreateRoom(createRoom()); + RoomManager.ClearRooms(); }); - AddAssert("manager not polled for rooms", () => ((RoomManager)roomManager).Rooms.Count == 0); - AddAssert("initial rooms not received", () => !roomManager.InitialRoomsReceived.Value); + AddAssert("manager not polled for rooms", () => ((RoomManager)RoomManager).Rooms.Count == 0); + AddAssert("initial rooms not received", () => !RoomManager.InitialRoomsReceived.Value); } [Test] @@ -98,13 +90,10 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("create room manager with a room", () => { - createRoomManager().With(d => d.OnLoadComplete += _ => - { - roomManager.CreateRoom(createRoom()); - }); + RoomManager.CreateRoom(createRoom()); }); - AddUntilStep("multiplayer room joined", () => roomContainer.Client.Room != null); + AddUntilStep("multiplayer room joined", () => Client.Room != null); } [Test] @@ -112,14 +101,11 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("create room manager with a room", () => { - createRoomManager().With(d => d.OnLoadComplete += _ => - { - roomManager.CreateRoom(createRoom()); - roomManager.PartRoom(); - }); + RoomManager.CreateRoom(createRoom()); + RoomManager.PartRoom(); }); - AddAssert("multiplayer room parted", () => roomContainer.Client.Room == null); + AddAssert("multiplayer room parted", () => Client.Room == null); } [Test] @@ -127,16 +113,13 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("create room manager with a room", () => { - createRoomManager().With(d => d.OnLoadComplete += _ => - { - var r = createRoom(); - roomManager.CreateRoom(r); - roomManager.PartRoom(); - roomManager.JoinRoom(r); - }); + var r = createRoom(); + RoomManager.CreateRoom(r); + RoomManager.PartRoom(); + RoomManager.JoinRoom(r); }); - AddUntilStep("multiplayer room joined", () => roomContainer.Client.Room != null); + AddUntilStep("multiplayer room joined", () => Client.Room != null); } private Room createRoom(Action initFunc = null) @@ -161,18 +144,14 @@ namespace osu.Game.Tests.Visual.Multiplayer return room; } - private TestMultiplayerRoomManager createRoomManager() + private class TestDependencies : MultiplayerTestSceneDependencies { - Child = roomContainer = new TestMultiplayerRoomContainer + public TestDependencies() { - RoomManager = - { - TimeBetweenListingPolls = { Value = 1 }, - TimeBetweenSelectionPolls = { Value = 1 } - } - }; - - return roomManager; + // Need to set these values as early as possible. + RoomManager.TimeBetweenListingPolls.Value = 1; + RoomManager.TimeBetweenSelectionPolls.Value = 1; + } } } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs index d00404102c..4966dfbe50 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs @@ -3,6 +3,7 @@ using System; using System.Linq; +using System.Threading.Tasks; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; @@ -37,40 +38,19 @@ namespace osu.Game.Tests.Visual.Multiplayer private IDisposable readyClickOperation; - protected override Container Content => content; - private readonly Container content; - - public TestSceneMultiplayerSpectateButton() - { - base.Content.Add(content = new Container - { - RelativeSizeAxes = Axes.Both - }); - } - - protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) - { - var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); - - return dependencies; - } - [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, Beatmap.Default)); - - var beatmapTracker = new OnlinePlayBeatmapAvailabilityTracker { SelectedItem = { BindTarget = selectedItem } }; - base.Content.Add(beatmapTracker); - Dependencies.Cache(beatmapTracker); - beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).Wait(); } [SetUp] public new void Setup() => Schedule(() => { + AvailabilityTracker.SelectedItem.BindTo(selectedItem); + importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First(); Beatmap.Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First()); selectedItem.Value = new PlaylistItem @@ -90,11 +70,15 @@ namespace osu.Game.Tests.Visual.Multiplayer Anchor = Anchor.Centre, Origin = Anchor.Centre, Size = new Vector2(200, 50), - OnSpectateClick = async () => + OnSpectateClick = () => { readyClickOperation = OngoingOperationTracker.BeginOperation(); - await Client.ToggleSpectate(); - readyClickOperation.Dispose(); + + Task.Run(async () => + { + await Client.ToggleSpectate(); + readyClickOperation.Dispose(); + }); } }, readyButton = new MultiplayerReadyButton @@ -102,18 +86,22 @@ namespace osu.Game.Tests.Visual.Multiplayer Anchor = Anchor.Centre, Origin = Anchor.Centre, Size = new Vector2(200, 50), - OnReadyClick = async () => + OnReadyClick = () => { readyClickOperation = OngoingOperationTracker.BeginOperation(); - if (Client.IsHost && Client.LocalUser?.State == MultiplayerUserState.Ready) + Task.Run(async () => { - await Client.StartMatch(); - return; - } + if (Client.IsHost && Client.LocalUser?.State == MultiplayerUserState.Ready) + { + await Client.StartMatch(); + return; + } - await Client.ToggleReady(); - readyClickOperation.Dispose(); + await Client.ToggleReady(); + + readyClickOperation.Dispose(); + }); } } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs index d95a95ebe5..e4bf9b36ed 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs @@ -14,16 +14,18 @@ using osu.Framework.Platform; using osu.Framework.Screens; using osu.Framework.Utils; using osu.Game.Beatmaps; +using osu.Game.Online.Rooms; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Playlists; +using osu.Game.Tests.Visual.OnlinePlay; namespace osu.Game.Tests.Visual.Multiplayer { - public class TestScenePlaylistsSongSelect : RoomTestScene + public class TestScenePlaylistsSongSelect : OnlinePlayTestScene { [Resolved] private BeatmapManager beatmapManager { get; set; } @@ -85,6 +87,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("reset", () => { + SelectedRoom.Value = new Room(); Ruleset.Value = new OsuRuleset().RulesetInfo; Beatmap.SetDefault(); SelectedMods.Value = Array.Empty(); @@ -98,14 +101,14 @@ namespace osu.Game.Tests.Visual.Multiplayer public void TestItemAddedIfEmptyOnStart() { AddStep("finalise selection", () => songSelect.FinaliseSelection()); - AddAssert("playlist has 1 item", () => Room.Playlist.Count == 1); + AddAssert("playlist has 1 item", () => SelectedRoom.Value.Playlist.Count == 1); } [Test] public void TestItemAddedWhenCreateNewItemClicked() { AddStep("create new item", () => songSelect.BeatmapDetails.CreateNewItem()); - AddAssert("playlist has 1 item", () => Room.Playlist.Count == 1); + AddAssert("playlist has 1 item", () => SelectedRoom.Value.Playlist.Count == 1); } [Test] @@ -113,7 +116,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("create new item", () => songSelect.BeatmapDetails.CreateNewItem()); AddStep("finalise selection", () => songSelect.FinaliseSelection()); - AddAssert("playlist has 1 item", () => Room.Playlist.Count == 1); + AddAssert("playlist has 1 item", () => SelectedRoom.Value.Playlist.Count == 1); } [Test] @@ -121,7 +124,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("create new item", () => songSelect.BeatmapDetails.CreateNewItem()); AddStep("create new item", () => songSelect.BeatmapDetails.CreateNewItem()); - AddAssert("playlist has 2 items", () => Room.Playlist.Count == 2); + AddAssert("playlist has 2 items", () => SelectedRoom.Value.Playlist.Count == 2); } [Test] @@ -131,13 +134,13 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("create new item", () => songSelect.BeatmapDetails.CreateNewItem()); AddStep("rearrange", () => { - var item = Room.Playlist[0]; - Room.Playlist.RemoveAt(0); - Room.Playlist.Add(item); + var item = SelectedRoom.Value.Playlist[0]; + SelectedRoom.Value.Playlist.RemoveAt(0); + SelectedRoom.Value.Playlist.Add(item); }); AddStep("create new item", () => songSelect.BeatmapDetails.CreateNewItem()); - AddAssert("new item has id 2", () => Room.Playlist.Last().ID == 2); + AddAssert("new item has id 2", () => SelectedRoom.Value.Playlist.Last().ID == 2); } /// @@ -151,8 +154,8 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("change mod rate", () => ((OsuModDoubleTime)SelectedMods.Value[0]).SpeedChange.Value = 2); AddStep("create item", () => songSelect.BeatmapDetails.CreateNewItem()); - AddAssert("item 1 has rate 1.5", () => Precision.AlmostEquals(1.5, ((OsuModDoubleTime)Room.Playlist.First().RequiredMods[0]).SpeedChange.Value)); - AddAssert("item 2 has rate 2", () => Precision.AlmostEquals(2, ((OsuModDoubleTime)Room.Playlist.Last().RequiredMods[0]).SpeedChange.Value)); + AddAssert("item 1 has rate 1.5", () => Precision.AlmostEquals(1.5, ((OsuModDoubleTime)SelectedRoom.Value.Playlist.First().RequiredMods[0]).SpeedChange.Value)); + AddAssert("item 2 has rate 2", () => Precision.AlmostEquals(2, ((OsuModDoubleTime)SelectedRoom.Value.Playlist.Last().RequiredMods[0]).SpeedChange.Value)); } /// @@ -174,7 +177,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("create item", () => songSelect.BeatmapDetails.CreateNewItem()); AddStep("change stored mod rate", () => mod.SpeedChange.Value = 2); - AddAssert("item has rate 1.5", () => Precision.AlmostEquals(1.5, ((OsuModDoubleTime)Room.Playlist.First().RequiredMods[0]).SpeedChange.Value)); + AddAssert("item has rate 1.5", () => Precision.AlmostEquals(1.5, ((OsuModDoubleTime)SelectedRoom.Value.Playlist.First().RequiredMods[0]).SpeedChange.Value)); } private class TestPlaylistsSongSelect : PlaylistsSongSelect diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneStarRatingRangeDisplay.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneStarRatingRangeDisplay.cs new file mode 100644 index 0000000000..cdeafdc9a3 --- /dev/null +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneStarRatingRangeDisplay.cs @@ -0,0 +1,40 @@ +// 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.Framework.Graphics; +using osu.Game.Beatmaps; +using osu.Game.Online.Rooms; +using osu.Game.Screens.OnlinePlay.Components; +using osu.Game.Tests.Visual.OnlinePlay; + +namespace osu.Game.Tests.Visual.Multiplayer +{ + public class TestSceneStarRatingRangeDisplay : OnlinePlayTestScene + { + [SetUp] + public new void Setup() => Schedule(() => + { + SelectedRoom.Value = new Room(); + + Child = new StarRatingRangeDisplay + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre + }; + }); + + [Test] + public void TestRange([Values(0, 2, 3, 4, 6, 7)] double min, [Values(0, 2, 3, 4, 6, 7)] double max) + { + AddStep("set playlist", () => + { + SelectedRoom.Value.Playlist.AddRange(new[] + { + new PlaylistItem { Beatmap = { Value = new BeatmapInfo { StarDifficulty = min } } }, + new PlaylistItem { Beatmap = { Value = new BeatmapInfo { StarDifficulty = max } } }, + }); + }); + } + } +} diff --git a/osu.Game.Tests/Visual/Navigation/TestScenePerformFromScreen.cs b/osu.Game.Tests/Visual/Navigation/TestScenePerformFromScreen.cs index 3cedaf9d45..92152bce18 100644 --- a/osu.Game.Tests/Visual/Navigation/TestScenePerformFromScreen.cs +++ b/osu.Game.Tests/Visual/Navigation/TestScenePerformFromScreen.cs @@ -11,6 +11,7 @@ using osu.Game.Overlays; using osu.Game.Screens; using osu.Game.Screens.Menu; using osu.Game.Screens.Play; +using osu.Game.Tests.Beatmaps.IO; using osuTK.Input; using static osu.Game.Tests.Visual.Navigation.TestSceneScreenNavigation; @@ -57,8 +58,11 @@ namespace osu.Game.Tests.Visual.Navigation [Test] public void TestPerformAtSongSelectFromPlayerLoader() { + AddStep("import beatmap", () => ImportBeatmapTest.LoadQuickOszIntoOsu(Game).Wait()); PushAndConfirm(() => new TestPlaySongSelect()); - PushAndConfirm(() => new PlayerLoader(() => new SoloPlayer())); + + AddStep("Press enter", () => InputManager.Key(Key.Enter)); + AddUntilStep("Wait for new screen", () => Game.ScreenStack.CurrentScreen is PlayerLoader); AddStep("try to perform", () => Game.PerformFromScreen(_ => actionPerformed = true, new[] { typeof(TestPlaySongSelect) })); AddUntilStep("returned to song select", () => Game.ScreenStack.CurrentScreen is TestPlaySongSelect); @@ -68,8 +72,11 @@ namespace osu.Game.Tests.Visual.Navigation [Test] public void TestPerformAtMenuFromPlayerLoader() { + AddStep("import beatmap", () => ImportBeatmapTest.LoadQuickOszIntoOsu(Game).Wait()); PushAndConfirm(() => new TestPlaySongSelect()); - PushAndConfirm(() => new PlayerLoader(() => new SoloPlayer())); + + AddStep("Press enter", () => InputManager.Key(Key.Enter)); + AddUntilStep("Wait for new screen", () => Game.ScreenStack.CurrentScreen is PlayerLoader); AddStep("try to perform", () => Game.PerformFromScreen(_ => actionPerformed = true)); AddUntilStep("returned to song select", () => Game.ScreenStack.CurrentScreen is MainMenu); diff --git a/osu.Game.Tests/Visual/Online/TestSceneWikiMarkdownContainer.cs b/osu.Game.Tests/Visual/Online/TestSceneWikiMarkdownContainer.cs index b6dce2c398..af2e4fc91a 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneWikiMarkdownContainer.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneWikiMarkdownContainer.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Linq; using Markdig.Syntax.Inlines; using NUnit.Framework; using osu.Framework.Allocation; @@ -9,6 +10,9 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers.Markdown; using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Testing; +using osu.Framework.Utils; using osu.Game.Graphics.Containers.Markdown; using osu.Game.Overlays; using osu.Game.Overlays.Wiki.Markdown; @@ -102,7 +106,7 @@ needs_cleanup: true { AddStep("Add absolute image", () => { - markdownContainer.DocumentUrl = "https://dev.ppy.sh"; + markdownContainer.CurrentPath = "https://dev.ppy.sh"; markdownContainer.Text = "![intro](/wiki/Interface/img/intro-screen.jpg)"; }); } @@ -112,8 +116,7 @@ needs_cleanup: true { AddStep("Add relative image", () => { - markdownContainer.DocumentUrl = "https://dev.ppy.sh"; - markdownContainer.CurrentPath = $"{API.WebsiteRootUrl}/wiki/Interface/"; + markdownContainer.CurrentPath = "https://dev.ppy.sh/wiki/Interface/"; markdownContainer.Text = "![intro](img/intro-screen.jpg)"; }); } @@ -123,8 +126,7 @@ needs_cleanup: true { AddStep("Add paragraph with block image", () => { - markdownContainer.DocumentUrl = "https://dev.ppy.sh"; - markdownContainer.CurrentPath = $"{API.WebsiteRootUrl}/wiki/Interface/"; + markdownContainer.CurrentPath = "https://dev.ppy.sh/wiki/Interface/"; markdownContainer.Text = @"Line before image ![play menu](img/play-menu.jpg ""Main Menu in osu!"") @@ -138,7 +140,7 @@ Line after image"; { AddStep("Add inline image", () => { - markdownContainer.DocumentUrl = "https://dev.ppy.sh"; + markdownContainer.CurrentPath = "https://dev.ppy.sh"; markdownContainer.Text = "![osu! mode icon](/wiki/shared/mode/osu.png) osu!"; }); } @@ -148,7 +150,7 @@ Line after image"; { AddStep("Add Table", () => { - markdownContainer.DocumentUrl = "https://dev.ppy.sh"; + markdownContainer.CurrentPath = "https://dev.ppy.sh"; markdownContainer.Text = @" | Image | Name | Effect | | :-: | :-: | :-- | @@ -162,15 +164,33 @@ Line after image"; }); } + [Test] + public void TestWideImageNotExceedContainer() + { + AddStep("Add image", () => + { + markdownContainer.CurrentPath = "https://dev.ppy.sh/wiki/osu!_Program_Files/"; + markdownContainer.Text = "![](img/file_structure.jpg \"The file structure of osu!'s installation folder, on Windows and macOS\")"; + }); + + AddUntilStep("Wait image to load", () => markdownContainer.ChildrenOfType().First().DelayedLoadCompleted); + + AddStep("Change container width", () => + { + markdownContainer.Width = 0.5f; + }); + + AddAssert("Image not exceed container width", () => + { + var spriteImage = markdownContainer.ChildrenOfType().First(); + return Precision.DefinitelyBigger(markdownContainer.DrawWidth, spriteImage.DrawWidth); + }); + } + private class TestMarkdownContainer : WikiMarkdownContainer { public LinkInline Link; - public new string DocumentUrl - { - set => base.DocumentUrl = value; - } - public override MarkdownTextFlowContainer CreateTextFlow() => new TestMarkdownTextFlowContainer { UrlAdded = link => Link = link, diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs index 618447eae2..b16b61c5c7 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs @@ -3,25 +3,21 @@ using System.Linq; using NUnit.Framework; -using osu.Framework.Allocation; using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Graphics.Containers; using osu.Game.Screens.OnlinePlay.Lounge; using osu.Game.Screens.OnlinePlay.Lounge.Components; using osu.Game.Screens.OnlinePlay.Playlists; -using osu.Game.Tests.Visual.Multiplayer; +using osu.Game.Tests.Visual.OnlinePlay; namespace osu.Game.Tests.Visual.Playlists { - public class TestScenePlaylistsLoungeSubScreen : RoomManagerTestScene + public class TestScenePlaylistsLoungeSubScreen : OnlinePlayTestScene { - private LoungeSubScreen loungeScreen; + protected new BasicTestRoomManager RoomManager => (BasicTestRoomManager)base.RoomManager; - [BackgroundDependencyLoader] - private void load() - { - } + private LoungeSubScreen loungeScreen; public override void SetUpSteps() { @@ -37,7 +33,7 @@ namespace osu.Game.Tests.Visual.Playlists [Test] public void TestScrollSelectedIntoView() { - AddRooms(30); + AddStep("add rooms", () => RoomManager.AddRooms(30)); AddUntilStep("first room is not masked", () => checkRoomVisible(roomsContainer.Rooms.First())); diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs index 44a79b6598..a320cb240f 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs @@ -3,7 +3,6 @@ using System; using NUnit.Framework; -using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -12,26 +11,28 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay; using osu.Game.Screens.OnlinePlay.Playlists; +using osu.Game.Tests.Visual.OnlinePlay; namespace osu.Game.Tests.Visual.Playlists { - public class TestScenePlaylistsMatchSettingsOverlay : RoomTestScene + public class TestScenePlaylistsMatchSettingsOverlay : OnlinePlayTestScene { - [Cached(Type = typeof(IRoomManager))] - private TestRoomManager roomManager = new TestRoomManager(); + protected new TestRoomManager RoomManager => (TestRoomManager)base.RoomManager; private TestRoomSettings settings; + protected override OnlinePlayTestSceneDependencies CreateOnlinePlayDependencies() => new TestDependencies(); + [SetUp] public new void Setup() => Schedule(() => { - settings = new TestRoomSettings + SelectedRoom.Value = new Room(); + + Child = settings = new TestRoomSettings { RelativeSizeAxes = Axes.Both, State = { Value = Visibility.Visible } }; - - Child = settings; }); [Test] @@ -39,19 +40,19 @@ namespace osu.Game.Tests.Visual.Playlists { AddStep("clear name and beatmap", () => { - Room.Name.Value = ""; - Room.Playlist.Clear(); + SelectedRoom.Value.Name.Value = ""; + SelectedRoom.Value.Playlist.Clear(); }); AddAssert("button disabled", () => !settings.ApplyButton.Enabled.Value); - AddStep("set name", () => Room.Name.Value = "Room name"); + AddStep("set name", () => SelectedRoom.Value.Name.Value = "Room name"); AddAssert("button disabled", () => !settings.ApplyButton.Enabled.Value); - AddStep("set beatmap", () => Room.Playlist.Add(new PlaylistItem { Beatmap = { Value = CreateBeatmap(Ruleset.Value).BeatmapInfo } })); + AddStep("set beatmap", () => SelectedRoom.Value.Playlist.Add(new PlaylistItem { Beatmap = { Value = CreateBeatmap(Ruleset.Value).BeatmapInfo } })); AddAssert("button enabled", () => settings.ApplyButton.Enabled.Value); - AddStep("clear name", () => Room.Name.Value = ""); + AddStep("clear name", () => SelectedRoom.Value.Name.Value = ""); AddAssert("button disabled", () => !settings.ApplyButton.Enabled.Value); } @@ -67,9 +68,9 @@ namespace osu.Game.Tests.Visual.Playlists { settings.NameField.Current.Value = expected_name; settings.DurationField.Current.Value = expectedDuration; - Room.Playlist.Add(new PlaylistItem { Beatmap = { Value = CreateBeatmap(Ruleset.Value).BeatmapInfo } }); + SelectedRoom.Value.Playlist.Add(new PlaylistItem { Beatmap = { Value = CreateBeatmap(Ruleset.Value).BeatmapInfo } }); - roomManager.CreateRequested = r => + RoomManager.CreateRequested = r => { createdRoom = r; return true; @@ -88,11 +89,11 @@ namespace osu.Game.Tests.Visual.Playlists AddStep("setup", () => { - Room.Name.Value = "Test Room"; - Room.Playlist.Add(new PlaylistItem { Beatmap = { Value = CreateBeatmap(Ruleset.Value).BeatmapInfo } }); + SelectedRoom.Value.Name.Value = "Test Room"; + SelectedRoom.Value.Playlist.Add(new PlaylistItem { Beatmap = { Value = CreateBeatmap(Ruleset.Value).BeatmapInfo } }); fail = true; - roomManager.CreateRequested = _ => !fail; + RoomManager.CreateRequested = _ => !fail; }); AddAssert("error not displayed", () => !settings.ErrorText.IsPresent); @@ -119,7 +120,12 @@ namespace osu.Game.Tests.Visual.Playlists public OsuSpriteText ErrorText => ((MatchSettings)Settings).ErrorText; } - private class TestRoomManager : IRoomManager + private class TestDependencies : OnlinePlayTestSceneDependencies + { + protected override IRoomManager CreateRoomManager() => new TestRoomManager(); + } + + protected class TestRoomManager : IRoomManager { public const string FAILED_TEXT = "failed"; diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsParticipantsList.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsParticipantsList.cs index 255f147ec9..76a78c0a3c 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsParticipantsList.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsParticipantsList.cs @@ -3,21 +3,23 @@ using NUnit.Framework; using osu.Framework.Graphics; +using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Components; +using osu.Game.Tests.Visual.OnlinePlay; using osu.Game.Users; namespace osu.Game.Tests.Visual.Playlists { - public class TestScenePlaylistsParticipantsList : RoomTestScene + public class TestScenePlaylistsParticipantsList : OnlinePlayTestScene { [SetUp] public new void Setup() => Schedule(() => { - Room.RoomID.Value = 7; + SelectedRoom.Value = new Room { RoomID = { Value = 7 } }; for (int i = 0; i < 50; i++) { - Room.RecentParticipants.Add(new User + SelectedRoom.Value.RecentParticipants.Add(new User { Username = "peppy", Statistics = new UserStatistics { GlobalRank = 1234 }, diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs index 6d7a254ab9..f2bfb80beb 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs @@ -15,20 +15,17 @@ using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; -using osu.Game.Screens.OnlinePlay; using osu.Game.Screens.OnlinePlay.Playlists; using osu.Game.Screens.Play; using osu.Game.Tests.Beatmaps; +using osu.Game.Tests.Visual.OnlinePlay; using osu.Game.Users; using osuTK.Input; namespace osu.Game.Tests.Visual.Playlists { - public class TestScenePlaylistsRoomSubScreen : RoomTestScene + public class TestScenePlaylistsRoomSubScreen : OnlinePlayTestScene { - [Cached(typeof(IRoomManager))] - private readonly TestRoomManager roomManager = new TestRoomManager(); - private BeatmapManager manager; private RulesetStore rulesets; @@ -56,8 +53,9 @@ namespace osu.Game.Tests.Visual.Playlists [SetUpSteps] public void SetupSteps() { + AddStep("set room", () => SelectedRoom.Value = new Room()); AddStep("ensure has beatmap", () => manager.Import(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo.BeatmapSet).Wait()); - AddStep("load match", () => LoadScreen(match = new TestPlaylistsRoomSubScreen(Room))); + AddStep("load match", () => LoadScreen(match = new TestPlaylistsRoomSubScreen(SelectedRoom.Value))); AddUntilStep("wait for load", () => match.IsCurrentScreen()); } @@ -66,12 +64,12 @@ namespace osu.Game.Tests.Visual.Playlists { AddStep("set room properties", () => { - Room.RoomID.Value = 1; - Room.Name.Value = "my awesome room"; - Room.Host.Value = new User { Id = 2, Username = "peppy" }; - Room.RecentParticipants.Add(Room.Host.Value); - Room.EndDate.Value = DateTimeOffset.Now.AddMinutes(5); - Room.Playlist.Add(new PlaylistItem + SelectedRoom.Value.RoomID.Value = 1; + SelectedRoom.Value.Name.Value = "my awesome room"; + SelectedRoom.Value.Host.Value = new User { Id = 2, Username = "peppy" }; + SelectedRoom.Value.RecentParticipants.Add(SelectedRoom.Value.Host.Value); + SelectedRoom.Value.EndDate.Value = DateTimeOffset.Now.AddMinutes(5); + SelectedRoom.Value.Playlist.Add(new PlaylistItem { Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo }, Ruleset = { Value = new OsuRuleset().RulesetInfo } @@ -87,9 +85,9 @@ namespace osu.Game.Tests.Visual.Playlists { AddStep("set room properties", () => { - Room.Name.Value = "my awesome room"; - Room.Host.Value = new User { Id = 2, Username = "peppy" }; - Room.Playlist.Add(new PlaylistItem + SelectedRoom.Value.Name.Value = "my awesome room"; + SelectedRoom.Value.Host.Value = new User { Id = 2, Username = "peppy" }; + SelectedRoom.Value.Playlist.Add(new PlaylistItem { Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo }, Ruleset = { Value = new OsuRuleset().RulesetInfo } @@ -103,7 +101,7 @@ namespace osu.Game.Tests.Visual.Playlists AddStep("click", () => InputManager.Click(MouseButton.Left)); - AddAssert("first playlist item selected", () => match.SelectedItem.Value == Room.Playlist[0]); + AddAssert("first playlist item selected", () => match.SelectedItem.Value == SelectedRoom.Value.Playlist[0]); } [Test] @@ -138,9 +136,9 @@ namespace osu.Game.Tests.Visual.Playlists AddStep("load room", () => { - Room.Name.Value = "my awesome room"; - Room.Host.Value = new User { Id = 2, Username = "peppy" }; - Room.Playlist.Add(new PlaylistItem + SelectedRoom.Value.Name.Value = "my awesome room"; + SelectedRoom.Value.Host.Value = new User { Id = 2, Username = "peppy" }; + SelectedRoom.Value.Playlist.Add(new PlaylistItem { Beatmap = { Value = importedSet.Beatmaps[0] }, Ruleset = { Value = new OsuRuleset().RulesetInfo } @@ -171,30 +169,5 @@ namespace osu.Game.Tests.Visual.Playlists { } } - - private class TestRoomManager : IRoomManager - { - public event Action RoomsUpdated - { - add => throw new NotImplementedException(); - remove => throw new NotImplementedException(); - } - - public IBindable InitialRoomsReceived { get; } = new Bindable(true); - - public IBindableList Rooms { get; } = new BindableList(); - - public void CreateRoom(Room room, Action onSuccess = null, Action onError = null) - { - room.RoomID.Value = 1; - onSuccess?.Invoke(room); - } - - public void JoinRoom(Room room, Action onSuccess = null, Action onError = null) => onSuccess?.Invoke(room); - - public void PartRoom() - { - } - } } } diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneColourPicker.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneColourPicker.cs new file mode 100644 index 0000000000..fa9179443d --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneColourPicker.cs @@ -0,0 +1,91 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Overlays; + +namespace osu.Game.Tests.Visual.UserInterface +{ + public class TestSceneColourPicker : OsuTestScene + { + private readonly Bindable colour = new Bindable(Colour4.Aquamarine); + + [SetUpSteps] + public void SetUpSteps() + { + AddStep("create pickers", () => Child = new GridContainer + { + RelativeSizeAxes = Axes.Both, + ColumnDimensions = new[] + { + new Dimension(), + new Dimension() + }, + Content = new[] + { + new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new OsuSpriteText + { + Text = @"No OverlayColourProvider", + Font = OsuFont.Default.With(size: 40) + }, + new OsuColourPicker + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Current = { BindTarget = colour }, + } + } + }, + new ColourProvidingContainer(OverlayColourScheme.Blue) + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new OsuSpriteText + { + Text = @"With blue OverlayColourProvider", + Font = OsuFont.Default.With(size: 40) + }, + new OsuColourPicker + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Current = { BindTarget = colour }, + } + } + } + } + } + }); + + AddStep("set green", () => colour.Value = Colour4.LimeGreen); + AddStep("set white", () => colour.Value = Colour4.White); + AddStep("set red", () => colour.Value = Colour4.Red); + } + + private class ColourProvidingContainer : Container + { + [Cached] + private OverlayColourProvider provider { get; } + + public ColourProvidingContainer(OverlayColourScheme colourScheme) + { + provider = new OverlayColourProvider(colourScheme); + } + } + } +} diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj index 35d3c7f202..161e248d96 100644 --- a/osu.Game.Tests/osu.Game.Tests.csproj +++ b/osu.Game.Tests/osu.Game.Tests.csproj @@ -5,7 +5,7 @@ - + diff --git a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj index 2084be765a..ba096abd36 100644 --- a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj +++ b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj @@ -7,7 +7,7 @@ - + WinExe diff --git a/osu.Game/Graphics/OsuColour.cs b/osu.Game/Graphics/OsuColour.cs index 15967c37c2..a44c28eaa6 100644 --- a/osu.Game/Graphics/OsuColour.cs +++ b/osu.Game/Graphics/OsuColour.cs @@ -32,10 +32,10 @@ namespace osu.Game.Graphics return Pink; case DifficultyRating.Expert: - return useLighterColour ? PurpleLight : Purple; + return PurpleLight; case DifficultyRating.ExpertPlus: - return useLighterColour ? Gray9 : Gray0; + return useLighterColour ? Gray9 : Color4Extensions.FromHex("#121415"); } } diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuColourPicker.cs b/osu.Game/Graphics/UserInterfaceV2/OsuColourPicker.cs new file mode 100644 index 0000000000..5394e5d0aa --- /dev/null +++ b/osu.Game/Graphics/UserInterfaceV2/OsuColourPicker.cs @@ -0,0 +1,19 @@ +// 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.UserInterface; + +namespace osu.Game.Graphics.UserInterfaceV2 +{ + public class OsuColourPicker : ColourPicker + { + public OsuColourPicker() + { + CornerRadius = 10; + Masking = true; + } + + protected override HSVColourPicker CreateHSVColourPicker() => new OsuHSVColourPicker(); + protected override HexColourPicker CreateHexColourPicker() => new OsuHexColourPicker(); + } +} diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuHSVColourPicker.cs b/osu.Game/Graphics/UserInterfaceV2/OsuHSVColourPicker.cs new file mode 100644 index 0000000000..06056f239b --- /dev/null +++ b/osu.Game/Graphics/UserInterfaceV2/OsuHSVColourPicker.cs @@ -0,0 +1,129 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using JetBrains.Annotations; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Effects; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.UserInterface; +using osu.Game.Overlays; +using osuTK; + +namespace osu.Game.Graphics.UserInterfaceV2 +{ + public class OsuHSVColourPicker : HSVColourPicker + { + private const float spacing = 10; + private const float corner_radius = 10; + private const float control_border_thickness = 3; + + protected override HueSelector CreateHueSelector() => new OsuHueSelector(); + protected override SaturationValueSelector CreateSaturationValueSelector() => new OsuSaturationValueSelector(); + + [BackgroundDependencyLoader(true)] + private void load([CanBeNull] OverlayColourProvider colourProvider, OsuColour osuColour) + { + Background.Colour = colourProvider?.Dark5 ?? osuColour.GreySeafoamDark; + + Content.Padding = new MarginPadding(spacing); + Content.Spacing = new Vector2(0, spacing); + } + + private static EdgeEffectParameters createShadowParameters() => new EdgeEffectParameters + { + Type = EdgeEffectType.Shadow, + Offset = new Vector2(0, 1), + Radius = 3, + Colour = Colour4.Black.Opacity(0.3f) + }; + + private class OsuHueSelector : HueSelector + { + public OsuHueSelector() + { + SliderBar.CornerRadius = corner_radius; + SliderBar.Masking = true; + } + + protected override Drawable CreateSliderNub() => new SliderNub(this); + + private class SliderNub : CompositeDrawable + { + private readonly Bindable hue; + private readonly Box fill; + + public SliderNub(OsuHueSelector osuHueSelector) + { + hue = osuHueSelector.Hue.GetBoundCopy(); + + InternalChild = new CircularContainer + { + Height = 35, + Width = 10, + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Masking = true, + BorderColour = Colour4.White, + BorderThickness = control_border_thickness, + EdgeEffect = createShadowParameters(), + Child = fill = new Box + { + RelativeSizeAxes = Axes.Both + } + }; + } + + protected override void LoadComplete() + { + hue.BindValueChanged(h => fill.Colour = Colour4.FromHSV(h.NewValue, 1, 1), true); + } + } + } + + private class OsuSaturationValueSelector : SaturationValueSelector + { + public OsuSaturationValueSelector() + { + SelectionArea.CornerRadius = corner_radius; + SelectionArea.Masking = true; + // purposefully use hard non-AA'd masking to avoid edge artifacts. + SelectionArea.MaskingSmoothness = 0; + } + + protected override Marker CreateMarker() => new OsuMarker(); + + private class OsuMarker : Marker + { + private readonly Box previewBox; + + public OsuMarker() + { + AutoSizeAxes = Axes.Both; + + InternalChild = new CircularContainer + { + Size = new Vector2(20), + Masking = true, + BorderColour = Colour4.White, + BorderThickness = control_border_thickness, + EdgeEffect = createShadowParameters(), + Child = previewBox = new Box + { + RelativeSizeAxes = Axes.Both + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Current.BindValueChanged(colour => previewBox.Colour = colour.NewValue, true); + } + } + } + } +} diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuHexColourPicker.cs b/osu.Game/Graphics/UserInterfaceV2/OsuHexColourPicker.cs new file mode 100644 index 0000000000..331a1b67c9 --- /dev/null +++ b/osu.Game/Graphics/UserInterfaceV2/OsuHexColourPicker.cs @@ -0,0 +1,57 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using JetBrains.Annotations; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.UserInterface; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays; + +namespace osu.Game.Graphics.UserInterfaceV2 +{ + public class OsuHexColourPicker : HexColourPicker + { + public OsuHexColourPicker() + { + Padding = new MarginPadding(20); + Spacing = 20; + } + + [BackgroundDependencyLoader(true)] + private void load([CanBeNull] OverlayColourProvider overlayColourProvider, OsuColour osuColour) + { + Background.Colour = overlayColourProvider?.Dark6 ?? osuColour.GreySeafoamDarker; + } + + protected override TextBox CreateHexCodeTextBox() => new OsuTextBox(); + protected override ColourPreview CreateColourPreview() => new OsuColourPreview(); + + private class OsuColourPreview : ColourPreview + { + private readonly Box preview; + + public OsuColourPreview() + { + InternalChild = new CircularContainer + { + RelativeSizeAxes = Axes.Both, + Masking = true, + Child = preview = new Box + { + RelativeSizeAxes = Axes.Both + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Current.BindValueChanged(colour => preview.Colour = colour.NewValue, true); + } + } + } +} diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 32136b8789..dcd2d68b43 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -223,7 +223,20 @@ namespace osu.Game // bind config int to database RulesetInfo configRuleset = LocalConfig.GetBindable(OsuSetting.Ruleset); - Ruleset.Value = RulesetStore.GetRuleset(configRuleset.Value) ?? RulesetStore.AvailableRulesets.First(); + + var preferredRuleset = RulesetStore.GetRuleset(configRuleset.Value); + + try + { + Ruleset.Value = preferredRuleset ?? RulesetStore.AvailableRulesets.First(); + } + catch (Exception e) + { + // on startup, a ruleset may be selected which has compatibility issues. + Logger.Error(e, $@"Failed to switch to preferred ruleset {preferredRuleset}."); + Ruleset.Value = RulesetStore.AvailableRulesets.First(); + } + Ruleset.ValueChanged += r => configRuleset.Value = r.NewValue.ID ?? 0; // bind config int to database SkinInfo @@ -478,6 +491,10 @@ namespace osu.Game public override Task Import(params ImportTask[] imports) { // encapsulate task as we don't want to begin the import process until in a ready state. + + // ReSharper disable once AsyncVoidLambda + // TODO: This is bad because `new Task` doesn't have a Func override. + // Only used for android imports and a bit of a mess. Probably needs rethinking overall. var importTask = new Task(async () => await base.Import(imports).ConfigureAwait(false)); waitForReady(() => this, _ => importTask.Start()); diff --git a/osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs b/osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs index f8e1ac0c84..cb144defbf 100644 --- a/osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs +++ b/osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using Humanizer; +using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Online.API.Requests.Responses; using osuTK.Graphics; @@ -17,11 +18,11 @@ namespace osu.Game.Overlays.Changelog Width *= 2; } - protected override string MainText => Value.DisplayName; + protected override LocalisableString MainText => Value.DisplayName; - protected override string AdditionalText => Value.LatestBuild.DisplayVersion; + protected override LocalisableString AdditionalText => Value.LatestBuild.DisplayVersion; - protected override string InfoText => Value.LatestBuild.Users > 0 ? $"{"user".ToQuantity(Value.LatestBuild.Users, "N0")} online" : null; + protected override LocalisableString InfoText => Value.LatestBuild.Users > 0 ? $"{"user".ToQuantity(Value.LatestBuild.Users, "N0")} online" : null; protected override Color4 GetBarColour(OsuColour colours) => Value.Colour; } diff --git a/osu.Game/Overlays/Dashboard/DashboardOverlayHeader.cs b/osu.Game/Overlays/Dashboard/DashboardOverlayHeader.cs index 3314ed957a..056d4ad6f7 100644 --- a/osu.Game/Overlays/Dashboard/DashboardOverlayHeader.cs +++ b/osu.Game/Overlays/Dashboard/DashboardOverlayHeader.cs @@ -1,7 +1,10 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.ComponentModel; +using osu.Framework.Localisation; +using osu.Game.Resources.Localisation.Web; namespace osu.Game.Overlays.Dashboard { @@ -13,13 +16,14 @@ namespace osu.Game.Overlays.Dashboard { public DashboardTitle() { - Title = "dashboard"; + Title = HomeStrings.UserTitle; Description = "view your friends and other information"; IconTexture = "Icons/Hexacons/social"; } } } + [LocalisableEnum(typeof(DashboardOverlayTabsEnumLocalisationMapper))] public enum DashboardOverlayTabs { Friends, @@ -27,4 +31,22 @@ namespace osu.Game.Overlays.Dashboard [Description("Currently Playing")] CurrentlyPlaying } + + public class DashboardOverlayTabsEnumLocalisationMapper : EnumLocalisationMapper + { + public override LocalisableString Map(DashboardOverlayTabs value) + { + switch (value) + { + case DashboardOverlayTabs.Friends: + return FriendsStrings.TitleCompact; + + case DashboardOverlayTabs.CurrentlyPlaying: + return @"Currently Playing"; + + default: + throw new ArgumentOutOfRangeException(nameof(value), value, null); + } + } + } } diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendsOnlineStatusItem.cs b/osu.Game/Overlays/Dashboard/Friends/FriendsOnlineStatusItem.cs index 7e902203f8..11dcb93e6f 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendsOnlineStatusItem.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendsOnlineStatusItem.cs @@ -2,6 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using System; +using osu.Framework.Extensions; +using osu.Framework.Localisation; using osu.Game.Graphics; using osuTK.Graphics; @@ -14,9 +16,9 @@ namespace osu.Game.Overlays.Dashboard.Friends { } - protected override string MainText => Value.Status.ToString(); + protected override LocalisableString MainText => Value.Status.GetLocalisableDescription(); - protected override string AdditionalText => Value.Count.ToString(); + protected override LocalisableString AdditionalText => Value.Count.ToString(); protected override Color4 GetBarColour(OsuColour colours) { diff --git a/osu.Game/Overlays/Dashboard/Friends/OnlineStatus.cs b/osu.Game/Overlays/Dashboard/Friends/OnlineStatus.cs index 6f2f55a6ed..4b5a7ef066 100644 --- a/osu.Game/Overlays/Dashboard/Friends/OnlineStatus.cs +++ b/osu.Game/Overlays/Dashboard/Friends/OnlineStatus.cs @@ -1,12 +1,38 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using osu.Framework.Localisation; +using osu.Game.Resources.Localisation.Web; + namespace osu.Game.Overlays.Dashboard.Friends { + [LocalisableEnum(typeof(OnlineStatusEnumLocalisationMapper))] public enum OnlineStatus { All, Online, Offline } + + public class OnlineStatusEnumLocalisationMapper : EnumLocalisationMapper + { + public override LocalisableString Map(OnlineStatus value) + { + switch (value) + { + case OnlineStatus.All: + return SortStrings.All; + + case OnlineStatus.Online: + return UsersStrings.StatusOnline; + + case OnlineStatus.Offline: + return UsersStrings.StatusOffline; + + default: + throw new ArgumentOutOfRangeException(nameof(value), value, null); + } + } + } } diff --git a/osu.Game/Overlays/Dashboard/Friends/UserSortTabControl.cs b/osu.Game/Overlays/Dashboard/Friends/UserSortTabControl.cs index 3a5f65212d..dc756e2957 100644 --- a/osu.Game/Overlays/Dashboard/Friends/UserSortTabControl.cs +++ b/osu.Game/Overlays/Dashboard/Friends/UserSortTabControl.cs @@ -1,7 +1,10 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.ComponentModel; +using osu.Framework.Localisation; +using osu.Game.Resources.Localisation.Web; namespace osu.Game.Overlays.Dashboard.Friends { @@ -9,6 +12,7 @@ namespace osu.Game.Overlays.Dashboard.Friends { } + [LocalisableEnum(typeof(UserSortCriteriaEnumLocalisationMappper))] public enum UserSortCriteria { [Description(@"Recently Active")] @@ -16,4 +20,25 @@ namespace osu.Game.Overlays.Dashboard.Friends Rank, Username } + + public class UserSortCriteriaEnumLocalisationMappper : EnumLocalisationMapper + { + public override LocalisableString Map(UserSortCriteria value) + { + switch (value) + { + case UserSortCriteria.LastVisit: + return SortStrings.LastVisit; + + case UserSortCriteria.Rank: + return SortStrings.Rank; + + case UserSortCriteria.Username: + return SortStrings.Username; + + default: + throw new ArgumentOutOfRangeException(nameof(value), value, null); + } + } + } } diff --git a/osu.Game/Overlays/OverlayPanelDisplayStyleControl.cs b/osu.Game/Overlays/OverlayPanelDisplayStyleControl.cs index 0ece96b56c..c2268ff43c 100644 --- a/osu.Game/Overlays/OverlayPanelDisplayStyleControl.cs +++ b/osu.Game/Overlays/OverlayPanelDisplayStyleControl.cs @@ -12,6 +12,9 @@ using osu.Framework.Allocation; using osuTK.Graphics; using osu.Framework.Graphics.Cursor; using osu.Framework.Localisation; +using System; +using osu.Game.Resources.Localisation.Web; +using osu.Framework.Extensions; namespace osu.Game.Overlays { @@ -57,7 +60,7 @@ namespace osu.Game.Overlays [Resolved] private OverlayColourProvider colourProvider { get; set; } - public LocalisableString TooltipText => $@"{Value} view"; + public LocalisableString TooltipText => Value.GetLocalisableDescription(); private readonly SpriteIcon icon; @@ -98,10 +101,32 @@ namespace osu.Game.Overlays } } + [LocalisableEnum(typeof(OverlayPanelDisplayStyleEnumLocalisationMapper))] public enum OverlayPanelDisplayStyle { Card, List, Brick } + + public class OverlayPanelDisplayStyleEnumLocalisationMapper : EnumLocalisationMapper + { + public override LocalisableString Map(OverlayPanelDisplayStyle value) + { + switch (value) + { + case OverlayPanelDisplayStyle.Card: + return UsersStrings.ViewModeCard; + + case OverlayPanelDisplayStyle.List: + return UsersStrings.ViewModeList; + + case OverlayPanelDisplayStyle.Brick: + return UsersStrings.ViewModeBrick; + + default: + throw new ArgumentOutOfRangeException(nameof(value), value, null); + } + } + } } diff --git a/osu.Game/Overlays/OverlayScrollContainer.cs b/osu.Game/Overlays/OverlayScrollContainer.cs index c5b4cc3645..ca5fc90027 100644 --- a/osu.Game/Overlays/OverlayScrollContainer.cs +++ b/osu.Game/Overlays/OverlayScrollContainer.cs @@ -12,6 +12,7 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; +using osu.Game.Resources.Localisation.Web; using osuTK; using osuTK.Graphics; @@ -118,7 +119,7 @@ namespace osu.Game.Overlays } }); - TooltipText = "Scroll to top"; + TooltipText = CommonStrings.ButtonsBackToTop; } [BackgroundDependencyLoader] diff --git a/osu.Game/Overlays/OverlayStreamItem.cs b/osu.Game/Overlays/OverlayStreamItem.cs index cd1391a3d8..56502ff70f 100644 --- a/osu.Game/Overlays/OverlayStreamItem.cs +++ b/osu.Game/Overlays/OverlayStreamItem.cs @@ -12,6 +12,7 @@ using osu.Framework.Allocation; using osu.Game.Graphics.Sprites; using osu.Game.Graphics; using osuTK.Graphics; +using osu.Framework.Localisation; namespace osu.Game.Overlays { @@ -88,11 +89,11 @@ namespace osu.Game.Overlays SelectedItem.BindValueChanged(_ => updateState(), true); } - protected abstract string MainText { get; } + protected abstract LocalisableString MainText { get; } - protected abstract string AdditionalText { get; } + protected abstract LocalisableString AdditionalText { get; } - protected virtual string InfoText => string.Empty; + protected virtual LocalisableString InfoText => string.Empty; protected abstract Color4 GetBarColour(OsuColour colours); diff --git a/osu.Game/Overlays/Wiki/Markdown/WikiMarkdownImageBlock.cs b/osu.Game/Overlays/Wiki/Markdown/WikiMarkdownImageBlock.cs index 179762103a..1a4f6087c7 100644 --- a/osu.Game/Overlays/Wiki/Markdown/WikiMarkdownImageBlock.cs +++ b/osu.Game/Overlays/Wiki/Markdown/WikiMarkdownImageBlock.cs @@ -6,6 +6,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers.Markdown; +using osu.Framework.Graphics.Sprites; using osuTK; namespace osu.Game.Overlays.Wiki.Markdown @@ -32,11 +33,7 @@ namespace osu.Game.Overlays.Wiki.Markdown { Children = new Drawable[] { - new WikiMarkdownImage(linkInline) - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - }, + new BlockMarkdownImage(linkInline), parentTextComponent.CreateSpriteText().With(t => { t.Text = linkInline.Title; @@ -45,5 +42,50 @@ namespace osu.Game.Overlays.Wiki.Markdown }), }; } + + private class BlockMarkdownImage : WikiMarkdownImage + { + public BlockMarkdownImage(LinkInline linkInline) + : base(linkInline) + { + AutoSizeAxes = Axes.Y; + RelativeSizeAxes = Axes.X; + } + + protected override ImageContainer CreateImageContainer(string url) => new BlockImageContainer(url); + + private class BlockImageContainer : ImageContainer + { + public BlockImageContainer(string url) + : base(url) + { + AutoSizeAxes = Axes.Y; + RelativeSizeAxes = Axes.X; + } + + protected override Sprite CreateImageSprite() => new ImageSprite(); + + private class ImageSprite : Sprite + { + public ImageSprite() + { + Anchor = Anchor.TopCentre; + Origin = Anchor.TopCentre; + } + + protected override void Update() + { + base.Update(); + + if (Width > Parent.DrawWidth) + { + float ratio = Height / Width; + Width = Parent.DrawWidth; + Height = ratio * Width; + } + } + } + } + } } } diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index 79d16013e3..6f00bb6c75 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -114,8 +114,8 @@ namespace osu.Game.Rulesets.Mods [JsonIgnore] public virtual bool UserPlayable => true; - [Obsolete("Going forward, the concept of \"ranked\" doesn't exist. The only exceptions are automation mods, which should now override and set UserPlayable to true.")] // Can be removed 20211009 - public virtual bool IsRanked => false; + [Obsolete("Going forward, the concept of \"ranked\" doesn't exist. The only exceptions are automation mods, which should now override and set UserPlayable to false.")] // Can be removed 20211009 + public virtual bool Ranked => false; /// /// Whether this mod requires configuration to apply changes to the game. diff --git a/osu.Game/Screens/OnlinePlay/Components/StarRatingRangeDisplay.cs b/osu.Game/Screens/OnlinePlay/Components/StarRatingRangeDisplay.cs new file mode 100644 index 0000000000..b2e35d7020 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Components/StarRatingRangeDisplay.cs @@ -0,0 +1,93 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Specialized; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Beatmaps; +using osu.Game.Graphics; +using osu.Game.Screens.Ranking.Expanded; +using osuTK; + +namespace osu.Game.Screens.OnlinePlay.Components +{ + public class StarRatingRangeDisplay : OnlinePlayComposite + { + [Resolved] + private OsuColour colours { get; set; } + + private StarRatingDisplay minDisplay; + private Drawable minBackground; + private StarRatingDisplay maxDisplay; + private Drawable maxBackground; + + public StarRatingRangeDisplay() + { + AutoSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChildren = new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + CornerRadius = 1, + Children = new[] + { + minBackground = new Box + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.5f), + }, + maxBackground = new Box + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.5f), + }, + } + }, + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + minDisplay = new StarRatingDisplay(default), + maxDisplay = new StarRatingDisplay(default) + } + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Playlist.BindCollectionChanged(updateRange, true); + } + + private void updateRange(object sender, NotifyCollectionChangedEventArgs e) + { + var orderedDifficulties = Playlist.Select(p => p.Beatmap.Value).OrderBy(b => b.StarDifficulty).ToArray(); + + StarDifficulty minDifficulty = new StarDifficulty(orderedDifficulties.Length > 0 ? orderedDifficulties[0].StarDifficulty : 0, 0); + StarDifficulty maxDifficulty = new StarDifficulty(orderedDifficulties.Length > 0 ? orderedDifficulties[^1].StarDifficulty : 0, 0); + + minDisplay.Current.Value = minDifficulty; + maxDisplay.Current.Value = maxDifficulty; + + minBackground.Colour = colours.ForDifficultyRating(minDifficulty.DifficultyRating, true); + maxBackground.Colour = colours.ForDifficultyRating(maxDifficulty.DifficultyRating, true); + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs index 8e59dc8579..5f135a3e90 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs @@ -93,7 +93,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components { bool matchingFilter = true; - matchingFilter &= r.Room.Playlist.Count == 0 || r.Room.Playlist.Any(i => i.Ruleset.Value.Equals(criteria.Ruleset)); + matchingFilter &= r.Room.Playlist.Count == 0 || criteria.Ruleset == null || r.Room.Playlist.Any(i => i.Ruleset.Value.Equals(criteria.Ruleset)); if (!string.IsNullOrEmpty(criteria.SearchString)) matchingFilter &= r.FilterTerms.Any(term => term.Contains(criteria.SearchString, StringComparison.InvariantCultureIgnoreCase)); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSpectatorPlayerClock.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSpectatorPlayerClock.cs index 9e1a020eca..20d12d62a3 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSpectatorPlayerClock.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSpectatorPlayerClock.cs @@ -34,7 +34,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate public void Stop() => IsRunning = false; - public bool Seek(double position) => true; + public bool Seek(double position) + { + CurrentTime = position; + return true; + } public void ResetSpeedAdjustments() { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSyncManager.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSyncManager.cs index efc12eaaa5..94278a47b6 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSyncManager.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSyncManager.cs @@ -1,8 +1,11 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Timing; @@ -28,16 +31,22 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate /// public const double MAXIMUM_START_DELAY = 15000; + public event Action ReadyToStart; + /// /// The master clock which is used to control the timing of all player clocks clocks. /// public IAdjustableClock MasterClock { get; } + public IBindable MasterState => masterState; + /// /// The player clocks. /// private readonly List playerClocks = new List(); + private readonly Bindable masterState = new Bindable(); + private bool hasStarted; private double? firstStartAttemptTime; @@ -46,7 +55,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate MasterClock = master; } - public void AddPlayerClock(ISpectatorPlayerClock clock) => playerClocks.Add(clock); + public void AddPlayerClock(ISpectatorPlayerClock clock) + { + Debug.Assert(!playerClocks.Contains(clock)); + playerClocks.Add(clock); + } public void RemovePlayerClock(ISpectatorPlayerClock clock) => playerClocks.Remove(clock); @@ -62,8 +75,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate return; } - updateCatchup(); - updateMasterClock(); + updatePlayerCatchup(); + updateMasterState(); } /// @@ -81,14 +94,20 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate int readyCount = playerClocks.Count(s => !s.WaitingOnFrames.Value); if (readyCount == playerClocks.Count) - return hasStarted = true; + return performStart(); if (readyCount > 0) { firstStartAttemptTime ??= Time.Current; if (Time.Current - firstStartAttemptTime > MAXIMUM_START_DELAY) - return hasStarted = true; + return performStart(); + } + + bool performStart() + { + ReadyToStart?.Invoke(); + return hasStarted = true; } return false; @@ -97,7 +116,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate /// /// Updates the catchup states of all player clocks clocks. /// - private void updateCatchup() + private void updatePlayerCatchup() { for (int i = 0; i < playerClocks.Count; i++) { @@ -135,19 +154,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate } /// - /// Updates the master clock's running state. + /// Updates the state of the master clock. /// - private void updateMasterClock() + private void updateMasterState() { bool anyInSync = playerClocks.Any(s => !s.IsCatchingUp); - - if (MasterClock.IsRunning != anyInSync) - { - if (anyInSync) - MasterClock.Start(); - else - MasterClock.Stop(); - } + masterState.Value = anyInSync ? MasterClockState.Synchronised : MasterClockState.TooFarAhead; } } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/ISyncManager.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/ISyncManager.cs index bd698108f6..3c644ccb78 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/ISyncManager.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/ISyncManager.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using osu.Framework.Bindables; using osu.Framework.Timing; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate @@ -10,11 +12,21 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate /// public interface ISyncManager { + /// + /// An event which is invoked when gameplay is ready to start. + /// + event Action ReadyToStart; + /// /// The master clock which player clocks should synchronise to. /// IAdjustableClock MasterClock { get; } + /// + /// An event which is invoked when the state of is changed. + /// + IBindable MasterState { get; } + /// /// Adds an to manage. /// diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MasterClockState.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MasterClockState.cs new file mode 100644 index 0000000000..8982d1669d --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MasterClockState.cs @@ -0,0 +1,18 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate +{ + public enum MasterClockState + { + /// + /// The master clock is synchronised with at least one player clock. + /// + Synchronised, + + /// + /// The master clock is too far ahead of any player clock and needs to slow down. + /// + TooFarAhead + } +} diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index 983daac909..2a2759e0dd 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -5,6 +5,7 @@ using System; using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Online.Multiplayer; @@ -42,6 +43,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate private PlayerGrid grid; private MultiSpectatorLeaderboard leaderboard; private PlayerArea currentAudioSource; + private bool canStartMasterClock; /// /// Creates a new . @@ -100,15 +102,24 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate Expanded = { Value = true }, Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - }, leaderboardContainer.Add); + }, l => + { + foreach (var instance in instances) + leaderboard.AddClock(instance.UserId, instance.GameplayClock); + + leaderboardContainer.Add(leaderboard); + }); } protected override void LoadComplete() { base.LoadComplete(); - masterClockContainer.Stop(); masterClockContainer.Reset(); + masterClockContainer.Stop(); + + syncManager.ReadyToStart += onReadyToStart; + syncManager.MasterState.BindValueChanged(onMasterStateChanged, true); } protected override void Update() @@ -129,19 +140,45 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate private bool isCandidateAudioSource([CanBeNull] ISpectatorPlayerClock clock) => clock?.IsRunning == true && !clock.IsCatchingUp && !clock.WaitingOnFrames.Value; + private void onReadyToStart() + { + // Seek the master clock to the gameplay time. + // This is chosen as the first available frame in the players' replays, which matches the seek by each individual SpectatorPlayer. + var startTime = instances.Where(i => i.Score != null) + .SelectMany(i => i.Score.Replay.Frames) + .Select(f => f.Time) + .DefaultIfEmpty(0) + .Min(); + + masterClockContainer.Seek(startTime); + masterClockContainer.Start(); + + // Although the clock has been started, this flag is set to allow for later synchronisation state changes to also be able to start it. + canStartMasterClock = true; + } + + private void onMasterStateChanged(ValueChangedEvent state) + { + switch (state.NewValue) + { + case MasterClockState.Synchronised: + if (canStartMasterClock) + masterClockContainer.Start(); + + break; + + case MasterClockState.TooFarAhead: + masterClockContainer.Stop(); + break; + } + } + protected override void OnUserStateChanged(int userId, SpectatorState spectatorState) { } protected override void StartGameplay(int userId, GameplayState gameplayState) - { - var instance = instances.Single(i => i.UserId == userId); - - instance.LoadScore(gameplayState.Score); - - syncManager.AddPlayerClock(instance.GameplayClock); - leaderboard.AddClock(instance.UserId, instance.GameplayClock); - } + => instances.Single(i => i.UserId == userId).LoadScore(gameplayState.Score); protected override void EndGameplay(int userId) { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs index fe79e5db72..95ccc08608 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs @@ -27,7 +27,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate /// /// Whether a is loaded in the area. /// - public bool PlayerLoaded => stack?.CurrentScreen is Player; + public bool PlayerLoaded => (stack?.CurrentScreen as Player)?.IsLoaded == true; /// /// The user id this corresponds to. diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 58f60d14cf..97854ee12f 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -768,6 +768,7 @@ namespace osu.Game.Screens.Play return false; HasFailed = true; + Score.ScoreInfo.Passed = false; // There is a chance that we could be in a paused state as the ruleset's internal clock (see FrameStabilityContainer) // could process an extra frame after the GameplayClock is stopped. @@ -950,6 +951,10 @@ namespace osu.Game.Screens.Play { screenSuspension?.Expire(); + // if arriving here and the results screen preparation task hasn't run, it's safe to say the user has not completed the beatmap. + if (prepareScoreForDisplayTask == null) + Score.ScoreInfo.Passed = false; + // EndPlaying() is typically called from ReplayRecorder.Dispose(). Disposal is currently asynchronous. // To resolve test failures, forcefully end playing synchronously when this screen exits. // Todo: Replace this with a more permanent solution once osu-framework has a synchronous cleanup method. diff --git a/osu.Game/Screens/Play/SoloPlayer.cs b/osu.Game/Screens/Play/SoloPlayer.cs index d0ef4131dc..ef1087dd62 100644 --- a/osu.Game/Screens/Play/SoloPlayer.cs +++ b/osu.Game/Screens/Play/SoloPlayer.cs @@ -12,6 +12,16 @@ namespace osu.Game.Screens.Play { public class SoloPlayer : SubmittingPlayer { + public SoloPlayer() + : this(null) + { + } + + protected SoloPlayer(PlayerConfiguration configuration = null) + : base(configuration) + { + } + protected override APIRequest CreateTokenRequest() { if (!(Beatmap.Value.BeatmapInfo.OnlineBeatmapID is int beatmapId)) @@ -27,9 +37,11 @@ namespace osu.Game.Screens.Play protected override APIRequest CreateSubmissionRequest(Score score, long token) { - Debug.Assert(Beatmap.Value.BeatmapInfo.OnlineBeatmapID != null); + var beatmap = score.ScoreInfo.Beatmap; - int beatmapId = Beatmap.Value.BeatmapInfo.OnlineBeatmapID.Value; + Debug.Assert(beatmap.OnlineBeatmapID != null); + + int beatmapId = beatmap.OnlineBeatmapID.Value; return new SubmitSoloScoreRequest(beatmapId, token, score.ScoreInfo); } diff --git a/osu.Game/Screens/Play/SpectatorPlayer.cs b/osu.Game/Screens/Play/SpectatorPlayer.cs index 0286b6b8a6..f662a479ec 100644 --- a/osu.Game/Screens/Play/SpectatorPlayer.cs +++ b/osu.Game/Screens/Play/SpectatorPlayer.cs @@ -47,8 +47,9 @@ namespace osu.Game.Screens.Play { base.StartGameplay(); + // Start gameplay along with the very first arrival frame (the latest one). + score.Replay.Frames.Clear(); spectatorClient.OnNewFrames += userSentFrames; - seekToGameplay(); } private void userSentFrames(int userId, FrameDataBundle bundle) @@ -62,6 +63,8 @@ namespace osu.Game.Screens.Play if (!this.IsCurrentScreen()) return; + bool isFirstBundle = score.Replay.Frames.Count == 0; + foreach (var frame in bundle.Frames) { IConvertibleReplayFrame convertibleFrame = GameplayRuleset.CreateConvertibleReplayFrame(); @@ -73,19 +76,8 @@ namespace osu.Game.Screens.Play score.Replay.Frames.Add(convertedFrame); } - seekToGameplay(); - } - - private bool seekedToGameplay; - - private void seekToGameplay() - { - if (seekedToGameplay || score.Replay.Frames.Count == 0) - return; - - NonFrameStableSeek(score.Replay.Frames[0].Time); - - seekedToGameplay = true; + if (isFirstBundle && score.Replay.Frames.Count > 0) + NonFrameStableSeek(score.Replay.Frames[0].Time); } protected override Score CreateScore() => score; diff --git a/osu.Game/Screens/Play/SubmittingPlayer.cs b/osu.Game/Screens/Play/SubmittingPlayer.cs index b843915a7c..7c5a06707d 100644 --- a/osu.Game/Screens/Play/SubmittingPlayer.cs +++ b/osu.Game/Screens/Play/SubmittingPlayer.cs @@ -27,6 +27,8 @@ namespace osu.Game.Screens.Play [Resolved] private IAPIProvider api { get; set; } + private TaskCompletionSource scoreSubmissionSource; + protected SubmittingPlayer(PlayerConfiguration configuration = null) : base(configuration) { @@ -106,27 +108,16 @@ namespace osu.Game.Screens.Play { await base.PrepareScoreForResultsAsync(score).ConfigureAwait(false); - // token may be null if the request failed but gameplay was still allowed (see HandleTokenRetrievalFailure). - if (token == null) - return; + await submitScore(score).ConfigureAwait(false); + } - var tcs = new TaskCompletionSource(); - var request = CreateSubmissionRequest(score, token.Value); + public override bool OnExiting(IScreen next) + { + var exiting = base.OnExiting(next); - request.Success += s => - { - score.ScoreInfo.OnlineScoreID = s.ID; - tcs.SetResult(true); - }; + submitScore(Score); - request.Failure += e => - { - Logger.Error(e, "Failed to submit score"); - tcs.SetResult(false); - }; - - api.Queue(request); - await tcs.Task.ConfigureAwait(false); + return exiting; } /// @@ -143,5 +134,33 @@ namespace osu.Game.Screens.Play /// The score to be submitted. /// The submission token. protected abstract APIRequest CreateSubmissionRequest(Score score, long token); + + private Task submitScore(Score score) + { + // token may be null if the request failed but gameplay was still allowed (see HandleTokenRetrievalFailure). + if (token == null) + return Task.CompletedTask; + + if (scoreSubmissionSource != null) + return scoreSubmissionSource.Task; + + scoreSubmissionSource = new TaskCompletionSource(); + var request = CreateSubmissionRequest(score, token.Value); + + request.Success += s => + { + score.ScoreInfo.OnlineScoreID = s.ID; + scoreSubmissionSource.SetResult(true); + }; + + request.Failure += e => + { + Logger.Error(e, "Failed to submit score"); + scoreSubmissionSource.SetResult(false); + }; + + api.Queue(request); + return scoreSubmissionSource.Task; + } } } diff --git a/osu.Game/Screens/Ranking/Expanded/StarRatingDisplay.cs b/osu.Game/Screens/Ranking/Expanded/StarRatingDisplay.cs index 7aba699216..e59a0de316 100644 --- a/osu.Game/Screens/Ranking/Expanded/StarRatingDisplay.cs +++ b/osu.Game/Screens/Ranking/Expanded/StarRatingDisplay.cs @@ -4,9 +4,7 @@ using System.Globalization; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; -using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; @@ -111,12 +109,9 @@ namespace osu.Game.Screens.Ranking.Expanded var rating = Current.Value.DifficultyRating; - background.Colour = rating == DifficultyRating.ExpertPlus - ? ColourInfo.GradientVertical(Color4Extensions.FromHex("#C1C1C1"), Color4Extensions.FromHex("#595959")) - : (ColourInfo)colours.ForDifficultyRating(rating); + background.Colour = colours.ForDifficultyRating(rating, true); textFlow.Clear(); - textFlow.AddText($"{wholePart}", s => { s.Colour = Color4.Black; diff --git a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs index 23f36ffe5b..7a35c8600d 100644 --- a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs +++ b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs @@ -127,6 +127,7 @@ namespace osu.Game.Skinning.Editor public override bool HandleFlip(Direction direction) { var selectionQuad = getSelectionQuad(); + Vector2 scaleFactor = direction == Direction.Horizontal ? new Vector2(-1, 1) : new Vector2(1, -1); foreach (var b in SelectedBlueprints) { @@ -136,10 +137,8 @@ namespace osu.Game.Skinning.Editor updateDrawablePosition(drawableItem, flippedPosition); - drawableItem.Scale *= new Vector2( - direction == Direction.Horizontal ? -1 : 1, - direction == Direction.Vertical ? -1 : 1 - ); + drawableItem.Scale *= scaleFactor; + drawableItem.Rotation -= drawableItem.Rotation % 180 * 2; } return true; diff --git a/osu.Game/Skinning/ResourceStoreBackedSkin.cs b/osu.Game/Skinning/ResourceStoreBackedSkin.cs new file mode 100644 index 0000000000..f041b82cf4 --- /dev/null +++ b/osu.Game/Skinning/ResourceStoreBackedSkin.cs @@ -0,0 +1,57 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable enable + +using System; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.OpenGL.Textures; +using osu.Framework.Graphics.Textures; +using osu.Framework.IO.Stores; +using osu.Framework.Platform; +using osu.Game.Audio; + +namespace osu.Game.Skinning +{ + /// + /// An that uses an underlying with namespaces for resources retrieval. + /// + public class ResourceStoreBackedSkin : ISkin, IDisposable + { + private readonly TextureStore textures; + private readonly ISampleStore samples; + + public ResourceStoreBackedSkin(IResourceStore resources, GameHost host, AudioManager audio) + { + textures = new TextureStore(host.CreateTextureLoaderStore(new NamespacedResourceStore(resources, @"Textures"))); + samples = audio.GetSampleStore(new NamespacedResourceStore(resources, @"Samples")); + } + + public Drawable? GetDrawableComponent(ISkinComponent component) => null; + + public Texture? GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => textures.Get(componentName, wrapModeS, wrapModeT); + + public ISample? GetSample(ISampleInfo sampleInfo) + { + foreach (var lookup in sampleInfo.LookupNames) + { + ISample? sample = samples.Get(lookup); + if (sample != null) + return sample; + } + + return null; + } + + public IBindable? GetConfig(TLookup lookup) => null; + + public void Dispose() + { + textures.Dispose(); + samples.Dispose(); + } + } +} diff --git a/osu.Game/Skinning/RulesetSkinProvidingContainer.cs b/osu.Game/Skinning/RulesetSkinProvidingContainer.cs index cb8b0fb3c8..19efc66814 100644 --- a/osu.Game/Skinning/RulesetSkinProvidingContainer.cs +++ b/osu.Game/Skinning/RulesetSkinProvidingContainer.cs @@ -1,10 +1,14 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; +using osu.Framework.Audio; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.IO.Stores; +using osu.Framework.Platform; using osu.Game.Beatmaps; using osu.Game.Rulesets; using osu.Game.Rulesets.UI; @@ -44,11 +48,16 @@ namespace osu.Game.Skinning private ISkinSource parentSource; + private ResourceStoreBackedSkin rulesetResourcesSkin; + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) { parentSource = parent.Get(); parentSource.SourceChanged += OnSourceChanged; + if (Ruleset.CreateResourceStore() is IResourceStore resources) + rulesetResourcesSkin = new ResourceStoreBackedSkin(resources, parent.Get(), parent.Get()); + // ensure sources are populated and ready for use before childrens' asynchronous load flow. UpdateSkinSources(); @@ -78,6 +87,16 @@ namespace osu.Game.Skinning break; } } + + int lastDefaultSkinIndex = SkinSources.IndexOf(SkinSources.OfType().LastOrDefault()); + + // Ruleset resources should be given the ability to override game-wide defaults + // This is achieved by placing them before the last instance of DefaultSkin. + // Note that DefaultSkin may not be present in some test scenes. + if (lastDefaultSkinIndex >= 0) + SkinSources.Insert(lastDefaultSkinIndex, rulesetResourcesSkin); + else + SkinSources.Add(rulesetResourcesSkin); } protected ISkin GetLegacyRulesetTransformedSkin(ISkin legacySkin) @@ -98,6 +117,8 @@ namespace osu.Game.Skinning if (parentSource != null) parentSource.SourceChanged -= OnSourceChanged; + + rulesetResourcesSkin?.Dispose(); } } } diff --git a/osu.Game/Tests/Visual/Multiplayer/IMultiplayerTestSceneDependencies.cs b/osu.Game/Tests/Visual/Multiplayer/IMultiplayerTestSceneDependencies.cs new file mode 100644 index 0000000000..204c189591 --- /dev/null +++ b/osu.Game/Tests/Visual/Multiplayer/IMultiplayerTestSceneDependencies.cs @@ -0,0 +1,37 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Database; +using osu.Game.Online.Multiplayer; +using osu.Game.Screens.OnlinePlay; +using osu.Game.Tests.Visual.OnlinePlay; +using osu.Game.Tests.Visual.Spectator; + +namespace osu.Game.Tests.Visual.Multiplayer +{ + /// + /// Interface that defines the dependencies required for multiplayer test scenes. + /// + public interface IMultiplayerTestSceneDependencies : IOnlinePlayTestSceneDependencies + { + /// + /// The cached . + /// + TestMultiplayerClient Client { get; } + + /// + /// The cached . + /// + new TestMultiplayerRoomManager RoomManager { get; } + + /// + /// The cached . + /// + TestUserLookupCache LookupCache { get; } + + /// + /// The cached . + /// + TestSpectatorClient SpectatorClient { get; } + } +} diff --git a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs index c76d1053b2..b7d3793ab1 100644 --- a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs +++ b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs @@ -2,66 +2,55 @@ // See the LICENCE file in the repository root for full licence text. using NUnit.Framework; -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; -using osu.Game.Screens.OnlinePlay; -using osu.Game.Screens.OnlinePlay.Lounge.Components; using osu.Game.Tests.Beatmaps; +using osu.Game.Tests.Visual.OnlinePlay; +using osu.Game.Tests.Visual.Spectator; namespace osu.Game.Tests.Visual.Multiplayer { - public abstract class MultiplayerTestScene : RoomTestScene + /// + /// The base test scene for all multiplayer components and screens. + /// + public abstract class MultiplayerTestScene : OnlinePlayTestScene, IMultiplayerTestSceneDependencies { public const int PLAYER_1_ID = 55; public const int PLAYER_2_ID = 56; - [Cached(typeof(MultiplayerClient))] - public TestMultiplayerClient Client { get; } + public TestMultiplayerClient Client => OnlinePlayDependencies.Client; + public new TestMultiplayerRoomManager RoomManager => OnlinePlayDependencies.RoomManager; + public TestUserLookupCache LookupCache => OnlinePlayDependencies?.LookupCache; + public TestSpectatorClient SpectatorClient => OnlinePlayDependencies?.SpectatorClient; - [Cached(typeof(IRoomManager))] - public TestMultiplayerRoomManager RoomManager { get; } - - [Cached] - public Bindable Filter { get; } - - [Cached] - public OngoingOperationTracker OngoingOperationTracker { get; } - - protected override Container Content => content; - private readonly TestMultiplayerRoomContainer content; + protected new MultiplayerTestSceneDependencies OnlinePlayDependencies => (MultiplayerTestSceneDependencies)base.OnlinePlayDependencies; private readonly bool joinRoom; protected MultiplayerTestScene(bool joinRoom = true) { this.joinRoom = joinRoom; - base.Content.Add(content = new TestMultiplayerRoomContainer { RelativeSizeAxes = Axes.Both }); - - Client = content.Client; - RoomManager = content.RoomManager; - Filter = content.Filter; - OngoingOperationTracker = content.OngoingOperationTracker; } [SetUp] public new void Setup() => Schedule(() => { - RoomManager.Schedule(() => RoomManager.PartRoom()); - if (joinRoom) { - Room.Name.Value = "test name"; - Room.Playlist.Add(new PlaylistItem + var room = new Room { - Beatmap = { Value = new TestBeatmap(Ruleset.Value).BeatmapInfo }, - Ruleset = { Value = Ruleset.Value } - }); + Name = { Value = "test name" }, + Playlist = + { + new PlaylistItem + { + Beatmap = { Value = new TestBeatmap(Ruleset.Value).BeatmapInfo }, + Ruleset = { Value = Ruleset.Value } + } + } + }; - RoomManager.Schedule(() => RoomManager.CreateRoom(Room)); + RoomManager.CreateRoom(room); + SelectedRoom.Value = room; } }); @@ -72,5 +61,7 @@ namespace osu.Game.Tests.Visual.Multiplayer if (joinRoom) AddUntilStep("wait for room join", () => Client.Room != null); } + + protected override OnlinePlayTestSceneDependencies CreateOnlinePlayDependencies() => new MultiplayerTestSceneDependencies(); } } diff --git a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestSceneDependencies.cs b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestSceneDependencies.cs new file mode 100644 index 0000000000..a2b0b066a7 --- /dev/null +++ b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestSceneDependencies.cs @@ -0,0 +1,38 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Database; +using osu.Game.Online.Multiplayer; +using osu.Game.Online.Spectator; +using osu.Game.Screens.OnlinePlay; +using osu.Game.Tests.Visual.OnlinePlay; +using osu.Game.Tests.Visual.Spectator; + +namespace osu.Game.Tests.Visual.Multiplayer +{ + /// + /// Contains the basic dependencies of multiplayer test scenes. + /// + public class MultiplayerTestSceneDependencies : OnlinePlayTestSceneDependencies, IMultiplayerTestSceneDependencies + { + public TestMultiplayerClient Client { get; } + public TestUserLookupCache LookupCache { get; } + public TestSpectatorClient SpectatorClient { get; } + public new TestMultiplayerRoomManager RoomManager => (TestMultiplayerRoomManager)base.RoomManager; + + public MultiplayerTestSceneDependencies() + { + Client = new TestMultiplayerClient(RoomManager); + LookupCache = new TestUserLookupCache(); + SpectatorClient = CreateSpectatorClient(); + + CacheAs(Client); + CacheAs(LookupCache); + CacheAs(SpectatorClient); + } + + protected override IRoomManager CreateRoomManager() => new TestMultiplayerRoomManager(); + + protected virtual TestSpectatorClient CreateSpectatorClient() => new TestSpectatorClient(); + } +} diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index b12bd8091d..b0c8d6d19b 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -20,6 +20,9 @@ using osu.Game.Users; namespace osu.Game.Tests.Visual.Multiplayer { + /// + /// A for use in multiplayer test scenes. Should generally not be used by itself outside of a . + /// public class TestMultiplayerClient : MultiplayerClient { public override IBindable IsConnected => isConnected; diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomContainer.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomContainer.cs deleted file mode 100644 index 1abf4d8f5d..0000000000 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomContainer.cs +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Game.Online.Multiplayer; -using osu.Game.Screens.OnlinePlay; -using osu.Game.Screens.OnlinePlay.Lounge.Components; - -namespace osu.Game.Tests.Visual.Multiplayer -{ - public class TestMultiplayerRoomContainer : Container - { - protected override Container Content => content; - private readonly Container content; - - [Cached(typeof(MultiplayerClient))] - public readonly TestMultiplayerClient Client; - - [Cached(typeof(IRoomManager))] - public readonly TestMultiplayerRoomManager RoomManager; - - [Cached] - public readonly Bindable Filter = new Bindable(new FilterCriteria()); - - [Cached] - public readonly OngoingOperationTracker OngoingOperationTracker; - - public TestMultiplayerRoomContainer() - { - RelativeSizeAxes = Axes.Both; - - RoomManager = new TestMultiplayerRoomManager(); - Client = new TestMultiplayerClient(RoomManager); - OngoingOperationTracker = new OngoingOperationTracker(); - - AddRangeInternal(new Drawable[] - { - Client, - RoomManager, - OngoingOperationTracker, - content = new Container { RelativeSizeAxes = Axes.Both } - }); - } - } -} diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs index 315be510a3..5d66cdba02 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs @@ -11,11 +11,15 @@ using osu.Game.Online.API.Requests; using osu.Game.Online.Rooms; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; +using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Lounge.Components; using osu.Game.Screens.OnlinePlay.Multiplayer; namespace osu.Game.Tests.Visual.Multiplayer { + /// + /// A for use in multiplayer test scenes. Should generally not be used by itself outside of a . + /// public class TestMultiplayerRoomManager : MultiplayerRoomManager { [Resolved] @@ -29,10 +33,9 @@ namespace osu.Game.Tests.Visual.Multiplayer public new readonly List Rooms = new List(); - protected override void LoadComplete() + [BackgroundDependencyLoader] + private void load() { - base.LoadComplete(); - int currentScoreId = 0; int currentRoomId = 0; int currentPlaylistItemId = 0; diff --git a/osu.Game/Tests/Visual/OnlinePlay/BasicTestRoomManager.cs b/osu.Game/Tests/Visual/OnlinePlay/BasicTestRoomManager.cs new file mode 100644 index 0000000000..813e617ac5 --- /dev/null +++ b/osu.Game/Tests/Visual/OnlinePlay/BasicTestRoomManager.cs @@ -0,0 +1,78 @@ +// 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.Game.Beatmaps; +using osu.Game.Online.Rooms; +using osu.Game.Rulesets; +using osu.Game.Screens.OnlinePlay; +using osu.Game.Screens.OnlinePlay.Components; +using osu.Game.Users; + +namespace osu.Game.Tests.Visual.OnlinePlay +{ + /// + /// A very simple for use in online play test scenes. + /// + public class BasicTestRoomManager : IRoomManager + { + public event Action RoomsUpdated + { + add { } + remove { } + } + + public readonly BindableList Rooms = new BindableList(); + + public IBindable InitialRoomsReceived { get; } = new Bindable(true); + + IBindableList IRoomManager.Rooms => Rooms; + + public void CreateRoom(Room room, Action onSuccess = null, Action onError = null) + { + room.RoomID.Value ??= Rooms.Select(r => r.RoomID.Value).Where(id => id != null).Select(id => id.Value).DefaultIfEmpty().Max() + 1; + Rooms.Add(room); + onSuccess?.Invoke(room); + } + + public void JoinRoom(Room room, Action onSuccess = null, Action onError = null) => onSuccess?.Invoke(room); + + public void PartRoom() + { + } + + public void AddRooms(int count, RulesetInfo ruleset = null) + { + for (int i = 0; i < count; i++) + { + var room = new Room + { + RoomID = { Value = i }, + Name = { Value = $"Room {i}" }, + Host = { Value = new User { Username = "Host" } }, + EndDate = { Value = DateTimeOffset.Now + TimeSpan.FromSeconds(10) }, + Category = { Value = i % 2 == 0 ? RoomCategory.Spotlight : RoomCategory.Normal } + }; + + if (ruleset != null) + { + room.Playlist.Add(new PlaylistItem + { + Ruleset = { Value = ruleset }, + Beatmap = + { + Value = new BeatmapInfo + { + Metadata = new BeatmapMetadata() + } + } + }); + } + + CreateRoom(room); + } + } + } +} diff --git a/osu.Game/Tests/Visual/OnlinePlay/IOnlinePlayTestSceneDependencies.cs b/osu.Game/Tests/Visual/OnlinePlay/IOnlinePlayTestSceneDependencies.cs new file mode 100644 index 0000000000..6e1e831d9b --- /dev/null +++ b/osu.Game/Tests/Visual/OnlinePlay/IOnlinePlayTestSceneDependencies.cs @@ -0,0 +1,41 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Bindables; +using osu.Game.Online.Rooms; +using osu.Game.Screens.OnlinePlay; +using osu.Game.Screens.OnlinePlay.Lounge.Components; + +namespace osu.Game.Tests.Visual.OnlinePlay +{ + /// + /// Interface that defines the dependencies required for online play test scenes. + /// + public interface IOnlinePlayTestSceneDependencies + { + /// + /// The cached . + /// + Bindable SelectedRoom { get; } + + /// + /// The cached + /// + IRoomManager RoomManager { get; } + + /// + /// The cached . + /// + Bindable Filter { get; } + + /// + /// The cached . + /// + OngoingOperationTracker OngoingOperationTracker { get; } + + /// + /// The cached . + /// + OnlinePlayBeatmapAvailabilityTracker AvailabilityTracker { get; } + } +} diff --git a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs new file mode 100644 index 0000000000..997c910dd4 --- /dev/null +++ b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs @@ -0,0 +1,104 @@ +// 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 NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Online.Rooms; +using osu.Game.Screens.OnlinePlay; +using osu.Game.Screens.OnlinePlay.Lounge.Components; + +namespace osu.Game.Tests.Visual.OnlinePlay +{ + /// + /// A base test scene for all online play components and screens. + /// + public abstract class OnlinePlayTestScene : ScreenTestScene, IOnlinePlayTestSceneDependencies + { + public Bindable SelectedRoom => OnlinePlayDependencies?.SelectedRoom; + public IRoomManager RoomManager => OnlinePlayDependencies?.RoomManager; + public Bindable Filter => OnlinePlayDependencies?.Filter; + public OngoingOperationTracker OngoingOperationTracker => OnlinePlayDependencies?.OngoingOperationTracker; + public OnlinePlayBeatmapAvailabilityTracker AvailabilityTracker => OnlinePlayDependencies?.AvailabilityTracker; + + /// + /// All dependencies required for online play components and screens. + /// + protected OnlinePlayTestSceneDependencies OnlinePlayDependencies => dependencies?.OnlinePlayDependencies; + + private DelegatedDependencyContainer dependencies; + + protected override Container Content => content; + private readonly Container content; + private readonly Container drawableDependenciesContainer; + + protected OnlinePlayTestScene() + { + base.Content.AddRange(new Drawable[] + { + drawableDependenciesContainer = new Container { RelativeSizeAxes = Axes.Both }, + content = new Container { RelativeSizeAxes = Axes.Both }, + }); + } + + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) + { + dependencies = new DelegatedDependencyContainer(base.CreateChildDependencies(parent)); + return dependencies; + } + + [SetUp] + public void Setup() => Schedule(() => + { + // Reset the room dependencies to a fresh state. + drawableDependenciesContainer.Clear(); + dependencies.OnlinePlayDependencies = CreateOnlinePlayDependencies(); + drawableDependenciesContainer.AddRange(OnlinePlayDependencies.DrawableComponents); + }); + + /// + /// Creates the room dependencies. Called every . + /// + /// + /// Any custom dependencies required for online play sub-classes should be added here. + /// + protected virtual OnlinePlayTestSceneDependencies CreateOnlinePlayDependencies() => new OnlinePlayTestSceneDependencies(); + + /// + /// A providing a mutable lookup source for online play dependencies. + /// + private class DelegatedDependencyContainer : IReadOnlyDependencyContainer + { + /// + /// The online play dependencies. + /// + public OnlinePlayTestSceneDependencies OnlinePlayDependencies { get; set; } + + private readonly IReadOnlyDependencyContainer parent; + private readonly DependencyContainer injectableDependencies; + + /// + /// Creates a new . + /// + /// The fallback to use when cannot satisfy a dependency. + public DelegatedDependencyContainer(IReadOnlyDependencyContainer parent) + { + this.parent = parent; + injectableDependencies = new DependencyContainer(this); + } + + public object Get(Type type) + => OnlinePlayDependencies?.Get(type) ?? parent.Get(type); + + public object Get(Type type, CacheInfo info) + => OnlinePlayDependencies?.Get(type, info) ?? parent.Get(type, info); + + public void Inject(T instance) + where T : class + => injectableDependencies.Inject(instance); + } + } +} diff --git a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs new file mode 100644 index 0000000000..ddbbfe501b --- /dev/null +++ b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs @@ -0,0 +1,78 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Game.Online.Rooms; +using osu.Game.Screens.OnlinePlay; +using osu.Game.Screens.OnlinePlay.Lounge.Components; + +namespace osu.Game.Tests.Visual.OnlinePlay +{ + /// + /// Contains the basic dependencies of online play test scenes. + /// + public class OnlinePlayTestSceneDependencies : IReadOnlyDependencyContainer, IOnlinePlayTestSceneDependencies + { + public Bindable SelectedRoom { get; } + public IRoomManager RoomManager { get; } + public Bindable Filter { get; } + public OngoingOperationTracker OngoingOperationTracker { get; } + public OnlinePlayBeatmapAvailabilityTracker AvailabilityTracker { get; } + + /// + /// All cached dependencies which are also components. + /// + public IReadOnlyList DrawableComponents => drawableComponents; + + private readonly List drawableComponents = new List(); + private readonly DependencyContainer dependencies; + + public OnlinePlayTestSceneDependencies() + { + SelectedRoom = new Bindable(); + RoomManager = CreateRoomManager(); + Filter = new Bindable(new FilterCriteria()); + OngoingOperationTracker = new OngoingOperationTracker(); + AvailabilityTracker = new OnlinePlayBeatmapAvailabilityTracker(); + + dependencies = new DependencyContainer(new CachedModelDependencyContainer(null) { Model = { BindTarget = SelectedRoom } }); + + CacheAs(SelectedRoom); + CacheAs(RoomManager); + CacheAs(Filter); + CacheAs(OngoingOperationTracker); + CacheAs(AvailabilityTracker); + } + + public object Get(Type type) + => dependencies.Get(type); + + public object Get(Type type, CacheInfo info) + => dependencies.Get(type, info); + + public void Inject(T instance) + where T : class + => dependencies.Inject(instance); + + protected void Cache(object instance) + { + dependencies.Cache(instance); + if (instance is Drawable drawable) + drawableComponents.Add(drawable); + } + + protected void CacheAs(T instance) + where T : class + { + dependencies.CacheAs(instance); + if (instance is Drawable drawable) + drawableComponents.Add(drawable); + } + + protected virtual IRoomManager CreateRoomManager() => new BasicTestRoomManager(); + } +} diff --git a/osu.Game/Tests/Visual/RoomTestScene.cs b/osu.Game/Tests/Visual/RoomTestScene.cs deleted file mode 100644 index aaf5c7624f..0000000000 --- a/osu.Game/Tests/Visual/RoomTestScene.cs +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using NUnit.Framework; -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Game.Online.Rooms; - -namespace osu.Game.Tests.Visual -{ - public abstract class RoomTestScene : ScreenTestScene - { - [Cached] - private readonly Bindable currentRoom = new Bindable(); - - protected Room Room => currentRoom.Value; - - private CachedModelDependencyContainer dependencies; - - protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) - { - dependencies = new CachedModelDependencyContainer(base.CreateChildDependencies(parent)); - dependencies.Model.BindTo(currentRoom); - return dependencies; - } - - [SetUp] - public void Setup() => Schedule(() => - { - currentRoom.Value = new Room(); - }); - } -} diff --git a/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs b/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs index c7aa43b377..f206d4f8b0 100644 --- a/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs +++ b/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs @@ -20,9 +20,15 @@ namespace osu.Game.Tests.Visual.Spectator { public class TestSpectatorClient : SpectatorClient { + /// + /// Maximum number of frames sent per bundle via . + /// + public const int FRAME_BUNDLE_SIZE = 10; + public override IBindable IsConnected { get; } = new Bindable(true); private readonly Dictionary userBeatmapDictionary = new Dictionary(); + private readonly Dictionary userNextFrameDictionary = new Dictionary(); [Resolved] private IAPIProvider api { get; set; } = null!; @@ -35,6 +41,7 @@ namespace osu.Game.Tests.Visual.Spectator public void StartPlay(int userId, int beatmapId) { userBeatmapDictionary[userId] = beatmapId; + userNextFrameDictionary[userId] = 0; sendPlayingState(userId); } @@ -57,24 +64,41 @@ namespace osu.Game.Tests.Visual.Spectator public new void Schedule(Action action) => base.Schedule(action); /// - /// Sends frames for an arbitrary user. + /// Sends frames for an arbitrary user, in bundles containing 10 frames each. /// /// The user to send frames for. - /// The frame index. - /// The number of frames to send. - public void SendFrames(int userId, int index, int count) + /// The total number of frames to send. + public void SendFrames(int userId, int count) { var frames = new List(); - for (int i = index; i < index + count; i++) - { - var buttonState = i == index + count - 1 ? ReplayButtonState.None : ReplayButtonState.Left1; + int currentFrameIndex = userNextFrameDictionary[userId]; + int lastFrameIndex = currentFrameIndex + count - 1; - frames.Add(new LegacyReplayFrame(i * 100, RNG.Next(0, 512), RNG.Next(0, 512), buttonState)); + for (; currentFrameIndex <= lastFrameIndex; currentFrameIndex++) + { + // This is done in the next frame so that currentFrameIndex is updated to the correct value. + if (frames.Count == FRAME_BUNDLE_SIZE) + flush(); + + var buttonState = currentFrameIndex == lastFrameIndex ? ReplayButtonState.None : ReplayButtonState.Left1; + frames.Add(new LegacyReplayFrame(currentFrameIndex * 100, RNG.Next(0, 512), RNG.Next(0, 512), buttonState)); } - var bundle = new FrameDataBundle(new ScoreInfo { Combo = index + count }, frames); - ((ISpectatorClient)this).UserSentFrames(userId, bundle); + flush(); + + userNextFrameDictionary[userId] = currentFrameIndex; + + void flush() + { + if (frames.Count == 0) + return; + + var bundle = new FrameDataBundle(new ScoreInfo { Combo = currentFrameIndex }, frames.ToArray()); + ((ISpectatorClient)this).UserSentFrames(userId, bundle); + + frames.Clear(); + } } protected override Task BeginPlayingInternal(SpectatorState state) diff --git a/osu.Game/Tests/Visual/TestPlayer.cs b/osu.Game/Tests/Visual/TestPlayer.cs index 09da4db952..5e5f20b307 100644 --- a/osu.Game/Tests/Visual/TestPlayer.cs +++ b/osu.Game/Tests/Visual/TestPlayer.cs @@ -1,14 +1,18 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Game.Online.API; +using osu.Game.Online.Rooms; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; +using osu.Game.Scoring; using osu.Game.Screens.Play; namespace osu.Game.Tests.Visual @@ -16,7 +20,7 @@ namespace osu.Game.Tests.Visual /// /// A player that exposes many components that would otherwise not be available, for testing purposes. /// - public class TestPlayer : Player + public class TestPlayer : SoloPlayer { protected override bool PauseOnFocusLost { get; } @@ -35,6 +39,10 @@ namespace osu.Game.Tests.Visual public new HealthProcessor HealthProcessor => base.HealthProcessor; + public bool TokenCreationRequested { get; private set; } + + public Score SubmittedScore { get; private set; } + public new bool PauseCooldownActive => base.PauseCooldownActive; public readonly List Results = new List(); @@ -49,6 +57,20 @@ namespace osu.Game.Tests.Visual PauseOnFocusLost = pauseOnFocusLost; } + protected override bool HandleTokenRetrievalFailure(Exception exception) => false; + + protected override APIRequest CreateTokenRequest() + { + TokenCreationRequested = true; + return base.CreateTokenRequest(); + } + + protected override APIRequest CreateSubmissionRequest(Score score, long token) + { + SubmittedScore = score; + return base.CreateSubmissionRequest(score, token); + } + protected override void PrepareReplay() { // Generally, replay generation is handled by whatever is constructing the player. diff --git a/osu.Game/Tests/Visual/TestUserLookupCache.cs b/osu.Game/Tests/Visual/TestUserLookupCache.cs new file mode 100644 index 0000000000..d2941b5bd5 --- /dev/null +++ b/osu.Game/Tests/Visual/TestUserLookupCache.cs @@ -0,0 +1,19 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Threading; +using System.Threading.Tasks; +using osu.Game.Database; +using osu.Game.Users; + +namespace osu.Game.Tests.Visual +{ + public class TestUserLookupCache : UserLookupCache + { + protected override Task ComputeValueAsync(int lookup, CancellationToken token = default) => Task.FromResult(new User + { + Id = lookup, + Username = $"User {lookup}" + }); + } +} diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index f047859dbb..14efcd516e 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -20,26 +20,26 @@ - - + + - - - + + + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + - - + + diff --git a/osu.iOS.props b/osu.iOS.props index 304047ad12..0ab0b26430 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -89,16 +89,16 @@ - + - - + + - + diff --git a/osu.sln.DotSettings b/osu.sln.DotSettings index 62751cebb1..d2c5b1223c 100644 --- a/osu.sln.DotSettings +++ b/osu.sln.DotSettings @@ -308,6 +308,7 @@ GL GLSL HID + HSV HTML HUD ID