diff --git a/osu.Android.props b/osu.Android.props index 2a08cb7867..fc01f9bf1d 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,6 +52,6 @@ - + diff --git a/osu.Android/OsuGameActivity.cs b/osu.Android/OsuGameActivity.cs index bf73f33b74..c397608bc6 100644 --- a/osu.Android/OsuGameActivity.cs +++ b/osu.Android/OsuGameActivity.cs @@ -17,10 +17,13 @@ using osu.Game.Database; namespace osu.Android { - [Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullUser, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false)] + [Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullUser, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false, LaunchMode = LaunchMode.SingleInstance)] [IntentFilter(new[] { Intent.ActionDefault, Intent.ActionSend, Intent.ActionSendMultiple }, Categories = new[] { Intent.CategoryDefault }, DataPathPatterns = new[] { ".*\\.osz", ".*\\.osk" }, DataMimeType = "application/*")] + [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryBrowsable, Intent.CategoryDefault }, DataSchemes = new[] { "osu", "osump" })] public class OsuGameActivity : AndroidGameActivity { + private static readonly string[] osu_url_schemes = { "osu", "osump" }; + private OsuGameAndroid game; protected override Framework.Game CreateGame() => game = new OsuGameAndroid(this); @@ -51,7 +54,9 @@ namespace osu.Android { case Intent.ActionDefault: if (intent.Scheme == ContentResolver.SchemeContent) - handleImportFromUris(intent.Data); + handleImportFromUri(intent.Data); + else if (osu_url_schemes.Contains(intent.Scheme)) + game.HandleLink(intent.DataString); break; case Intent.ActionSend: diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs index 5cb1519196..596430f9e5 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs @@ -355,7 +355,11 @@ namespace osu.Game.Rulesets.Mania.Tests protected override bool PauseOnFocusLost => false; public ScoreAccessibleReplayPlayer(Score score) - : base(score, false, false) + : base(score, new PlayerConfiguration + { + AllowPause = false, + ShowResults = false, + }) { } } diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs index cecac38f70..18891f8c58 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs @@ -176,7 +176,11 @@ namespace osu.Game.Rulesets.Mania.Tests protected override bool PauseOnFocusLost => false; public ScoreAccessibleReplayPlayer(Score score) - : base(score, false, false) + : base(score, new PlayerConfiguration + { + AllowPause = false, + ShowResults = false, + }) { } } diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaJudgementPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaJudgementPiece.cs index 9684cbb167..5d662c18d3 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaJudgementPiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaJudgementPiece.cs @@ -5,11 +5,11 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Animations; using osu.Framework.Graphics.Containers; +using osu.Framework.Utils; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Scoring; using osu.Game.Skinning; -using osuTK; namespace osu.Game.Rulesets.Mania.Skinning.Legacy { @@ -56,31 +56,30 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy (animation as IFramedAnimation)?.GotoFrame(0); + this.FadeInFromZero(20, Easing.Out) + .Then().Delay(160) + .FadeOutFromOne(40, Easing.In); + switch (result) { case HitResult.None: break; case HitResult.Miss: - animation.ScaleTo(1.6f); - animation.ScaleTo(1, 100, Easing.In); - - animation.MoveTo(Vector2.Zero); - animation.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint); + animation.ScaleTo(1.2f).Then().ScaleTo(1, 100, Easing.Out); animation.RotateTo(0); - animation.RotateTo(40, 800, Easing.InQuint); - - this.FadeOutFromOne(800); + animation.RotateTo(RNG.NextSingle(-5.73f, 5.73f), 100, Easing.Out); break; default: - animation.ScaleTo(0.8f); - animation.ScaleTo(1, 250, Easing.OutElastic); - - animation.Delay(50).ScaleTo(0.75f, 250); - - this.Delay(50).FadeOut(200); + animation.ScaleTo(0.8f) + .Then().ScaleTo(1, 40) + // this is actually correct to match stable; there were overlapping transforms. + .Then().ScaleTo(0.85f) + .Then().ScaleTo(0.7f, 40) + .Then().Delay(100) + .Then().ScaleTo(0.4f, 40, Easing.In); break; } } diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyBeatmapSkin.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyBeatmapSkin.cs index 3ff37c4147..a768626005 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyBeatmapSkin.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyBeatmapSkin.cs @@ -74,7 +74,11 @@ namespace osu.Game.Rulesets.Osu.Tests private readonly bool userHasCustomColours; public ExposedPlayer(bool userHasCustomColours) - : base(false, false) + : base(new PlayerConfiguration + { + AllowPause = false, + ShowResults = false, + }) { this.userHasCustomColours = userHasCustomColours; } diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs index 32a36ab317..296b421a11 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs @@ -439,7 +439,11 @@ namespace osu.Game.Rulesets.Osu.Tests protected override bool PauseOnFocusLost => false; public ScoreAccessibleReplayPlayer(Score score) - : base(score, false, false) + : base(score, new PlayerConfiguration + { + AllowPause = false, + ShowResults = false, + }) { } } diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs index 0164fb8bf4..2cc031405e 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs @@ -378,7 +378,11 @@ namespace osu.Game.Rulesets.Osu.Tests protected override bool PauseOnFocusLost => false; public ScoreAccessibleReplayPlayer(Score score) - : base(score, false, false) + : base(score, new PlayerConfiguration + { + AllowPause = false, + ShowResults = false, + }) { } } diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index ec8c68005f..660e1844aa 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -157,10 +157,16 @@ namespace osu.Game.Rulesets.Osu.Edit foreach (var h in hitObjects) { - h.Position = new Vector2( - quad.TopLeft.X + (h.X - quad.TopLeft.X) / quad.Width * (quad.Width + scale.X), - quad.TopLeft.Y + (h.Y - quad.TopLeft.Y) / quad.Height * (quad.Height + scale.Y) - ); + var newPosition = h.Position; + + // guard against no-ops and NaN. + if (scale.X != 0 && quad.Width > 0) + newPosition.X = quad.TopLeft.X + (h.X - quad.TopLeft.X) / quad.Width * (quad.Width + scale.X); + + if (scale.Y != 0 && quad.Height > 0) + newPosition.Y = quad.TopLeft.Y + (h.Y - quad.TopLeft.Y) / quad.Height * (quad.Height + scale.Y); + + h.Position = newPosition; } } diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs b/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs index ac20407ed2..20c0818d03 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs @@ -56,7 +56,7 @@ namespace osu.Game.Rulesets.Osu.Mods var destination = e.MousePosition; FlashlightPosition = Interpolation.ValueAt( - Math.Clamp(Clock.ElapsedFrameTime, 0, follow_delay), position, destination, 0, follow_delay, Easing.Out); + Math.Min(Math.Abs(Clock.ElapsedFrameTime), follow_delay), position, destination, 0, follow_delay, Easing.Out); return base.OnMouseMove(e); } diff --git a/osu.Game.Rulesets.Taiko.Tests/HitObjectApplicationTestScene.cs b/osu.Game.Rulesets.Taiko.Tests/HitObjectApplicationTestScene.cs index 07c7b4d1db..a1d000386f 100644 --- a/osu.Game.Rulesets.Taiko.Tests/HitObjectApplicationTestScene.cs +++ b/osu.Game.Rulesets.Taiko.Tests/HitObjectApplicationTestScene.cs @@ -25,16 +25,22 @@ namespace osu.Game.Rulesets.Taiko.Tests private ScrollingHitObjectContainer hitObjectContainer; - [SetUpSteps] - public void SetUp() - => AddStep("create SHOC", () => Child = hitObjectContainer = new ScrollingHitObjectContainer + [BackgroundDependencyLoader] + private void load() + { + Child = hitObjectContainer = new ScrollingHitObjectContainer { RelativeSizeAxes = Axes.X, Height = 200, Anchor = Anchor.Centre, Origin = Anchor.Centre, Clock = new FramedClock(new StopwatchClock()) - }); + }; + } + + [SetUpSteps] + public void SetUp() + => AddStep("clear SHOC", () => hitObjectContainer.Clear(false)); protected void AddHitObject(DrawableHitObject hitObject) => AddStep("add to SHOC", () => hitObjectContainer.Add(hitObject)); diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneBarLineApplication.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneBarLineApplication.cs index 65230a07bc..a970965141 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneBarLineApplication.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneBarLineApplication.cs @@ -12,12 +12,13 @@ namespace osu.Game.Rulesets.Taiko.Tests [Test] public void TestApplyNewBarLine() { - DrawableBarLine barLine = new DrawableBarLine(PrepareObject(new BarLine + DrawableBarLine barLine = new DrawableBarLine(); + + AddStep("apply new bar line", () => barLine.Apply(PrepareObject(new BarLine { StartTime = 400, Major = true - })); - + }), null)); AddHitObject(barLine); RemoveHitObject(barLine); diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumRollApplication.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumRollApplication.cs new file mode 100644 index 0000000000..54450e27db --- /dev/null +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumRollApplication.cs @@ -0,0 +1,39 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.Objects.Drawables; + +namespace osu.Game.Rulesets.Taiko.Tests +{ + public class TestSceneDrumRollApplication : HitObjectApplicationTestScene + { + [Test] + public void TestApplyNewDrumRoll() + { + var drumRoll = new DrawableDrumRoll(); + + AddStep("apply new drum roll", () => drumRoll.Apply(PrepareObject(new DrumRoll + { + StartTime = 300, + Duration = 500, + IsStrong = false, + TickRate = 2 + }), null)); + + AddHitObject(drumRoll); + RemoveHitObject(drumRoll); + + AddStep("apply new drum roll", () => drumRoll.Apply(PrepareObject(new DrumRoll + { + StartTime = 150, + Duration = 400, + IsStrong = true, + TickRate = 16 + }), null)); + + AddHitObject(drumRoll); + } + } +} diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneHitApplication.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneHitApplication.cs new file mode 100644 index 0000000000..52fd440857 --- /dev/null +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneHitApplication.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 NUnit.Framework; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.Objects.Drawables; + +namespace osu.Game.Rulesets.Taiko.Tests +{ + public class TestSceneHitApplication : HitObjectApplicationTestScene + { + [Test] + public void TestApplyNewHit() + { + var hit = new DrawableHit(); + + AddStep("apply new hit", () => hit.Apply(PrepareObject(new Hit + { + Type = HitType.Rim, + IsStrong = false, + StartTime = 300 + }), null)); + + AddHitObject(hit); + RemoveHitObject(hit); + + AddStep("apply new hit", () => hit.Apply(PrepareObject(new Hit + { + Type = HitType.Centre, + IsStrong = true, + StartTime = 500 + }), null)); + + AddHitObject(hit); + } + } +} diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneSampleOutput.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneSampleOutput.cs index 4ba9c447fb..296468d98d 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneSampleOutput.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneSampleOutput.cs @@ -1,6 +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 System.Collections.Generic; using System.Linq; using osu.Framework.Testing; using osu.Game.Audio; @@ -18,24 +19,33 @@ namespace osu.Game.Rulesets.Taiko.Tests public override void SetUpSteps() { base.SetUpSteps(); - AddAssert("has correct samples", () => + + var expectedSampleNames = new[] { - var names = Player.DrawableRuleset.Playfield.AllHitObjects.OfType().Select(h => string.Join(',', h.GetSamples().Select(s => s.Name))); + string.Empty, + string.Empty, + string.Empty, + string.Empty, + HitSampleInfo.HIT_FINISH, + HitSampleInfo.HIT_WHISTLE, + HitSampleInfo.HIT_WHISTLE, + HitSampleInfo.HIT_WHISTLE, + }; + var actualSampleNames = new List(); - var expected = new[] - { - string.Empty, - string.Empty, - string.Empty, - string.Empty, - HitSampleInfo.HIT_FINISH, - HitSampleInfo.HIT_WHISTLE, - HitSampleInfo.HIT_WHISTLE, - HitSampleInfo.HIT_WHISTLE, - }; + // due to pooling we can't access all samples right away due to object re-use, + // so we need to collect as we go. + AddStep("collect sample names", () => Player.DrawableRuleset.Playfield.NewResult += (dho, _) => + { + if (!(dho is DrawableHit h)) + return; - return names.SequenceEqual(expected); + actualSampleNames.Add(string.Join(',', h.GetSamples().Select(s => s.Name))); }); + + AddUntilStep("all samples collected", () => actualSampleNames.Count == expectedSampleNames.Length); + + AddAssert("samples are correct", () => actualSampleNames.SequenceEqual(expectedSampleNames)); } protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TaikoBeatmapConversionTest().GetBeatmap("sample-to-type-conversions"); diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs index 4925b6fdfc..d066abf767 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs @@ -3,6 +3,7 @@ using System; using System.Linq; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Utils; using osu.Game.Graphics; @@ -31,15 +32,26 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables /// private int rollingHits; - private Container tickContainer; + private readonly Container tickContainer; private Color4 colourIdle; private Color4 colourEngaged; - public DrawableDrumRoll(DrumRoll drumRoll) + public DrawableDrumRoll() + : this(null) + { + } + + public DrawableDrumRoll([CanBeNull] DrumRoll drumRoll) : base(drumRoll) { RelativeSizeAxes = Axes.Y; + + Content.Add(tickContainer = new Container + { + RelativeSizeAxes = Axes.Both, + Depth = float.MinValue + }); } [BackgroundDependencyLoader] @@ -47,12 +59,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { colourIdle = colours.YellowDark; colourEngaged = colours.YellowDarker; - - Content.Add(tickContainer = new Container - { - RelativeSizeAxes = Axes.Both, - Depth = float.MinValue - }); } protected override void LoadComplete() @@ -68,6 +74,12 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables updateColour(); } + protected override void OnFree() + { + base.OnFree(); + rollingHits = 0; + } + protected override void AddNestedHitObject(DrawableHitObject hitObject) { base.AddNestedHitObject(hitObject); @@ -83,7 +95,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables protected override void ClearNestedHitObjects() { base.ClearNestedHitObjects(); - tickContainer.Clear(); + tickContainer.Clear(false); } protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject) @@ -114,7 +126,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables rollingHits = Math.Clamp(rollingHits, 0, rolling_hits_for_engaged_colour); - updateColour(); + updateColour(100); } protected override void CheckForResult(bool userTriggered, double timeOffset) @@ -154,27 +166,34 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables Content.X = DrawHeight / 2; } - protected override DrawableStrongNestedHit CreateStrongNestedHit(DrumRoll.StrongNestedHit hitObject) => new StrongNestedHit(hitObject, this); + protected override DrawableStrongNestedHit CreateStrongNestedHit(DrumRoll.StrongNestedHit hitObject) => new StrongNestedHit(hitObject); - private void updateColour() + private void updateColour(double fadeDuration = 0) { Color4 newColour = Interpolation.ValueAt((float)rollingHits / rolling_hits_for_engaged_colour, colourIdle, colourEngaged, 0, 1); - (MainPiece.Drawable as IHasAccentColour)?.FadeAccent(newColour, 100); + (MainPiece.Drawable as IHasAccentColour)?.FadeAccent(newColour, fadeDuration); } - private class StrongNestedHit : DrawableStrongNestedHit + public class StrongNestedHit : DrawableStrongNestedHit { - public StrongNestedHit(DrumRoll.StrongNestedHit nestedHit, DrawableDrumRoll drumRoll) - : base(nestedHit, drumRoll) + public new DrawableDrumRoll ParentHitObject => (DrawableDrumRoll)base.ParentHitObject; + + public StrongNestedHit() + : this(null) + { + } + + public StrongNestedHit([CanBeNull] DrumRoll.StrongNestedHit nestedHit) + : base(nestedHit) { } protected override void CheckForResult(bool userTriggered, double timeOffset) { - if (!MainObject.Judged) + if (!ParentHitObject.Judged) return; - ApplyResult(r => r.Type = MainObject.IsHit ? r.Judgement.MaxResult : r.Judgement.MinResult); + ApplyResult(r => r.Type = ParentHitObject.IsHit ? r.Judgement.MaxResult : r.Judgement.MinResult); } public override bool OnPressed(TaikoAction action) => false; diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs index c6761de5e3..0df45c424d 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using JetBrains.Annotations; using osu.Framework.Graphics; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Taiko.Skinning.Default; @@ -16,7 +17,12 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables /// public HitType JudgementType; - public DrawableDrumRollTick(DrumRollTick tick) + public DrawableDrumRollTick() + : this(null) + { + } + + public DrawableDrumRollTick([CanBeNull] DrumRollTick tick) : base(tick) { FillMode = FillMode.Fit; @@ -61,21 +67,28 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables return UpdateResult(true); } - protected override DrawableStrongNestedHit CreateStrongNestedHit(DrumRollTick.StrongNestedHit hitObject) => new StrongNestedHit(hitObject, this); + protected override DrawableStrongNestedHit CreateStrongNestedHit(DrumRollTick.StrongNestedHit hitObject) => new StrongNestedHit(hitObject); - private class StrongNestedHit : DrawableStrongNestedHit + public class StrongNestedHit : DrawableStrongNestedHit { - public StrongNestedHit(DrumRollTick.StrongNestedHit nestedHit, DrawableDrumRollTick tick) - : base(nestedHit, tick) + public new DrawableDrumRollTick ParentHitObject => (DrawableDrumRollTick)base.ParentHitObject; + + public StrongNestedHit() + : this(null) + { + } + + public StrongNestedHit([CanBeNull] DrumRollTick.StrongNestedHit nestedHit) + : base(nestedHit) { } protected override void CheckForResult(bool userTriggered, double timeOffset) { - if (!MainObject.Judged) + if (!ParentHitObject.Judged) return; - ApplyResult(r => r.Type = MainObject.IsHit ? r.Judgement.MaxResult : r.Judgement.MinResult); + ApplyResult(r => r.Type = ParentHitObject.IsHit ? r.Judgement.MaxResult : r.Judgement.MinResult); } public override bool OnPressed(TaikoAction action) => false; diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs index 431f2980ec..38cda69a46 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs @@ -5,7 +5,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using osu.Framework.Allocation; +using JetBrains.Annotations; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Audio; @@ -36,29 +36,51 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables private bool pressHandledThisFrame; - private readonly Bindable type; + private readonly Bindable type = new Bindable(); - public DrawableHit(Hit hit) - : base(hit) + public DrawableHit() + : this(null) { - type = HitObject.TypeBindable.GetBoundCopy(); - FillMode = FillMode.Fit; - - updateActionsFromType(); } - [BackgroundDependencyLoader] - private void load() + public DrawableHit([CanBeNull] Hit hit) + : base(hit) { + FillMode = FillMode.Fit; + } + + protected override void OnApply() + { + type.BindTo(HitObject.TypeBindable); type.BindValueChanged(_ => { updateActionsFromType(); - // will overwrite samples, should only be called on change. + // will overwrite samples, should only be called on subsequent changes + // after the initial application. updateSamplesFromTypeChange(); RecreatePieces(); }); + + // action update also has to happen immediately on application. + updateActionsFromType(); + + base.OnApply(); + } + + protected override void OnFree() + { + base.OnFree(); + + type.UnbindFrom(HitObject.TypeBindable); + type.UnbindEvents(); + + UnproxyContent(); + + HitActions = null; + HitAction = null; + validActionPressed = pressHandledThisFrame = false; } private HitSampleInfo[] getRimSamples() => HitObject.Samples.Where(s => s.Name == HitSampleInfo.HIT_CLAP || s.Name == HitSampleInfo.HIT_WHISTLE).ToArray(); @@ -228,32 +250,37 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables } } - protected override DrawableStrongNestedHit CreateStrongNestedHit(Hit.StrongNestedHit hitObject) => new StrongNestedHit(hitObject, this); + protected override DrawableStrongNestedHit CreateStrongNestedHit(Hit.StrongNestedHit hitObject) => new StrongNestedHit(hitObject); - private class StrongNestedHit : DrawableStrongNestedHit + public class StrongNestedHit : DrawableStrongNestedHit { + public new DrawableHit ParentHitObject => (DrawableHit)base.ParentHitObject; + /// /// The lenience for the second key press. /// This does not adjust by map difficulty in ScoreV2 yet. /// private const double second_hit_window = 30; - public new DrawableHit MainObject => (DrawableHit)base.MainObject; + public StrongNestedHit() + : this(null) + { + } - public StrongNestedHit(Hit.StrongNestedHit nestedHit, DrawableHit hit) - : base(nestedHit, hit) + public StrongNestedHit([CanBeNull] Hit.StrongNestedHit nestedHit) + : base(nestedHit) { } protected override void CheckForResult(bool userTriggered, double timeOffset) { - if (!MainObject.Result.HasResult) + if (!ParentHitObject.Result.HasResult) { base.CheckForResult(userTriggered, timeOffset); return; } - if (!MainObject.Result.IsHit) + if (!ParentHitObject.Result.IsHit) { ApplyResult(r => r.Type = r.Judgement.MinResult); return; @@ -261,27 +288,27 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables if (!userTriggered) { - if (timeOffset - MainObject.Result.TimeOffset > second_hit_window) + if (timeOffset - ParentHitObject.Result.TimeOffset > second_hit_window) ApplyResult(r => r.Type = r.Judgement.MinResult); return; } - if (Math.Abs(timeOffset - MainObject.Result.TimeOffset) <= second_hit_window) + if (Math.Abs(timeOffset - ParentHitObject.Result.TimeOffset) <= second_hit_window) ApplyResult(r => r.Type = r.Judgement.MaxResult); } public override bool OnPressed(TaikoAction action) { // Don't process actions until the main hitobject is hit - if (!MainObject.IsHit) + if (!ParentHitObject.IsHit) return false; // Don't process actions if the pressed button was released - if (MainObject.HitAction == null) + if (ParentHitObject.HitAction == null) return false; // Don't handle invalid hit action presses - if (!MainObject.HitActions.Contains(action)) + if (!ParentHitObject.HitActions.Contains(action)) return false; return UpdateResult(true); diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableStrongNestedHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableStrongNestedHit.cs index d2e8888197..9c22e34387 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableStrongNestedHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableStrongNestedHit.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.Game.Rulesets.Objects.Drawables; +using JetBrains.Annotations; using osu.Game.Rulesets.Taiko.Judgements; namespace osu.Game.Rulesets.Taiko.Objects.Drawables @@ -11,12 +11,11 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables /// public abstract class DrawableStrongNestedHit : DrawableTaikoHitObject { - public readonly DrawableHitObject MainObject; + public new DrawableTaikoHitObject ParentHitObject => (DrawableTaikoHitObject)base.ParentHitObject; - protected DrawableStrongNestedHit(StrongNestedHitObject nestedHit, DrawableHitObject mainObject) + protected DrawableStrongNestedHit([CanBeNull] StrongNestedHitObject nestedHit) : base(nestedHit) { - MainObject = mainObject; } } } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs index 229d581d0c..60f9521996 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs @@ -3,6 +3,7 @@ using System; using System.Linq; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -35,7 +36,12 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables private readonly CircularContainer targetRing; private readonly CircularContainer expandingRing; - public DrawableSwell(Swell swell) + public DrawableSwell() + : this(null) + { + } + + public DrawableSwell([CanBeNull] Swell swell) : base(swell) { FillMode = FillMode.Fit; @@ -123,12 +129,13 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables Origin = Anchor.Centre, }); - protected override void LoadComplete() + protected override void OnFree() { - base.LoadComplete(); + base.OnFree(); - // We need to set this here because RelativeSizeAxes won't/can't set our size by default with a different RelativeChildSize - Width *= Parent.RelativeChildSize.X; + UnproxyContent(); + + lastWasCentre = null; } protected override void AddNestedHitObject(DrawableHitObject hitObject) @@ -146,7 +153,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables protected override void ClearNestedHitObjects() { base.ClearNestedHitObjects(); - ticks.Clear(); + ticks.Clear(false); } protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs index 14c86d151f..47fc7e5ab3 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs @@ -1,6 +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 JetBrains.Annotations; using osu.Framework.Graphics; using osu.Game.Rulesets.Taiko.Skinning.Default; using osu.Game.Skinning; @@ -11,7 +12,12 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { public override bool DisplayResult => false; - public DrawableSwellTick(SwellTick hitObject) + public DrawableSwellTick() + : this(null) + { + } + + public DrawableSwellTick([CanBeNull] SwellTick hitObject) : base(hitObject) { } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs index cf3aa69b6f..6041eccb51 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.Linq; -using osu.Framework.Allocation; +using JetBrains.Annotations; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Primitives; @@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables private readonly Container nonProxiedContent; - protected DrawableTaikoHitObject(TaikoHitObject hitObject) + protected DrawableTaikoHitObject([CanBeNull] TaikoHitObject hitObject) : base(hitObject) { AddRangeInternal(new[] @@ -113,25 +113,23 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { public override Vector2 OriginPosition => new Vector2(DrawHeight / 2); - public new TObject HitObject; + public new TObject HitObject => (TObject)base.HitObject; protected Vector2 BaseSize; protected SkinnableDrawable MainPiece; - protected DrawableTaikoHitObject(TObject hitObject) + protected DrawableTaikoHitObject([CanBeNull] TObject hitObject) : base(hitObject) { - HitObject = hitObject; - Anchor = Anchor.CentreLeft; Origin = Anchor.Custom; RelativeSizeAxes = Axes.Both; } - [BackgroundDependencyLoader] - private void load() + protected override void OnApply() { + base.OnApply(); RecreatePieces(); } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoStrongableHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoStrongableHitObject.cs index af3e94d9c6..4f1523eb3f 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoStrongableHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoStrongableHitObject.cs @@ -2,7 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Linq; -using osu.Framework.Allocation; +using JetBrains.Annotations; using osu.Framework.Bindables; using osu.Framework.Graphics.Containers; using osu.Game.Audio; @@ -16,28 +16,38 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables where TObject : TaikoStrongableHitObject where TStrongNestedObject : StrongNestedHitObject { - private readonly Bindable isStrong; + private readonly Bindable isStrong = new BindableBool(); private readonly Container strongHitContainer; - protected DrawableTaikoStrongableHitObject(TObject hitObject) + protected DrawableTaikoStrongableHitObject([CanBeNull] TObject hitObject) : base(hitObject) { - isStrong = HitObject.IsStrongBindable.GetBoundCopy(); - AddInternal(strongHitContainer = new Container()); } - [BackgroundDependencyLoader] - private void load() + protected override void OnApply() { + isStrong.BindTo(HitObject.IsStrongBindable); isStrong.BindValueChanged(_ => { - // will overwrite samples, should only be called on change. + // will overwrite samples, should only be called on subsequent changes + // after the initial application. updateSamplesFromStrong(); RecreatePieces(); }); + + base.OnApply(); + } + + protected override void OnFree() + { + base.OnFree(); + + isStrong.UnbindFrom(HitObject.IsStrongBindable); + // ensure the next application does not accidentally overwrite samples. + isStrong.UnbindEvents(); } private HitSampleInfo[] getStrongSamples() => HitObject.Samples.Where(s => s.Name == HitSampleInfo.HIT_FINISH).ToArray(); @@ -86,7 +96,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables protected override void ClearNestedHitObjects() { base.ClearNestedHitObjects(); - strongHitContainer.Clear(); + strongHitContainer.Clear(false); } protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject) diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs index 9cf931ee0a..ed8e6859a2 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs @@ -7,7 +7,6 @@ using osu.Framework.Graphics; using osu.Game.Beatmaps; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Taiko.Objects; -using osu.Game.Rulesets.Taiko.Objects.Drawables; using osu.Game.Rulesets.UI; using osu.Game.Rulesets.Taiko.Replays; using osu.Framework.Input; @@ -64,22 +63,7 @@ namespace osu.Game.Rulesets.Taiko.UI protected override Playfield CreatePlayfield() => new TaikoPlayfield(Beatmap.ControlPointInfo); - public override DrawableHitObject CreateDrawableRepresentation(TaikoHitObject h) - { - switch (h) - { - case Hit hit: - return new DrawableHit(hit); - - case DrumRoll drumRoll: - return new DrawableDrumRoll(drumRoll); - - case Swell swell: - return new DrawableSwell(swell); - } - - return null; - } + public override DrawableHitObject CreateDrawableRepresentation(TaikoHitObject h) => null; protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new TaikoFramedReplayInputHandler(replay); diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index d20b190c86..148ec7755e 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -147,6 +147,32 @@ namespace osu.Game.Rulesets.Taiko.UI }, drumRollHitContainer.CreateProxy(), }; + + RegisterPool(50); + RegisterPool(50); + + RegisterPool(5); + RegisterPool(5); + + RegisterPool(100); + RegisterPool(100); + + RegisterPool(5); + RegisterPool(100); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + NewResult += OnNewResult; + } + + protected override void OnNewDrawableHitObject(DrawableHitObject drawableHitObject) + { + base.OnNewDrawableHitObject(drawableHitObject); + + var taikoObject = (DrawableTaikoHitObject)drawableHitObject; + topLevelHitContainer.Add(taikoObject.CreateProxiedContent()); } protected override void Update() @@ -207,9 +233,7 @@ namespace osu.Game.Rulesets.Taiko.UI barLinePlayfield.Add(barLine); break; - case DrawableTaikoHitObject taikoObject: - h.OnNewResult += OnNewResult; - topLevelHitContainer.Add(taikoObject.CreateProxiedContent()); + case DrawableTaikoHitObject _: base.Add(h); break; @@ -226,8 +250,6 @@ namespace osu.Game.Rulesets.Taiko.UI return barLinePlayfield.Remove(barLine); case DrawableTaikoHitObject _: - h.OnNewResult -= OnNewResult; - // todo: consider tidying of proxied content if required. return base.Remove(h); default: @@ -248,7 +270,7 @@ namespace osu.Game.Rulesets.Taiko.UI { case TaikoStrongJudgement _: if (result.IsHit) - hitExplosionContainer.Children.FirstOrDefault(e => e.JudgedObject == ((DrawableStrongNestedHit)judgedObject).MainObject)?.VisualiseSecondHit(); + hitExplosionContainer.Children.FirstOrDefault(e => e.JudgedObject == ((DrawableStrongNestedHit)judgedObject).ParentHitObject)?.VisualiseSecondHit(); break; case TaikoDrumRollTickJudgement _: diff --git a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs index d46769a7c0..38cb6729c3 100644 --- a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs +++ b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs @@ -10,9 +10,11 @@ using NUnit.Framework; using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Graphics.Audio; +using osu.Framework.Graphics.Textures; using osu.Framework.IO.Stores; using osu.Framework.Testing; using osu.Game.Audio; +using osu.Game.IO; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; @@ -27,7 +29,7 @@ using osu.Game.Tests.Visual; namespace osu.Game.Tests.Gameplay { [HeadlessTest] - public class TestSceneStoryboardSamples : OsuTestScene + public class TestSceneStoryboardSamples : OsuTestScene, IStorageResourceProvider { [Test] public void TestRetrieveTopLevelSample() @@ -35,7 +37,7 @@ namespace osu.Game.Tests.Gameplay ISkin skin = null; SampleChannel channel = null; - AddStep("create skin", () => skin = new TestSkin("test-sample", Audio)); + AddStep("create skin", () => skin = new TestSkin("test-sample", this)); AddStep("retrieve sample", () => channel = skin.GetSample(new SampleInfo("test-sample"))); AddAssert("sample is non-null", () => channel != null); @@ -47,7 +49,7 @@ namespace osu.Game.Tests.Gameplay ISkin skin = null; SampleChannel channel = null; - AddStep("create skin", () => skin = new TestSkin("folder/test-sample", Audio)); + AddStep("create skin", () => skin = new TestSkin("folder/test-sample", this)); AddStep("retrieve sample", () => channel = skin.GetSample(new SampleInfo("folder/test-sample"))); AddAssert("sample is non-null", () => channel != null); @@ -105,7 +107,7 @@ namespace osu.Game.Tests.Gameplay AddStep("setup storyboard sample", () => { - Beatmap.Value = new TestCustomSkinWorkingBeatmap(new OsuRuleset().RulesetInfo, Audio); + Beatmap.Value = new TestCustomSkinWorkingBeatmap(new OsuRuleset().RulesetInfo, this); SelectedMods.Value = new[] { testedMod }; var beatmapSkinSourceContainer = new BeatmapSkinProvidingContainer(Beatmap.Value.Skin); @@ -128,8 +130,8 @@ namespace osu.Game.Tests.Gameplay private class TestSkin : LegacySkin { - public TestSkin(string resourceName, AudioManager audioManager) - : base(DefaultLegacySkin.Info, new TestResourceStore(resourceName), audioManager, "skin.ini") + public TestSkin(string resourceName, IStorageResourceProvider resources) + : base(DefaultLegacySkin.Info, new TestResourceStore(resourceName), resources, "skin.ini") { } } @@ -158,15 +160,15 @@ namespace osu.Game.Tests.Gameplay private class TestCustomSkinWorkingBeatmap : ClockBackedTestWorkingBeatmap { - private readonly AudioManager audio; + private readonly IStorageResourceProvider resources; - public TestCustomSkinWorkingBeatmap(RulesetInfo ruleset, AudioManager audio) - : base(ruleset, null, audio) + public TestCustomSkinWorkingBeatmap(RulesetInfo ruleset, IStorageResourceProvider resources) + : base(ruleset, null, resources.AudioManager) { - this.audio = audio; + this.resources = resources; } - protected override ISkin GetSkin() => new TestSkin("test-sample", audio); + protected override ISkin GetSkin() => new TestSkin("test-sample", resources); } private class TestDrawableStoryboardSample : DrawableStoryboardSample @@ -176,5 +178,13 @@ namespace osu.Game.Tests.Gameplay { } } + + #region IResourceStorageProvider + + public AudioManager AudioManager => Audio; + public IResourceStore Files => null; + public IResourceStore CreateTextureLoaderStore(IResourceStore underlyingStore) => null; + + #endregion } } diff --git a/osu.Game.Tests/Visual/Components/TestScenePollingComponent.cs b/osu.Game.Tests/Visual/Components/TestScenePollingComponent.cs index fb10015ef4..2236f85b92 100644 --- a/osu.Game.Tests/Visual/Components/TestScenePollingComponent.cs +++ b/osu.Game.Tests/Visual/Components/TestScenePollingComponent.cs @@ -61,12 +61,12 @@ namespace osu.Game.Tests.Visual.Components { createPoller(true); - AddStep("set poll interval to 1", () => poller.TimeBetweenPolls = TimePerAction * safety_adjust); + AddStep("set poll interval to 1", () => poller.TimeBetweenPolls.Value = TimePerAction * safety_adjust); checkCount(1); checkCount(2); checkCount(3); - AddStep("set poll interval to 5", () => poller.TimeBetweenPolls = TimePerAction * safety_adjust * 5); + AddStep("set poll interval to 5", () => poller.TimeBetweenPolls.Value = TimePerAction * safety_adjust * 5); checkCount(4); checkCount(4); checkCount(4); @@ -76,7 +76,7 @@ namespace osu.Game.Tests.Visual.Components checkCount(5); checkCount(5); - AddStep("set poll interval to 1", () => poller.TimeBetweenPolls = TimePerAction * safety_adjust); + AddStep("set poll interval to 1", () => poller.TimeBetweenPolls.Value = TimePerAction * safety_adjust); checkCount(6); checkCount(7); } @@ -87,7 +87,7 @@ namespace osu.Game.Tests.Visual.Components { createPoller(false); - AddStep("set poll interval to 1", () => poller.TimeBetweenPolls = TimePerAction * safety_adjust * 5); + AddStep("set poll interval to 1", () => poller.TimeBetweenPolls.Value = TimePerAction * safety_adjust * 5); checkCount(0); skip(); checkCount(0); @@ -141,7 +141,7 @@ namespace osu.Game.Tests.Visual.Components public class TestSlowPoller : TestPoller { - protected override Task Poll() => Task.Delay((int)(TimeBetweenPolls / 2f / Clock.Rate)).ContinueWith(_ => base.Poll()); + protected override Task Poll() => Task.Delay((int)(TimeBetweenPolls.Value / 2f / Clock.Rate)).ContinueWith(_ => base.Poll()); } } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneCompletionCancellation.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneCompletionCancellation.cs index 6fd5511e5a..4ee48fd853 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneCompletionCancellation.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneCompletionCancellation.cs @@ -10,6 +10,8 @@ using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Scoring; +using osu.Game.Screens.Ranking; using osu.Game.Storyboards; using osuTK; @@ -50,7 +52,7 @@ namespace osu.Game.Tests.Visual.Gameplay cancel(); complete(); - AddUntilStep("attempted to push ranking", () => ((FakeRankingPushPlayer)Player).GotoRankingInvoked); + AddUntilStep("attempted to push ranking", () => ((FakeRankingPushPlayer)Player).ResultsCreated); } /// @@ -84,7 +86,7 @@ namespace osu.Game.Tests.Visual.Gameplay { // wait to ensure there was no attempt of pushing the results screen. AddWaitStep("wait", resultsDisplayWaitCount); - AddAssert("no attempt to push ranking", () => !((FakeRankingPushPlayer)Player).GotoRankingInvoked); + AddAssert("no attempt to push ranking", () => !((FakeRankingPushPlayer)Player).ResultsCreated); } protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) @@ -110,16 +112,18 @@ namespace osu.Game.Tests.Visual.Gameplay public class FakeRankingPushPlayer : TestPlayer { - public bool GotoRankingInvoked; + public bool ResultsCreated { get; private set; } public FakeRankingPushPlayer() : base(true, true) { } - protected override void GotoRanking() + protected override ResultsScreen CreateResults(ScoreInfo score) { - GotoRankingInvoked = true; + var results = base.CreateResults(score); + ResultsCreated = true; + return results; } } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs index df970c1c46..c0a021436e 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs @@ -6,6 +6,7 @@ using NUnit.Framework; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Testing; +using osu.Framework.Utils; using osu.Game.Screens.Play.HUD; using osu.Game.Users; using osuTK; @@ -26,7 +27,6 @@ namespace osu.Game.Tests.Visual.Gameplay Anchor = Anchor.Centre, Origin = Anchor.Centre, Scale = new Vector2(2), - RelativeSizeAxes = Axes.X, }); } @@ -39,7 +39,7 @@ namespace osu.Game.Tests.Visual.Gameplay playerScore.Value = 1222333; }); - AddStep("add player user", () => leaderboard.AddPlayer(playerScore, new User { Username = "You" })); + AddStep("add local player", () => createLeaderboardScore(playerScore, new User { Username = "You", Id = 3 }, true)); AddSliderStep("set player score", 50, 5000000, 1222333, v => playerScore.Value = v); } @@ -49,22 +49,46 @@ namespace osu.Game.Tests.Visual.Gameplay var player2Score = new BindableDouble(1234567); var player3Score = new BindableDouble(1111111); - AddStep("add player 2", () => leaderboard.AddPlayer(player2Score, new User { Username = "Player 2" })); - AddStep("add player 3", () => leaderboard.AddPlayer(player3Score, new User { Username = "Player 3" })); + AddStep("add player 2", () => createLeaderboardScore(player2Score, new User { Username = "Player 2" })); + AddStep("add player 3", () => createLeaderboardScore(player3Score, new User { Username = "Player 3" })); - AddAssert("is player 2 position #1", () => leaderboard.CheckPositionByUsername("Player 2", 1)); - AddAssert("is player position #2", () => leaderboard.CheckPositionByUsername("You", 2)); - AddAssert("is player 3 position #3", () => leaderboard.CheckPositionByUsername("Player 3", 3)); + AddUntilStep("is player 2 position #1", () => leaderboard.CheckPositionByUsername("Player 2", 1)); + AddUntilStep("is player position #2", () => leaderboard.CheckPositionByUsername("You", 2)); + AddUntilStep("is player 3 position #3", () => leaderboard.CheckPositionByUsername("Player 3", 3)); AddStep("set score above player 3", () => player2Score.Value = playerScore.Value - 500); - AddAssert("is player position #1", () => leaderboard.CheckPositionByUsername("You", 1)); - AddAssert("is player 2 position #2", () => leaderboard.CheckPositionByUsername("Player 2", 2)); - AddAssert("is player 3 position #3", () => leaderboard.CheckPositionByUsername("Player 3", 3)); + AddUntilStep("is player position #1", () => leaderboard.CheckPositionByUsername("You", 1)); + AddUntilStep("is player 2 position #2", () => leaderboard.CheckPositionByUsername("Player 2", 2)); + AddUntilStep("is player 3 position #3", () => leaderboard.CheckPositionByUsername("Player 3", 3)); AddStep("set score below players", () => player2Score.Value = playerScore.Value - 123456); - AddAssert("is player position #1", () => leaderboard.CheckPositionByUsername("You", 1)); - AddAssert("is player 3 position #2", () => leaderboard.CheckPositionByUsername("Player 3", 2)); - AddAssert("is player 2 position #3", () => leaderboard.CheckPositionByUsername("Player 2", 3)); + AddUntilStep("is player position #1", () => leaderboard.CheckPositionByUsername("You", 1)); + AddUntilStep("is player 3 position #2", () => leaderboard.CheckPositionByUsername("Player 3", 2)); + AddUntilStep("is player 2 position #3", () => leaderboard.CheckPositionByUsername("Player 2", 3)); + } + + [Test] + public void TestRandomScores() + { + int playerNumber = 1; + AddRepeatStep("add player with random score", () => createRandomScore(new User { Username = $"Player {playerNumber++}" }), 10); + } + + [Test] + public void TestExistingUsers() + { + AddStep("add peppy", () => createRandomScore(new User { Username = "peppy", Id = 2 })); + AddStep("add smoogipoo", () => createRandomScore(new User { Username = "smoogipoo", Id = 1040328 })); + AddStep("add flyte", () => createRandomScore(new User { Username = "flyte", Id = 3103765 })); + AddStep("add frenzibyte", () => createRandomScore(new User { Username = "frenzibyte", Id = 14210502 })); + } + + private void createRandomScore(User user) => createLeaderboardScore(new BindableDouble(RNG.Next(0, 5_000_000)), user); + + private void createLeaderboardScore(BindableDouble score, User user, bool isTracked = false) + { + var leaderboardScore = leaderboard.AddPlayer(user, isTracked); + leaderboardScore.TotalScore.BindTo(score); } private class TestGameplayLeaderboard : GameplayLeaderboard diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneMultiplayerGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneMultiplayerGameplayLeaderboard.cs new file mode 100644 index 0000000000..8078c7b994 --- /dev/null +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneMultiplayerGameplayLeaderboard.cs @@ -0,0 +1,157 @@ +// 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 System.Threading.Tasks; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; +using osu.Framework.Utils; +using osu.Game.Database; +using osu.Game.Online; +using osu.Game.Online.Spectator; +using osu.Game.Replays.Legacy; +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; + +namespace osu.Game.Tests.Visual.Gameplay +{ + public class TestSceneMultiplayerGameplayLeaderboard : OsuTestScene + { + [Cached(typeof(SpectatorStreamingClient))] + private TestMultiplayerStreaming streamingClient = new TestMultiplayerStreaming(16); + + [Cached(typeof(UserLookupCache))] + private UserLookupCache lookupCache = new TestSceneCurrentlyPlayingDisplay.TestUserLookupCache(); + + private MultiplayerGameplayLeaderboard leaderboard; + + protected override Container Content { get; } = new Container { RelativeSizeAxes = Axes.Both }; + + public TestSceneMultiplayerGameplayLeaderboard() + { + base.Content.Children = new Drawable[] + { + streamingClient, + lookupCache, + Content + }; + } + + [SetUpSteps] + public void SetUpSteps() + { + AddStep("create leaderboard", () => + { + OsuScoreProcessor scoreProcessor; + Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value); + + var playable = Beatmap.Value.GetPlayableBeatmap(Ruleset.Value); + + streamingClient.Start(Beatmap.Value.BeatmapInfo.OnlineBeatmapID ?? 0); + + Children = new Drawable[] + { + scoreProcessor = new OsuScoreProcessor(), + }; + + scoreProcessor.ApplyBeatmap(playable); + + LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(scoreProcessor, streamingClient.PlayingUsers.ToArray()) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, Add); + }); + + AddUntilStep("wait for load", () => leaderboard.IsLoaded); + } + + [Test] + public void TestScoreUpdates() + { + AddRepeatStep("update state", () => streamingClient.RandomlyUpdateState(), 100); + } + + public class TestMultiplayerStreaming : SpectatorStreamingClient + { + public new BindableList PlayingUsers => (BindableList)base.PlayingUsers; + + private readonly int totalUsers; + + public TestMultiplayerStreaming(int totalUsers) + : base(new DevelopmentEndpointConfiguration()) + { + this.totalUsers = totalUsers; + } + + public void Start(int beatmapId) + { + for (int i = 0; i < totalUsers; i++) + { + ((ISpectatorClient)this).UserBeganPlaying(i, new SpectatorState + { + BeatmapID = beatmapId, + RulesetID = 0, + }); + } + } + + private readonly Dictionary lastHeaders = new Dictionary(); + + public void RandomlyUpdateState() + { + foreach (var userId in PlayingUsers) + { + if (RNG.NextBool()) + continue; + + if (!lastHeaders.TryGetValue(userId, out var header)) + { + lastHeaders[userId] = header = new FrameHeader(new ScoreInfo + { + Statistics = new Dictionary + { + [HitResult.Miss] = 0, + [HitResult.Meh] = 0, + [HitResult.Great] = 0 + } + }); + } + + switch (RNG.Next(0, 3)) + { + case 0: + header.Combo = 0; + header.Statistics[HitResult.Miss]++; + break; + + case 1: + header.Combo++; + header.MaxCombo = Math.Max(header.MaxCombo, header.Combo); + header.Statistics[HitResult.Meh]++; + break; + + default: + header.Combo++; + header.MaxCombo = Math.Max(header.MaxCombo, header.Combo); + header.Statistics[HitResult.Great]++; + break; + } + + ((ISpectatorClient)this).UserSentFrames(userId, new FrameDataBundle(header, Array.Empty())); + } + } + + protected override Task Connect() => Task.CompletedTask; + } + } +} diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index 3e5b561a6f..26524f07da 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -12,6 +12,7 @@ using osu.Framework.Screens; using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Beatmaps; +using osu.Game.Online; using osu.Game.Online.Spectator; using osu.Game.Replays.Legacy; using osu.Game.Rulesets.Osu; @@ -232,12 +233,17 @@ namespace osu.Game.Tests.Visual.Gameplay public class TestSpectatorStreamingClient : SpectatorStreamingClient { - public readonly User StreamingUser = new User { Id = 1234, Username = "Test user" }; + public readonly User StreamingUser = new User { Id = 55, Username = "Test user" }; public new BindableList PlayingUsers => (BindableList)base.PlayingUsers; private int beatmapId; + public TestSpectatorStreamingClient() + : base(new DevelopmentEndpointConfiguration()) + { + } + protected override Task Connect() { return Task.CompletedTask; diff --git a/osu.Game.Tests/Visual/Multiplayer/RoomManagerTestScene.cs b/osu.Game.Tests/Visual/Multiplayer/RoomManagerTestScene.cs index 8b7e0fd9da..c665a57452 100644 --- a/osu.Game.Tests/Visual/Multiplayer/RoomManagerTestScene.cs +++ b/osu.Game.Tests/Visual/Multiplayer/RoomManagerTestScene.cs @@ -4,14 +4,14 @@ using System; using osu.Framework.Allocation; using osu.Game.Beatmaps; -using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; using osu.Game.Rulesets; -using osu.Game.Screens.Multi; +using osu.Game.Screens.OnlinePlay; using osu.Game.Users; namespace osu.Game.Tests.Visual.Multiplayer { - public abstract class RoomManagerTestScene : MultiplayerTestScene + public abstract class RoomManagerTestScene : RoomTestScene { [Cached(Type = typeof(IRoomManager))] protected TestRoomManager RoomManager { get; } = new TestRoomManager(); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestRoomManager.cs b/osu.Game.Tests/Visual/Multiplayer/TestRoomManager.cs index 67a53307fc..1785c99784 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestRoomManager.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestRoomManager.cs @@ -3,8 +3,8 @@ using System; using osu.Framework.Bindables; -using osu.Game.Online.Multiplayer; -using osu.Game.Screens.Multi; +using osu.Game.Online.Rooms; +using osu.Game.Screens.OnlinePlay; namespace osu.Game.Tests.Visual.Multiplayer { @@ -18,7 +18,7 @@ namespace osu.Game.Tests.Visual.Multiplayer public readonly BindableList Rooms = new BindableList(); - public Bindable InitialRoomsReceived { get; } = new Bindable(true); + public IBindable InitialRoomsReceived { get; } = new Bindable(true); IBindableList IRoomManager.Rooms => Rooms; diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs index 55b026eff6..65c0cfd328 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs @@ -13,12 +13,12 @@ using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; -using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; -using osu.Game.Screens.Multi; +using osu.Game.Screens.OnlinePlay; using osu.Game.Tests.Beatmaps; using osuTK; using osuTK.Input; diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomInfo.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomInfo.cs index 9baaa42c83..9f24347ae9 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomInfo.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomInfo.cs @@ -4,13 +4,13 @@ using System; using NUnit.Framework; using osu.Framework.Graphics; -using osu.Game.Online.Multiplayer.RoomStatuses; -using osu.Game.Screens.Multi.Lounge.Components; +using osu.Game.Online.Rooms.RoomStatuses; +using osu.Game.Screens.OnlinePlay.Lounge.Components; using osu.Game.Users; namespace osu.Game.Tests.Visual.Multiplayer { - public class TestSceneLoungeRoomInfo : MultiplayerTestScene + public class TestSceneLoungeRoomInfo : RoomTestScene { [SetUp] public new void Setup() => Schedule(() => diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs index e33d15cfff..279dcfa584 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs @@ -6,10 +6,10 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Graphics; -using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Osu; -using osu.Game.Screens.Multi.Lounge.Components; +using osu.Game.Screens.OnlinePlay.Lounge.Components; using osuTK.Graphics; using osuTK.Input; diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs index 6b1d90e06e..9ad9f2c883 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs @@ -5,17 +5,17 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Beatmaps; -using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; -using osu.Game.Screens.Multi.Components; +using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Tests.Beatmaps; using osuTK; namespace osu.Game.Tests.Visual.Multiplayer { - public class TestSceneMatchBeatmapDetailArea : MultiplayerTestScene + public class TestSceneMatchBeatmapDetailArea : RoomTestScene { [Resolved] private BeatmapManager beatmapManager { get; set; } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchHeader.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchHeader.cs index ec5292e51e..7cdc6b1a7d 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchHeader.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchHeader.cs @@ -3,15 +3,15 @@ using NUnit.Framework; using osu.Game.Beatmaps; -using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; -using osu.Game.Screens.Multi.Match.Components; +using osu.Game.Screens.OnlinePlay.Match.Components; using osu.Game.Users; namespace osu.Game.Tests.Visual.Multiplayer { - public class TestSceneMatchHeader : MultiplayerTestScene + public class TestSceneMatchHeader : RoomTestScene { public TestSceneMatchHeader() { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboard.cs index a72f71d79c..64eaf0556b 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboard.cs @@ -7,13 +7,13 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Online.API; -using osu.Game.Screens.Multi.Match.Components; +using osu.Game.Screens.OnlinePlay.Match.Components; using osu.Game.Users; using osuTK; namespace osu.Game.Tests.Visual.Multiplayer { - public class TestSceneMatchLeaderboard : MultiplayerTestScene + public class TestSceneMatchLeaderboard : RoomTestScene { protected override bool UseOnlineAPI => true; diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSongSelect.cs index 4742fd0d84..e0fd7d9874 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSongSelect.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSongSelect.cs @@ -18,12 +18,12 @@ using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; -using osu.Game.Screens.Multi.Components; +using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.Select; namespace osu.Game.Tests.Visual.Multiplayer { - public class TestSceneMatchSongSelect : MultiplayerTestScene + public class TestSceneMatchSongSelect : RoomTestScene { [Resolved] private BeatmapManager beatmapManager { get; set; } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiHeader.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiHeader.cs index 76ab402b72..2244dcfc56 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiHeader.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiHeader.cs @@ -5,7 +5,7 @@ using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Screens; using osu.Game.Screens; -using osu.Game.Screens.Multi; +using osu.Game.Screens.OnlinePlay; namespace osu.Game.Tests.Visual.Multiplayer { @@ -18,24 +18,24 @@ namespace osu.Game.Tests.Visual.Multiplayer OsuScreenStack screenStack = new OsuScreenStack { RelativeSizeAxes = Axes.Both }; - screenStack.Push(new TestMultiplayerSubScreen(index)); + screenStack.Push(new TestOnlinePlaySubScreen(index)); Children = new Drawable[] { screenStack, - new Header(screenStack) + new Header("Multiplayer", screenStack) }; - AddStep("push multi screen", () => screenStack.CurrentScreen.Push(new TestMultiplayerSubScreen(++index))); + AddStep("push multi screen", () => screenStack.CurrentScreen.Push(new TestOnlinePlaySubScreen(++index))); } - private class TestMultiplayerSubScreen : OsuScreen, IMultiplayerSubScreen + private class TestOnlinePlaySubScreen : OsuScreen, IOnlinePlaySubScreen { private readonly int index; public string ShortTitle => $"Screen {index}"; - public TestMultiplayerSubScreen(int index) + public TestOnlinePlaySubScreen(int index) { this.index = index; } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs new file mode 100644 index 0000000000..2e39471dc0 --- /dev/null +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -0,0 +1,47 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Game.Screens.OnlinePlay.Components; +using osu.Game.Users; + +namespace osu.Game.Tests.Visual.Multiplayer +{ + public class TestSceneMultiplayer : MultiplayerTestScene + { + public TestSceneMultiplayer() + { + var multi = new TestMultiplayer(); + + AddStep("show", () => LoadScreen(multi)); + AddUntilStep("wait for loaded", () => multi.IsLoaded); + } + + [Test] + public void TestOneUserJoinedMultipleTimes() + { + var user = new User { Id = 33 }; + + AddRepeatStep("add user multiple times", () => Client.AddUser(user), 3); + + AddAssert("room has 2 users", () => Client.Room?.Users.Count == 2); + } + + [Test] + public void TestOneUserLeftMultipleTimes() + { + var user = new User { Id = 44 }; + + AddStep("add user", () => Client.AddUser(user)); + AddAssert("room has 2 users", () => Client.Room?.Users.Count == 2); + + AddRepeatStep("remove user multiple times", () => Client.RemoveUser(user), 3); + AddAssert("room has 1 user", () => Client.Room?.Users.Count == 1); + } + + private class TestMultiplayer : Screens.OnlinePlay.Multiplayer.Multiplayer + { + protected override RoomManager CreateRoomManager() => new TestMultiplayerRoomManager(); + } + } +} diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs new file mode 100644 index 0000000000..8869718fd1 --- /dev/null +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs @@ -0,0 +1,77 @@ +// 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.Framework.Testing; +using osu.Game.Online.Rooms; +using osu.Game.Rulesets.Osu; +using osu.Game.Screens.OnlinePlay.Multiplayer; +using osu.Game.Screens.OnlinePlay.Multiplayer.Match; +using osu.Game.Tests.Beatmaps; +using osuTK.Input; + +namespace osu.Game.Tests.Visual.Multiplayer +{ + public class TestSceneMultiplayerMatchSubScreen : MultiplayerTestScene + { + private MultiplayerMatchSubScreen screen; + + public TestSceneMultiplayerMatchSubScreen() + : base(false) + { + } + + [SetUp] + public new void Setup() => Schedule(() => + { + Room.Name.Value = "Test Room"; + }); + + [SetUpSteps] + public void SetupSteps() + { + AddStep("load match", () => LoadScreen(screen = new MultiplayerMatchSubScreen(Room))); + AddUntilStep("wait for load", () => screen.IsCurrentScreen()); + } + + [Test] + public void TestSettingValidity() + { + AddAssert("create button not enabled", () => !this.ChildrenOfType().Single().Enabled.Value); + + AddStep("set playlist", () => + { + Room.Playlist.Add(new PlaylistItem + { + Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo }, + Ruleset = { Value = new OsuRuleset().RulesetInfo }, + }); + }); + + AddAssert("create button enabled", () => this.ChildrenOfType().Single().Enabled.Value); + } + + [Test] + public void TestCreatedRoom() + { + AddStep("set playlist", () => + { + Room.Playlist.Add(new PlaylistItem + { + Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo }, + Ruleset = { Value = new OsuRuleset().RulesetInfo }, + }); + }); + + AddStep("click create button", () => + { + InputManager.MoveMouseTo(this.ChildrenOfType().Single()); + InputManager.Click(MouseButton.Left); + }); + + AddWaitStep("wait", 10); + } + } +} diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs new file mode 100644 index 0000000000..9181170bee --- /dev/null +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs @@ -0,0 +1,117 @@ +// 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.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Testing; +using osu.Framework.Utils; +using osu.Game.Online.Multiplayer; +using osu.Game.Screens.OnlinePlay.Multiplayer.Participants; +using osu.Game.Users; +using osuTK; + +namespace osu.Game.Tests.Visual.Multiplayer +{ + public class TestSceneMultiplayerParticipantsList : MultiplayerTestScene + { + [SetUp] + public new void Setup() => Schedule(() => + { + Child = new ParticipantsList + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Y, + Size = new Vector2(380, 0.7f) + }; + }); + + [Test] + public void TestAddUser() + { + AddAssert("one unique panel", () => this.ChildrenOfType().Select(p => p.User).Distinct().Count() == 1); + + AddStep("add user", () => Client.AddUser(new User + { + Id = 3, + Username = "Second", + CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + })); + + AddAssert("two unique panels", () => this.ChildrenOfType().Select(p => p.User).Distinct().Count() == 2); + } + + [Test] + public void TestRemoveUser() + { + User secondUser = null; + + AddStep("add a user", () => + { + Client.AddUser(secondUser = new User + { + Id = 3, + Username = "Second", + CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + }); + }); + + AddStep("remove host", () => Client.RemoveUser(API.LocalUser.Value)); + + AddAssert("single panel is for second user", () => this.ChildrenOfType().Single().User.User == secondUser); + } + + [Test] + public void TestToggleReadyState() + { + AddAssert("ready mark invisible", () => !this.ChildrenOfType().Single().IsPresent); + + AddStep("make user ready", () => Client.ChangeState(MultiplayerUserState.Ready)); + AddUntilStep("ready mark visible", () => this.ChildrenOfType().Single().IsPresent); + + AddStep("make user idle", () => Client.ChangeState(MultiplayerUserState.Idle)); + AddUntilStep("ready mark invisible", () => !this.ChildrenOfType().Single().IsPresent); + } + + [Test] + public void TestCrownChangesStateWhenHostTransferred() + { + AddStep("add user", () => Client.AddUser(new User + { + Id = 3, + Username = "Second", + CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + })); + + AddUntilStep("first user crown visible", () => this.ChildrenOfType().ElementAt(0).ChildrenOfType().First().Alpha == 1); + AddUntilStep("second user crown hidden", () => this.ChildrenOfType().ElementAt(1).ChildrenOfType().First().Alpha == 0); + + AddStep("make second user host", () => Client.TransferHost(3)); + + AddUntilStep("first user crown hidden", () => this.ChildrenOfType().ElementAt(0).ChildrenOfType().First().Alpha == 0); + AddUntilStep("second user crown visible", () => this.ChildrenOfType().ElementAt(1).ChildrenOfType().First().Alpha == 1); + } + + [Test] + public void TestManyUsers() + { + AddStep("add many users", () => + { + for (int i = 0; i < 20; i++) + { + Client.AddUser(new User + { + Id = i, + Username = $"User {i}", + CurrentModeRank = RNG.Next(1, 100000), + CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + }); + + Client.ChangeUserState(i, (MultiplayerUserState)RNG.Next(0, (int)MultiplayerUserState.Results + 1)); + } + }); + } + } +} diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs new file mode 100644 index 0000000000..6b11613f1c --- /dev/null +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs @@ -0,0 +1,164 @@ +// 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.Allocation; +using osu.Framework.Audio; +using osu.Framework.Graphics; +using osu.Framework.Platform; +using osu.Framework.Utils; +using osu.Game.Beatmaps; +using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; +using osu.Game.Rulesets; +using osu.Game.Screens.OnlinePlay.Multiplayer.Match; +using osu.Game.Tests.Resources; +using osu.Game.Users; +using osuTK; +using osuTK.Input; + +namespace osu.Game.Tests.Visual.Multiplayer +{ + public class TestSceneMultiplayerReadyButton : MultiplayerTestScene + { + private MultiplayerReadyButton button; + + private BeatmapManager beatmaps; + private RulesetStore rulesets; + + [BackgroundDependencyLoader] + private void load(GameHost host, AudioManager audio) + { + Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); + Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, host, Beatmap.Default)); + beatmaps.Import(TestResources.GetTestBeatmapForImport(true)).Wait(); + } + + [SetUp] + public new void Setup() => Schedule(() => + { + var beatmap = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First().Beatmaps.First(); + + Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmap); + + Child = button = new MultiplayerReadyButton + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(200, 50), + SelectedItem = + { + Value = new PlaylistItem + { + Beatmap = { Value = beatmap }, + Ruleset = { Value = beatmap.Ruleset } + } + } + }; + }); + + [Test] + public void TestToggleStateWhenNotHost() + { + AddStep("add second user as host", () => + { + Client.AddUser(new User { Id = 2, Username = "Another user" }); + Client.TransferHost(2); + }); + + addClickButtonStep(); + AddAssert("user is ready", () => Client.Room?.Users[0].State == MultiplayerUserState.Ready); + + addClickButtonStep(); + AddAssert("user is idle", () => Client.Room?.Users[0].State == MultiplayerUserState.Idle); + } + + [TestCase(true)] + [TestCase(false)] + public void TestToggleStateWhenHost(bool allReady) + { + AddStep("setup", () => + { + Client.TransferHost(Client.Room?.Users[0].UserID ?? 0); + + if (!allReady) + Client.AddUser(new User { Id = 2, Username = "Another user" }); + }); + + addClickButtonStep(); + AddAssert("user is ready", () => Client.Room?.Users[0].State == MultiplayerUserState.Ready); + + addClickButtonStep(); + AddAssert("match started", () => Client.Room?.Users[0].State == MultiplayerUserState.WaitingForLoad); + } + + [Test] + public void TestBecomeHostWhileReady() + { + AddStep("add host", () => + { + Client.AddUser(new User { Id = 2, Username = "Another user" }); + Client.TransferHost(2); + }); + + addClickButtonStep(); + AddStep("make user host", () => Client.TransferHost(Client.Room?.Users[0].UserID ?? 0)); + + addClickButtonStep(); + AddAssert("match started", () => Client.Room?.Users[0].State == MultiplayerUserState.WaitingForLoad); + } + + [Test] + public void TestLoseHostWhileReady() + { + AddStep("setup", () => + { + Client.TransferHost(Client.Room?.Users[0].UserID ?? 0); + Client.AddUser(new User { Id = 2, Username = "Another user" }); + }); + + addClickButtonStep(); + AddStep("transfer host", () => Client.TransferHost(Client.Room?.Users[1].UserID ?? 0)); + + addClickButtonStep(); + AddAssert("match not started", () => Client.Room?.Users[0].State == MultiplayerUserState.Idle); + } + + [TestCase(true)] + [TestCase(false)] + public void TestManyUsersChangingState(bool isHost) + { + const int users = 10; + AddStep("setup", () => + { + Client.TransferHost(Client.Room?.Users[0].UserID ?? 0); + for (int i = 0; i < users; i++) + Client.AddUser(new User { Id = i, Username = "Another user" }); + }); + + if (!isHost) + AddStep("transfer host", () => Client.TransferHost(2)); + + addClickButtonStep(); + + AddRepeatStep("change user ready state", () => + { + Client.ChangeUserState(RNG.Next(0, users), RNG.NextBool() ? MultiplayerUserState.Ready : MultiplayerUserState.Idle); + }, 20); + + AddRepeatStep("ready all users", () => + { + var nextUnready = Client.Room?.Users.FirstOrDefault(c => c.State == MultiplayerUserState.Idle); + if (nextUnready != null) + Client.ChangeUserState(nextUnready.UserID, MultiplayerUserState.Ready); + }, users); + } + + private void addClickButtonStep() => AddStep("click button", () => + { + InputManager.MoveMouseTo(button); + InputManager.Click(MouseButton.Left); + }); + } +} diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerRoomManager.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerRoomManager.cs new file mode 100644 index 0000000000..7a3845cbf3 --- /dev/null +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerRoomManager.cs @@ -0,0 +1,153 @@ +// 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.Framework.Testing; +using osu.Game.Online.Rooms; + +namespace osu.Game.Tests.Visual.Multiplayer +{ + [HeadlessTest] + public class TestSceneMultiplayerRoomManager : RoomTestScene + { + private TestMultiplayerRoomContainer roomContainer; + private TestMultiplayerRoomManager roomManager => roomContainer.RoomManager; + + [Test] + public void TestPollsInitially() + { + AddStep("create room manager with a few rooms", () => + { + createRoomManager().With(d => d.OnLoadComplete += _ => + { + roomManager.CreateRoom(new Room { Name = { Value = "1" } }); + roomManager.PartRoom(); + roomManager.CreateRoom(new Room { Name = { Value = "2" } }); + roomManager.PartRoom(); + roomManager.ClearRooms(); + }); + }); + + AddAssert("manager polled for rooms", () => roomManager.Rooms.Count == 2); + AddAssert("initial rooms received", () => roomManager.InitialRoomsReceived.Value); + } + + [Test] + public void TestRoomsClearedOnDisconnection() + { + AddStep("create room manager with a few rooms", () => + { + createRoomManager().With(d => d.OnLoadComplete += _ => + { + roomManager.CreateRoom(new Room()); + roomManager.PartRoom(); + roomManager.CreateRoom(new Room()); + roomManager.PartRoom(); + }); + }); + + AddStep("disconnect", () => roomContainer.Client.Disconnect()); + + AddAssert("rooms cleared", () => roomManager.Rooms.Count == 0); + AddAssert("initial rooms not received", () => !roomManager.InitialRoomsReceived.Value); + } + + [Test] + public void TestRoomsPolledOnReconnect() + { + AddStep("create room manager with a few rooms", () => + { + createRoomManager().With(d => d.OnLoadComplete += _ => + { + roomManager.CreateRoom(new Room()); + roomManager.PartRoom(); + roomManager.CreateRoom(new Room()); + roomManager.PartRoom(); + }); + }); + + AddStep("disconnect", () => roomContainer.Client.Disconnect()); + AddStep("connect", () => roomContainer.Client.Connect()); + + AddAssert("manager polled for rooms", () => roomManager.Rooms.Count == 2); + AddAssert("initial rooms received", () => roomManager.InitialRoomsReceived.Value); + } + + [Test] + public void TestRoomsNotPolledWhenJoined() + { + AddStep("create room manager with a room", () => + { + createRoomManager().With(d => d.OnLoadComplete += _ => + { + roomManager.CreateRoom(new Room()); + roomManager.ClearRooms(); + }); + }); + + AddAssert("manager not polled for rooms", () => roomManager.Rooms.Count == 0); + AddAssert("initial rooms not received", () => !roomManager.InitialRoomsReceived.Value); + } + + [Test] + public void TestMultiplayerRoomJoinedWhenCreated() + { + AddStep("create room manager with a room", () => + { + createRoomManager().With(d => d.OnLoadComplete += _ => + { + roomManager.CreateRoom(new Room()); + }); + }); + + AddAssert("multiplayer room joined", () => roomContainer.Client.Room != null); + } + + [Test] + public void TestMultiplayerRoomPartedWhenAPIRoomParted() + { + AddStep("create room manager with a room", () => + { + createRoomManager().With(d => d.OnLoadComplete += _ => + { + roomManager.CreateRoom(new Room()); + roomManager.PartRoom(); + }); + }); + + AddAssert("multiplayer room parted", () => roomContainer.Client.Room == null); + } + + [Test] + public void TestMultiplayerRoomJoinedWhenAPIRoomJoined() + { + AddStep("create room manager with a room", () => + { + createRoomManager().With(d => d.OnLoadComplete += _ => + { + var r = new Room(); + roomManager.CreateRoom(r); + roomManager.PartRoom(); + roomManager.JoinRoom(r); + }); + }); + + AddAssert("multiplayer room joined", () => roomContainer.Client.Room != null); + } + + private TestMultiplayerRoomManager createRoomManager() + { + Child = roomContainer = new TestMultiplayerRoomContainer + { + RoomManager = + { + TimeBetweenListingPolls = { Value = 1 }, + TimeBetweenSelectionPolls = { Value = 1 } + } + }; + + return roomManager; + } + } +} diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomStatus.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomStatus.cs index c1dfb94464..cec40635f3 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomStatus.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomStatus.cs @@ -4,9 +4,9 @@ using System; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Game.Online.Multiplayer; -using osu.Game.Online.Multiplayer.RoomStatuses; -using osu.Game.Screens.Multi.Lounge.Components; +using osu.Game.Online.Rooms; +using osu.Game.Online.Rooms.RoomStatuses; +using osu.Game.Screens.OnlinePlay.Lounge.Components; namespace osu.Game.Tests.Visual.Multiplayer { @@ -22,22 +22,28 @@ namespace osu.Game.Tests.Visual.Multiplayer { new DrawableRoom(new Room { - Name = { Value = "Room 1" }, + Name = { Value = "Open - ending in 1 day" }, Status = { Value = new RoomStatusOpen() }, EndDate = { Value = DateTimeOffset.Now.AddDays(1) } }) { MatchingFilter = true }, new DrawableRoom(new Room { - Name = { Value = "Room 2" }, + Name = { Value = "Playing - ending in 1 day" }, Status = { Value = new RoomStatusPlaying() }, EndDate = { Value = DateTimeOffset.Now.AddDays(1) } }) { MatchingFilter = true }, new DrawableRoom(new Room { - Name = { Value = "Room 3" }, + Name = { Value = "Ended" }, Status = { Value = new RoomStatusEnded() }, EndDate = { Value = DateTimeOffset.Now } }) { MatchingFilter = true }, + new DrawableRoom(new Room + { + Name = { Value = "Open" }, + Status = { Value = new RoomStatusOpen() }, + Category = { Value = RoomCategory.Realtime } + }) { MatchingFilter = true }, } }; } diff --git a/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs b/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs index a003b9ae4d..f0ddefa51d 100644 --- a/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs +++ b/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs @@ -55,8 +55,14 @@ namespace osu.Game.Tests.Visual.Navigation var secondimport = importBeatmap(3); presentAndConfirm(secondimport); + // Test presenting same beatmap more than once + presentAndConfirm(secondimport); + presentSecondDifficultyAndConfirm(firstImport, 1); presentSecondDifficultyAndConfirm(secondimport, 3); + + // Test presenting same beatmap more than once + presentSecondDifficultyAndConfirm(secondimport, 3); } [Test] diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index d87854a7ea..8480e6eaaa 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -107,14 +107,14 @@ namespace osu.Game.Tests.Visual.Navigation [Test] public void TestExitMultiWithEscape() { - PushAndConfirm(() => new Screens.Multi.Multiplayer()); + PushAndConfirm(() => new Screens.OnlinePlay.Playlists.Playlists()); exitViaEscapeAndConfirm(); } [Test] public void TestExitMultiWithBackButton() { - PushAndConfirm(() => new Screens.Multi.Multiplayer()); + PushAndConfirm(() => new Screens.OnlinePlay.Playlists.Playlists()); exitViaBackButtonAndConfirm(); } diff --git a/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs index 02f6de2269..998e42b478 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs @@ -41,6 +41,7 @@ namespace osu.Game.Tests.Visual.Online } [Test] + [Ignore("needs to be updated to not be so server dependent")] public void ShowWithBuild() { AddStep(@"Show with Lazer 2018.712.0", () => @@ -49,7 +50,7 @@ namespace osu.Game.Tests.Visual.Online { Version = "2018.712.0", DisplayVersion = "2018.712.0", - UpdateStream = new APIUpdateStream { Id = 7, Name = OsuGameBase.CLIENT_STREAM_NAME }, + UpdateStream = new APIUpdateStream { Id = 5, Name = OsuGameBase.CLIENT_STREAM_NAME }, ChangelogEntries = new List { new APIChangelogEntry @@ -64,7 +65,7 @@ namespace osu.Game.Tests.Visual.Online AddUntilStep(@"wait for streams", () => changelog.Streams?.Count > 0); AddAssert(@"correct build displayed", () => changelog.Current.Value.Version == "2018.712.0"); - AddAssert(@"correct stream selected", () => changelog.Header.Streams.Current.Value.Id == 7); + AddAssert(@"correct stream selected", () => changelog.Header.Streams.Current.Value.Id == 5); } [Test] diff --git a/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs index 7eba64f418..1666c9cde4 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs @@ -69,8 +69,32 @@ namespace osu.Game.Tests.Visual.Online internal class TestUserLookupCache : UserLookupCache { + private static readonly string[] usernames = + { + "fieryrage", + "Kerensa", + "MillhioreF", + "Player01", + "smoogipoo", + "Ephemeral", + "BTMC", + "Cilvery", + "m980", + "HappyStick", + "LittleEndu", + "frenzibyte", + "Zallius", + "BanchoBot", + "rocketminer210", + "pishifat" + }; + protected override Task ComputeValueAsync(int lookup, CancellationToken token = default) - => Task.FromResult(new User { Username = "peppy", Id = 2 }); + => Task.FromResult(new User + { + Id = lookup, + Username = usernames[lookup % usernames.Length], + }); } } } diff --git a/osu.Game.Tests/Visual/Online/TestSceneNowPlayingCommand.cs b/osu.Game.Tests/Visual/Online/TestSceneNowPlayingCommand.cs index 0324da6cf5..64e80e9f02 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneNowPlayingCommand.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneNowPlayingCommand.cs @@ -70,7 +70,7 @@ namespace osu.Game.Tests.Visual.Online AddStep("Run command", () => Add(new NowPlayingCommand())); if (hasOnlineId) - AddAssert("Check link presence", () => postTarget.LastMessage.Contains("https://osu.ppy.sh/b/1234")); + AddAssert("Check link presence", () => postTarget.LastMessage.Contains("/b/1234")); else AddAssert("Check link not present", () => !postTarget.LastMessage.Contains("https://")); } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftFilterControl.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsFilterControl.cs similarity index 62% rename from osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftFilterControl.cs rename to osu.Game.Tests/Visual/Playlists/TestScenePlaylistsFilterControl.cs index f635a28b5c..40e191dd7e 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftFilterControl.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsFilterControl.cs @@ -2,15 +2,15 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Graphics; -using osu.Game.Screens.Multi.Lounge.Components; +using osu.Game.Screens.OnlinePlay.Lounge.Components; -namespace osu.Game.Tests.Visual.Multiplayer +namespace osu.Game.Tests.Visual.Playlists { - public class TestSceneTimeshiftFilterControl : OsuTestScene + public class TestScenePlaylistsFilterControl : OsuTestScene { - public TestSceneTimeshiftFilterControl() + public TestScenePlaylistsFilterControl() { - Child = new TimeshiftFilterControl + Child = new PlaylistsFilterControl { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeSubScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs similarity index 84% rename from osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeSubScreen.cs rename to osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs index 68987127d2..008c862cc3 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeSubScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs @@ -8,12 +8,14 @@ using osu.Framework.Graphics; using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Graphics.Containers; -using osu.Game.Screens.Multi.Lounge; -using osu.Game.Screens.Multi.Lounge.Components; +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; -namespace osu.Game.Tests.Visual.Multiplayer +namespace osu.Game.Tests.Visual.Playlists { - public class TestSceneLoungeSubScreen : RoomManagerTestScene + public class TestScenePlaylistsLoungeSubScreen : RoomManagerTestScene { private LoungeSubScreen loungeScreen; @@ -26,7 +28,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { base.SetUpSteps(); - AddStep("push screen", () => LoadScreen(loungeScreen = new LoungeSubScreen + AddStep("push screen", () => LoadScreen(loungeScreen = new PlaylistsLoungeSubScreen { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs similarity index 86% rename from osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs rename to osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs index cbe8cc6137..44a79b6598 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs @@ -9,13 +9,13 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; -using osu.Game.Online.Multiplayer; -using osu.Game.Screens.Multi; -using osu.Game.Screens.Multi.Match.Components; +using osu.Game.Online.Rooms; +using osu.Game.Screens.OnlinePlay; +using osu.Game.Screens.OnlinePlay.Playlists; -namespace osu.Game.Tests.Visual.Multiplayer +namespace osu.Game.Tests.Visual.Playlists { - public class TestSceneMatchSettingsOverlay : MultiplayerTestScene + public class TestScenePlaylistsMatchSettingsOverlay : RoomTestScene { [Cached(Type = typeof(IRoomManager))] private TestRoomManager roomManager = new TestRoomManager(); @@ -109,14 +109,14 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("error not displayed", () => !settings.ErrorText.IsPresent); } - private class TestRoomSettings : MatchSettingsOverlay + private class TestRoomSettings : PlaylistsMatchSettingsOverlay { - public TriangleButton ApplyButton => Settings.ApplyButton; + public TriangleButton ApplyButton => ((MatchSettings)Settings).ApplyButton; - public OsuTextBox NameField => Settings.NameField; - public OsuDropdown DurationField => Settings.DurationField; + public OsuTextBox NameField => ((MatchSettings)Settings).NameField; + public OsuDropdown DurationField => ((MatchSettings)Settings).DurationField; - public OsuSpriteText ErrorText => Settings.ErrorText; + public OsuSpriteText ErrorText => ((MatchSettings)Settings).ErrorText; } private class TestRoomManager : IRoomManager @@ -131,7 +131,7 @@ namespace osu.Game.Tests.Visual.Multiplayer remove { } } - public Bindable InitialRoomsReceived { get; } = new Bindable(true); + public IBindable InitialRoomsReceived { get; } = new Bindable(true); public IBindableList Rooms => null; diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneParticipantsList.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsParticipantsList.cs similarity index 87% rename from osu.Game.Tests/Visual/Multiplayer/TestSceneParticipantsList.cs rename to osu.Game.Tests/Visual/Playlists/TestScenePlaylistsParticipantsList.cs index 7bbec7d30e..8dd81e02e2 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneParticipantsList.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsParticipantsList.cs @@ -3,12 +3,12 @@ using NUnit.Framework; using osu.Framework.Graphics; -using osu.Game.Screens.Multi.Components; +using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Users; -namespace osu.Game.Tests.Visual.Multiplayer +namespace osu.Game.Tests.Visual.Playlists { - public class TestSceneParticipantsList : MultiplayerTestScene + public class TestScenePlaylistsParticipantsList : RoomTestScene { [SetUp] public new void Setup() => Schedule(() => @@ -20,6 +20,7 @@ namespace osu.Game.Tests.Visual.Multiplayer Room.RecentParticipants.Add(new User { Username = "peppy", + CurrentModeRank = 1234, Id = 2 }); } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs similarity index 98% rename from osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs rename to osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs index 03fd2b968c..cdcded8f61 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs @@ -15,18 +15,18 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Online.API.Requests; -using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; -using osu.Game.Screens.Multi.Ranking; +using osu.Game.Screens.OnlinePlay.Playlists; using osu.Game.Screens.Ranking; using osu.Game.Tests.Beatmaps; using osu.Game.Users; -namespace osu.Game.Tests.Visual.Multiplayer +namespace osu.Game.Tests.Visual.Playlists { - public class TestSceneTimeshiftResultsScreen : ScreenTestScene + public class TestScenePlaylistsResultsScreen : ScreenTestScene { private const int scores_per_result = 10; @@ -360,7 +360,7 @@ namespace osu.Game.Tests.Visual.Multiplayer }; } - private class TestResultsScreen : TimeshiftResultsScreen + private class TestResultsScreen : PlaylistsResultsScreen { public new LoadingSpinner LeftSpinner => base.LeftSpinner; public new LoadingSpinner CentreSpinner => base.CentreSpinner; diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSubScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs similarity index 88% rename from osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSubScreen.cs rename to osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs index 65e9893851..a4c87d3ace 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs @@ -12,19 +12,19 @@ using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterface; -using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; -using osu.Game.Screens.Multi; -using osu.Game.Screens.Multi.Match; -using osu.Game.Screens.Multi.Match.Components; +using osu.Game.Screens.OnlinePlay; +using osu.Game.Screens.OnlinePlay.Match.Components; +using osu.Game.Screens.OnlinePlay.Playlists; using osu.Game.Tests.Beatmaps; using osu.Game.Users; using osuTK.Input; -namespace osu.Game.Tests.Visual.Multiplayer +namespace osu.Game.Tests.Visual.Playlists { - public class TestSceneMatchSubScreen : MultiplayerTestScene + public class TestScenePlaylistsRoomSubScreen : RoomTestScene { protected override bool UseOnlineAPI => true; @@ -34,7 +34,7 @@ namespace osu.Game.Tests.Visual.Multiplayer private BeatmapManager manager; private RulesetStore rulesets; - private TestMatchSubScreen match; + private TestPlaylistsRoomSubScreen match; [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) @@ -48,7 +48,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [SetUpSteps] public void SetupSteps() { - AddStep("load match", () => LoadScreen(match = new TestMatchSubScreen(Room))); + AddStep("load match", () => LoadScreen(match = new TestPlaylistsRoomSubScreen(Room))); AddUntilStep("wait for load", () => match.IsCurrentScreen()); } @@ -120,7 +120,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("create room", () => { - InputManager.MoveMouseTo(match.ChildrenOfType().Single()); + InputManager.MoveMouseTo(match.ChildrenOfType().Single()); InputManager.Click(MouseButton.Left); }); @@ -131,13 +131,13 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("match has original beatmap", () => match.Beatmap.Value.Beatmap.BeatmapInfo.BaseDifficulty.CircleSize != 1); } - private class TestMatchSubScreen : MatchSubScreen + private class TestPlaylistsRoomSubScreen : PlaylistsRoomSubScreen { public new Bindable SelectedItem => base.SelectedItem; public new Bindable Beatmap => base.Beatmap; - public TestMatchSubScreen(Room room) + public TestPlaylistsRoomSubScreen(Room room) : base(room) { } @@ -151,7 +151,7 @@ namespace osu.Game.Tests.Visual.Multiplayer remove => throw new NotImplementedException(); } - public Bindable InitialRoomsReceived { get; } = new Bindable(true); + public IBindable InitialRoomsReceived { get; } = new Bindable(true); public IBindableList Rooms { get; } = new BindableList(); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsScreen.cs similarity index 72% rename from osu.Game.Tests/Visual/Multiplayer/TestSceneMultiScreen.cs rename to osu.Game.Tests/Visual/Playlists/TestScenePlaylistsScreen.cs index 3924b0333f..e52f823f0b 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsScreen.cs @@ -5,19 +5,19 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Game.Overlays; -namespace osu.Game.Tests.Visual.Multiplayer +namespace osu.Game.Tests.Visual.Playlists { [TestFixture] - public class TestSceneMultiScreen : ScreenTestScene + public class TestScenePlaylistsScreen : ScreenTestScene { protected override bool UseOnlineAPI => true; [Cached] private MusicController musicController { get; set; } = new MusicController(); - public TestSceneMultiScreen() + public TestScenePlaylistsScreen() { - Screens.Multi.Multiplayer multi = new Screens.Multi.Multiplayer(); + var multi = new Screens.OnlinePlay.Playlists.Playlists(); AddStep("show", () => LoadScreen(multi)); AddUntilStep("wait for loaded", () => multi.IsLoaded); diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs new file mode 100644 index 0000000000..53a956c77c --- /dev/null +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs @@ -0,0 +1,211 @@ +// 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 NUnit.Framework; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Catch; +using osu.Game.Rulesets.Mania; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Taiko; +using osu.Game.Tests.Visual.Navigation; +using osu.Game.Users; + +namespace osu.Game.Tests.Visual.SongSelect +{ + public class TestSceneBeatmapRecommendations : OsuGameTestScene + { + [SetUpSteps] + public override void SetUpSteps() + { + AddStep("register request handling", () => + { + ((DummyAPIAccess)API).HandleRequest = req => + { + switch (req) + { + case GetUserRequest userRequest: + userRequest.TriggerSuccess(getUser(userRequest.Ruleset.ID)); + break; + } + }; + }); + + base.SetUpSteps(); + + User getUser(int? rulesetID) + { + return new User + { + Username = @"Dummy", + Id = 1001, + Statistics = new UserStatistics + { + PP = getNecessaryPP(rulesetID) + } + }; + } + + decimal getNecessaryPP(int? rulesetID) + { + switch (rulesetID) + { + case 0: + return 336; // recommended star rating of 2 + + case 1: + return 928; // SR 3 + + case 2: + return 1905; // SR 4 + + case 3: + return 3329; // SR 5 + + default: + return 0; + } + } + } + + [Test] + public void TestPresentedBeatmapIsRecommended() + { + List beatmapSets = null; + const int import_count = 5; + + AddStep("import 5 maps", () => + { + beatmapSets = new List(); + + for (int i = 0; i < import_count; ++i) + { + beatmapSets.Add(importBeatmapSet(i, Enumerable.Repeat(new OsuRuleset().RulesetInfo, 5))); + } + }); + + AddAssert("all sets imported", () => ensureAllBeatmapSetsImported(beatmapSets)); + + presentAndConfirm(() => beatmapSets[3], 2); + } + + [Test] + public void TestCurrentRulesetIsRecommended() + { + BeatmapSetInfo catchSet = null, mixedSet = null; + + AddStep("create catch beatmapset", () => catchSet = importBeatmapSet(0, new[] { new CatchRuleset().RulesetInfo })); + AddStep("create mixed beatmapset", () => mixedSet = importBeatmapSet(1, + new[] { new TaikoRuleset().RulesetInfo, new CatchRuleset().RulesetInfo, new ManiaRuleset().RulesetInfo })); + + AddAssert("all sets imported", () => ensureAllBeatmapSetsImported(new[] { catchSet, mixedSet })); + + // Switch to catch + presentAndConfirm(() => catchSet, 1); + + // Present mixed difficulty set, expect current ruleset to be selected + presentAndConfirm(() => mixedSet, 2); + } + + [Test] + public void TestBestRulesetIsRecommended() + { + BeatmapSetInfo osuSet = null, mixedSet = null; + + AddStep("create osu! beatmapset", () => osuSet = importBeatmapSet(0, new[] { new OsuRuleset().RulesetInfo })); + AddStep("create mixed beatmapset", () => mixedSet = importBeatmapSet(1, + new[] { new TaikoRuleset().RulesetInfo, new CatchRuleset().RulesetInfo, new ManiaRuleset().RulesetInfo })); + + AddAssert("all sets imported", () => ensureAllBeatmapSetsImported(new[] { osuSet, mixedSet })); + + // Make sure we are on standard ruleset + presentAndConfirm(() => osuSet, 1); + + // Present mixed difficulty set, expect ruleset with highest star difficulty + presentAndConfirm(() => mixedSet, 3); + } + + [Test] + public void TestSecondBestRulesetIsRecommended() + { + BeatmapSetInfo osuSet = null, mixedSet = null; + + AddStep("create osu! beatmapset", () => osuSet = importBeatmapSet(0, new[] { new OsuRuleset().RulesetInfo })); + AddStep("create mixed beatmapset", () => mixedSet = importBeatmapSet(1, + new[] { new TaikoRuleset().RulesetInfo, new CatchRuleset().RulesetInfo, new TaikoRuleset().RulesetInfo })); + + AddAssert("all sets imported", () => ensureAllBeatmapSetsImported(new[] { osuSet, mixedSet })); + + // Make sure we are on standard ruleset + presentAndConfirm(() => osuSet, 1); + + // Present mixed difficulty set, expect ruleset with second highest star difficulty + presentAndConfirm(() => mixedSet, 2); + } + + [Test] + public void TestCorrectStarRatingIsUsed() + { + BeatmapSetInfo osuSet = null, maniaSet = null; + + AddStep("create osu! beatmapset", () => osuSet = importBeatmapSet(0, new[] { new OsuRuleset().RulesetInfo })); + AddStep("create mania beatmapset", () => maniaSet = importBeatmapSet(1, Enumerable.Repeat(new ManiaRuleset().RulesetInfo, 10))); + + AddAssert("all sets imported", () => ensureAllBeatmapSetsImported(new[] { osuSet, maniaSet })); + + // Make sure we are on standard ruleset + presentAndConfirm(() => osuSet, 1); + + // Present mania set, expect the difficulty that matches recommended mania star rating + presentAndConfirm(() => maniaSet, 5); + } + + private BeatmapSetInfo importBeatmapSet(int importID, IEnumerable difficultyRulesets) + { + var metadata = new BeatmapMetadata + { + Artist = "SomeArtist", + AuthorString = "SomeAuthor", + Title = $"import {importID}" + }; + + var beatmapSet = new BeatmapSetInfo + { + Hash = Guid.NewGuid().ToString(), + OnlineBeatmapSetID = importID, + Metadata = metadata, + Beatmaps = difficultyRulesets.Select((ruleset, difficultyIndex) => new BeatmapInfo + { + OnlineBeatmapID = importID * 1024 + difficultyIndex, + Metadata = metadata, + BaseDifficulty = new BeatmapDifficulty(), + Ruleset = ruleset, + StarDifficulty = difficultyIndex + 1, + Version = $"SR{difficultyIndex + 1}" + }).ToList() + }; + + return Game.BeatmapManager.Import(beatmapSet).Result; + } + + private bool ensureAllBeatmapSetsImported(IEnumerable beatmapSets) => beatmapSets.All(set => set != null); + + private void presentAndConfirm(Func getImport, int expectedDiff) + { + AddStep("present beatmap", () => Game.PresentBeatmap(getImport())); + + AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is Screens.Select.SongSelect); + AddUntilStep("recommended beatmap displayed", () => + { + int? expectedID = getImport().Beatmaps[expectedDiff - 1].OnlineBeatmapID; + return Game.Beatmap.Value.BeatmapInfo.OnlineBeatmapID == expectedID; + }); + } + } +} diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 33e024fa28..42418e532b 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -16,6 +16,7 @@ using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Graphics.Textures; +using osu.Framework.IO.Stores; using osu.Framework.Lists; using osu.Framework.Logging; using osu.Framework.Platform; @@ -28,8 +29,8 @@ using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Rulesets; using osu.Game.Rulesets.Objects; -using osu.Game.Users; using osu.Game.Skinning; +using osu.Game.Users; using Decoder = osu.Game.Beatmaps.Formats.Decoder; namespace osu.Game.Beatmaps @@ -38,7 +39,7 @@ namespace osu.Game.Beatmaps /// Handles the storage and retrieval of Beatmaps/WorkingBeatmaps. /// [ExcludeFromDynamicCompile] - public partial class BeatmapManager : DownloadableArchiveModelManager, IDisposable + public partial class BeatmapManager : DownloadableArchiveModelManager, IDisposable, IBeatmapResourceProvider { /// /// Fired when a single difficulty has been hidden. @@ -68,9 +69,12 @@ namespace osu.Game.Beatmaps private readonly RulesetStore rulesets; private readonly BeatmapStore beatmaps; private readonly AudioManager audioManager; - private readonly TextureStore textureStore; + private readonly LargeTextureStore largeTextureStore; private readonly ITrackStore trackStore; + [CanBeNull] + private readonly GameHost host; + [CanBeNull] private readonly BeatmapOnlineLookupQueue onlineLookupQueue; @@ -80,6 +84,7 @@ namespace osu.Game.Beatmaps { this.rulesets = rulesets; this.audioManager = audioManager; + this.host = host; DefaultBeatmap = defaultBeatmap; @@ -92,7 +97,7 @@ namespace osu.Game.Beatmaps if (performOnlineLookups) onlineLookupQueue = new BeatmapOnlineLookupQueue(api, storage); - textureStore = new LargeTextureStore(host?.CreateTextureLoaderStore(Files.Store)); + largeTextureStore = new LargeTextureStore(host?.CreateTextureLoaderStore(Files.Store)); trackStore = audioManager.GetTrackStore(Files.Store); } @@ -302,7 +307,7 @@ namespace osu.Game.Beatmaps beatmapInfo.Metadata ??= beatmapInfo.BeatmapSet.Metadata; - workingCache.Add(working = new BeatmapManagerWorkingBeatmap(Files.Store, textureStore, trackStore, beatmapInfo, audioManager)); + workingCache.Add(working = new BeatmapManagerWorkingBeatmap(beatmapInfo, this)); return working; } @@ -492,6 +497,16 @@ namespace osu.Game.Beatmaps onlineLookupQueue?.Dispose(); } + #region IResourceStorageProvider + + TextureStore IBeatmapResourceProvider.LargeTextureStore => largeTextureStore; + ITrackStore IBeatmapResourceProvider.Tracks => trackStore; + AudioManager IStorageResourceProvider.AudioManager => audioManager; + IResourceStore IStorageResourceProvider.Files => Files.Store; + IResourceStore IStorageResourceProvider.CreateTextureLoaderStore(IResourceStore underlyingStore) => host?.CreateTextureLoaderStore(underlyingStore); + + #endregion + /// /// A dummy WorkingBeatmap for the purpose of retrieving a beatmap for star difficulty calculation. /// diff --git a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs index f5c0d97c1f..62cf29dc03 100644 --- a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs @@ -2,11 +2,10 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Diagnostics.CodeAnalysis; using System.Linq; -using osu.Framework.Audio; using osu.Framework.Audio.Track; using osu.Framework.Graphics.Textures; -using osu.Framework.IO.Stores; using osu.Framework.Logging; using osu.Framework.Testing; using osu.Game.Beatmaps.Formats; @@ -21,16 +20,13 @@ namespace osu.Game.Beatmaps [ExcludeFromDynamicCompile] private class BeatmapManagerWorkingBeatmap : WorkingBeatmap { - private readonly IResourceStore store; - private readonly TextureStore textureStore; - private readonly ITrackStore trackStore; + [NotNull] + private readonly IBeatmapResourceProvider resources; - public BeatmapManagerWorkingBeatmap(IResourceStore store, TextureStore textureStore, ITrackStore trackStore, BeatmapInfo beatmapInfo, AudioManager audioManager) - : base(beatmapInfo, audioManager) + public BeatmapManagerWorkingBeatmap(BeatmapInfo beatmapInfo, [NotNull] IBeatmapResourceProvider resources) + : base(beatmapInfo, resources.AudioManager) { - this.store = store; - this.textureStore = textureStore; - this.trackStore = trackStore; + this.resources = resources; } protected override IBeatmap GetBeatmap() @@ -40,7 +36,7 @@ namespace osu.Game.Beatmaps try { - using (var stream = new LineBufferedReader(store.GetStream(getPathForFile(BeatmapInfo.Path)))) + using (var stream = new LineBufferedReader(resources.Files.GetStream(getPathForFile(BeatmapInfo.Path)))) return Decoder.GetDecoder(stream).Decode(stream); } catch (Exception e) @@ -61,7 +57,7 @@ namespace osu.Game.Beatmaps try { - return textureStore.Get(getPathForFile(Metadata.BackgroundFile)); + return resources.LargeTextureStore.Get(getPathForFile(Metadata.BackgroundFile)); } catch (Exception e) { @@ -77,7 +73,7 @@ namespace osu.Game.Beatmaps try { - return trackStore.Get(getPathForFile(Metadata.AudioFile)); + return resources.Tracks.Get(getPathForFile(Metadata.AudioFile)); } catch (Exception e) { @@ -93,7 +89,7 @@ namespace osu.Game.Beatmaps try { - var trackData = store.GetStream(getPathForFile(Metadata.AudioFile)); + var trackData = resources.Files.GetStream(getPathForFile(Metadata.AudioFile)); return trackData == null ? null : new Waveform(trackData); } catch (Exception e) @@ -109,7 +105,7 @@ namespace osu.Game.Beatmaps try { - using (var stream = new LineBufferedReader(store.GetStream(getPathForFile(BeatmapInfo.Path)))) + using (var stream = new LineBufferedReader(resources.Files.GetStream(getPathForFile(BeatmapInfo.Path)))) { var decoder = Decoder.GetDecoder(stream); @@ -118,7 +114,7 @@ namespace osu.Game.Beatmaps storyboard = decoder.Decode(stream); else { - using (var secondaryStream = new LineBufferedReader(store.GetStream(getPathForFile(BeatmapSetInfo.StoryboardFile)))) + using (var secondaryStream = new LineBufferedReader(resources.Files.GetStream(getPathForFile(BeatmapSetInfo.StoryboardFile)))) storyboard = decoder.Decode(stream, secondaryStream); } } @@ -138,7 +134,7 @@ namespace osu.Game.Beatmaps { try { - return new LegacyBeatmapSkin(BeatmapInfo, store, AudioManager); + return new LegacyBeatmapSkin(BeatmapInfo, resources.Files, resources); } catch (Exception e) { diff --git a/osu.Game/Screens/Select/DifficultyRecommender.cs b/osu.Game/Beatmaps/DifficultyRecommender.cs similarity index 56% rename from osu.Game/Screens/Select/DifficultyRecommender.cs rename to osu.Game/Beatmaps/DifficultyRecommender.cs index ff54e0a8df..340c47d89b 100644 --- a/osu.Game/Screens/Select/DifficultyRecommender.cs +++ b/osu.Game/Beatmaps/DifficultyRecommender.cs @@ -4,17 +4,21 @@ using System; using System.Collections.Generic; using System.Linq; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; -using osu.Game.Beatmaps; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Rulesets; -namespace osu.Game.Screens.Select +namespace osu.Game.Beatmaps { + /// + /// A class which will recommend the most suitable difficulty for the local user from a beatmap set. + /// This requires the user to be logged in, as it sources from the user's online profile. + /// public class DifficultyRecommender : Component { [Resolved] @@ -26,7 +30,12 @@ namespace osu.Game.Screens.Select [Resolved] private Bindable ruleset { get; set; } - private readonly Dictionary recommendedStarDifficulty = new Dictionary(); + /// + /// The user for which the last requests were run. + /// + private int? requestedUserId; + + private readonly Dictionary recommendedDifficultyMapping = new Dictionary(); private readonly IBindable apiState = new Bindable(); @@ -45,42 +54,64 @@ namespace osu.Game.Screens.Select /// /// A collection of beatmaps to select a difficulty from. /// The recommended difficulty, or null if a recommendation could not be provided. + [CanBeNull] public BeatmapInfo GetRecommendedBeatmap(IEnumerable beatmaps) { - if (recommendedStarDifficulty.TryGetValue(ruleset.Value, out var stars)) + foreach (var r in orderedRulesets) { - return beatmaps.OrderBy(b => + if (!recommendedDifficultyMapping.TryGetValue(r, out var recommendation)) + continue; + + BeatmapInfo beatmap = beatmaps.Where(b => b.Ruleset.Equals(r)).OrderBy(b => { - var difference = b.StarDifficulty - stars; + var difference = b.StarDifficulty - recommendation; return difference >= 0 ? difference * 2 : difference * -1; // prefer easier over harder }).FirstOrDefault(); + + if (beatmap != null) + return beatmap; } return null; } - private void calculateRecommendedDifficulties() + private void fetchRecommendedValues() { - rulesets.AvailableRulesets.ForEach(rulesetInfo => + if (recommendedDifficultyMapping.Count > 0 && api.LocalUser.Value.Id == requestedUserId) + return; + + requestedUserId = api.LocalUser.Value.Id; + + // only query API for built-in rulesets + rulesets.AvailableRulesets.Where(ruleset => ruleset.ID <= ILegacyRuleset.MAX_LEGACY_RULESET_ID).ForEach(rulesetInfo => { var req = new GetUserRequest(api.LocalUser.Value.Id, rulesetInfo); req.Success += result => { // algorithm taken from https://github.com/ppy/osu-web/blob/e6e2825516449e3d0f3f5e1852c6bdd3428c3437/app/Models/User.php#L1505 - recommendedStarDifficulty[rulesetInfo] = Math.Pow((double)(result.Statistics.PP ?? 0), 0.4) * 0.195; + recommendedDifficultyMapping[rulesetInfo] = Math.Pow((double)(result.Statistics.PP ?? 0), 0.4) * 0.195; }; api.Queue(req); }); } + /// + /// Rulesets ordered descending by their respective recommended difficulties. + /// The currently selected ruleset will always be first. + /// + private IEnumerable orderedRulesets => + recommendedDifficultyMapping + .OrderByDescending(pair => pair.Value).Select(pair => pair.Key).Where(r => !r.Equals(ruleset.Value)) + .Prepend(ruleset.Value); + private void onlineStateChanged(ValueChangedEvent state) => Schedule(() => { switch (state.NewValue) { case APIState.Online: - calculateRecommendedDifficulties(); + fetchRecommendedValues(); break; } }); diff --git a/osu.Game/Beatmaps/IBeatmapResourceProvider.cs b/osu.Game/Beatmaps/IBeatmapResourceProvider.cs new file mode 100644 index 0000000000..dfea0c7a30 --- /dev/null +++ b/osu.Game/Beatmaps/IBeatmapResourceProvider.cs @@ -0,0 +1,22 @@ +// 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.Audio.Track; +using osu.Framework.Graphics.Textures; +using osu.Game.IO; + +namespace osu.Game.Beatmaps +{ + public interface IBeatmapResourceProvider : IStorageResourceProvider + { + /// + /// Retrieve a global large texture store, used for loading beatmap backgrounds. + /// + TextureStore LargeTextureStore { get; } + + /// + /// Access a global track store for retrieving beatmap tracks from. + /// + ITrackStore Tracks { get; } + } +} diff --git a/osu.Game/Configuration/DevelopmentOsuConfigManager.cs b/osu.Game/Configuration/DevelopmentOsuConfigManager.cs new file mode 100644 index 0000000000..ff19dd874c --- /dev/null +++ b/osu.Game/Configuration/DevelopmentOsuConfigManager.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.Platform; +using osu.Framework.Testing; + +namespace osu.Game.Configuration +{ + [ExcludeFromDynamicCompile] + public class DevelopmentOsuConfigManager : OsuConfigManager + { + protected override string Filename => base.Filename.Replace(".ini", ".dev.ini"); + + public DevelopmentOsuConfigManager(Storage storage) + : base(storage) + { + } + } +} diff --git a/osu.Game/Extensions/TaskExtensions.cs b/osu.Game/Extensions/TaskExtensions.cs new file mode 100644 index 0000000000..a1215d786b --- /dev/null +++ b/osu.Game/Extensions/TaskExtensions.cs @@ -0,0 +1,26 @@ +// 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.Tasks; +using osu.Framework.Logging; + +namespace osu.Game.Extensions +{ + public static class TaskExtensions + { + /// + /// Denote a task which is to be run without local error handling logic, where failure is not catastrophic. + /// Avoids unobserved exceptions from being fired. + /// + /// The task. + /// Whether errors should be logged as important, or silently ignored. + public static void CatchUnobservedExceptions(this Task task, bool logOnError = false) + { + task.ContinueWith(t => + { + if (logOnError) + Logger.Log($"Error running task: {t.Exception?.Message ?? "unknown"}", LoggingTarget.Runtime, LogLevel.Important); + }, TaskContinuationOptions.NotOnRanToCompletion); + } + } +} diff --git a/osu.Game/IO/IStorageResourceProvider.cs b/osu.Game/IO/IStorageResourceProvider.cs new file mode 100644 index 0000000000..cbd1039807 --- /dev/null +++ b/osu.Game/IO/IStorageResourceProvider.cs @@ -0,0 +1,29 @@ +// 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.Audio; +using osu.Framework.Graphics.Textures; +using osu.Framework.IO.Stores; + +namespace osu.Game.IO +{ + public interface IStorageResourceProvider + { + /// + /// Retrieve the game-wide audio manager. + /// + AudioManager AudioManager { get; } + + /// + /// Access game-wide user files. + /// + IResourceStore Files { get; } + + /// + /// Create a texture loader store based on an underlying data store. + /// + /// The underlying provider of texture data (in arbitrary image formats). + /// A texture loader store. + IResourceStore CreateTextureLoaderStore(IResourceStore underlyingStore); + } +} diff --git a/osu.Game/IO/Serialization/Converters/SnakeCaseStringEnumConverter.cs b/osu.Game/IO/Serialization/Converters/SnakeCaseStringEnumConverter.cs new file mode 100644 index 0000000000..1d82a5bc87 --- /dev/null +++ b/osu.Game/IO/Serialization/Converters/SnakeCaseStringEnumConverter.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. + +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Serialization; + +namespace osu.Game.IO.Serialization.Converters +{ + public class SnakeCaseStringEnumConverter : StringEnumConverter + { + public SnakeCaseStringEnumConverter() + { + NamingStrategy = new SnakeCaseNamingStrategy(); + } + } +} diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index fe500b9548..133ba22406 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -26,12 +26,12 @@ namespace osu.Game.Online.API private readonly OAuth authentication; - public string Endpoint => @"https://osu.ppy.sh"; - private const string client_id = @"5"; - private const string client_secret = @"FGc9GAtyHzeQDshWP5Ah7dega8hJACAJpQtw6OXk"; - private readonly Queue queue = new Queue(); + public string APIEndpointUrl { get; } + + public string WebsiteRootUrl { get; } + /// /// The username/email provided by the user when initiating a login. /// @@ -55,11 +55,14 @@ namespace osu.Game.Online.API private readonly Logger log; - public APIAccess(OsuConfigManager config) + public APIAccess(OsuConfigManager config, EndpointConfiguration endpointConfiguration) { this.config = config; - authentication = new OAuth(client_id, client_secret, Endpoint); + APIEndpointUrl = endpointConfiguration.APIEndpointUrl; + WebsiteRootUrl = endpointConfiguration.WebsiteRootUrl; + + authentication = new OAuth(endpointConfiguration.APIClientID, endpointConfiguration.APIClientSecret, APIEndpointUrl); log = Logger.GetLogger(LoggingTarget.Network); ProvidedUsername = config.Get(OsuSetting.Username); @@ -245,7 +248,7 @@ namespace osu.Game.Online.API var req = new RegistrationRequest { - Url = $@"{Endpoint}/users", + Url = $@"{APIEndpointUrl}/users", Method = HttpMethod.Post, Username = username, Email = email, diff --git a/osu.Game/Online/API/APIRequest.cs b/osu.Game/Online/API/APIRequest.cs index 6912d9b629..a7174324d8 100644 --- a/osu.Game/Online/API/APIRequest.cs +++ b/osu.Game/Online/API/APIRequest.cs @@ -57,7 +57,7 @@ namespace osu.Game.Online.API protected virtual WebRequest CreateWebRequest() => new OsuWebRequest(Uri); - protected virtual string Uri => $@"{API.Endpoint}/api/v2/{Target}"; + protected virtual string Uri => $@"{API.APIEndpointUrl}/api/v2/{Target}"; protected APIAccess API; protected WebRequest WebRequest; diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs index 265298270c..3e996ac97f 100644 --- a/osu.Game/Online/API/DummyAPIAccess.cs +++ b/osu.Game/Online/API/DummyAPIAccess.cs @@ -28,7 +28,9 @@ namespace osu.Game.Online.API public string ProvidedUsername => LocalUser.Value.Username; - public string Endpoint => "http://localhost"; + public string APIEndpointUrl => "http://localhost"; + + public string WebsiteRootUrl => "http://localhost"; /// /// Provide handling logic for an arbitrary API request. diff --git a/osu.Game/Online/API/IAPIProvider.cs b/osu.Game/Online/API/IAPIProvider.cs index 3a444460f2..1951dfaf40 100644 --- a/osu.Game/Online/API/IAPIProvider.cs +++ b/osu.Game/Online/API/IAPIProvider.cs @@ -46,7 +46,12 @@ namespace osu.Game.Online.API /// /// The URL endpoint for this API. Does not include a trailing slash. /// - string Endpoint { get; } + string APIEndpointUrl { get; } + + /// + /// The root URL of of the website, excluding the trailing slash. + /// + string WebsiteRootUrl { get; } /// /// The current connection state of the API. diff --git a/osu.Game/Online/API/Requests/GetBeatmapSetRequest.cs b/osu.Game/Online/API/Requests/GetBeatmapSetRequest.cs index 8e6deeb3c6..158ae03b8d 100644 --- a/osu.Game/Online/API/Requests/GetBeatmapSetRequest.cs +++ b/osu.Game/Online/API/Requests/GetBeatmapSetRequest.cs @@ -7,16 +7,16 @@ namespace osu.Game.Online.API.Requests { public class GetBeatmapSetRequest : APIRequest { - private readonly int id; - private readonly BeatmapSetLookupType type; + public readonly int ID; + public readonly BeatmapSetLookupType Type; public GetBeatmapSetRequest(int id, BeatmapSetLookupType type = BeatmapSetLookupType.SetId) { - this.id = id; - this.type = type; + ID = id; + Type = type; } - protected override string Target => type == BeatmapSetLookupType.SetId ? $@"beatmapsets/{id}" : $@"beatmapsets/lookup?beatmap_id={id}"; + protected override string Target => Type == BeatmapSetLookupType.SetId ? $@"beatmapsets/{ID}" : $@"beatmapsets/lookup?beatmap_id={ID}"; } public enum BeatmapSetLookupType diff --git a/osu.Game/Online/API/Requests/GetUserRequest.cs b/osu.Game/Online/API/Requests/GetUserRequest.cs index 31b7e95b39..42aad6f9eb 100644 --- a/osu.Game/Online/API/Requests/GetUserRequest.cs +++ b/osu.Game/Online/API/Requests/GetUserRequest.cs @@ -9,14 +9,14 @@ namespace osu.Game.Online.API.Requests public class GetUserRequest : APIRequest { private readonly long? userId; - private readonly RulesetInfo ruleset; + public readonly RulesetInfo Ruleset; public GetUserRequest(long? userId = null, RulesetInfo ruleset = null) { this.userId = userId; - this.ruleset = ruleset; + Ruleset = ruleset; } - protected override string Target => userId.HasValue ? $@"users/{userId}/{ruleset?.ShortName}" : $@"me/{ruleset?.ShortName}"; + protected override string Target => userId.HasValue ? $@"users/{userId}/{Ruleset?.ShortName}" : $@"me/{Ruleset?.ShortName}"; } } diff --git a/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs b/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs index 6d0160fbc4..720d6bfff4 100644 --- a/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs +++ b/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs @@ -80,7 +80,7 @@ namespace osu.Game.Online.API.Requests.Responses public BeatmapSetInfo ToBeatmapSet(RulesetStore rulesets) { - return new BeatmapSetInfo + var beatmapSet = new BeatmapSetInfo { OnlineBeatmapSetID = OnlineBeatmapSetID, Metadata = this, @@ -104,8 +104,17 @@ namespace osu.Game.Online.API.Requests.Responses Genre = genre, Language = language }, - Beatmaps = beatmaps?.Select(b => b.ToBeatmap(rulesets)).ToList(), }; + + beatmapSet.Beatmaps = beatmaps?.Select(b => + { + var beatmap = b.ToBeatmap(rulesets); + beatmap.BeatmapSet = beatmapSet; + beatmap.Metadata = beatmapSet.Metadata; + return beatmap; + }).ToList(); + + return beatmapSet; } } } diff --git a/osu.Game/Online/API/Requests/Responses/APIChangelogEntry.cs b/osu.Game/Online/API/Requests/Responses/APIChangelogEntry.cs index f949ab5da5..1ff7523ba6 100644 --- a/osu.Game/Online/API/Requests/Responses/APIChangelogEntry.cs +++ b/osu.Game/Online/API/Requests/Responses/APIChangelogEntry.cs @@ -48,6 +48,7 @@ namespace osu.Game.Online.API.Requests.Responses public enum ChangelogEntryType { Add, - Fix + Fix, + Misc } } diff --git a/osu.Game/Online/Chat/ChannelManager.cs b/osu.Game/Online/Chat/ChannelManager.cs index 16f46581c5..62ae507419 100644 --- a/osu.Game/Online/Chat/ChannelManager.cs +++ b/osu.Game/Online/Chat/ChannelManager.cs @@ -57,7 +57,7 @@ namespace osu.Game.Online.Chat { CurrentChannel.ValueChanged += currentChannelChanged; - HighPollRate.BindValueChanged(enabled => TimeBetweenPolls = enabled.NewValue ? 1000 : 6000, true); + HighPollRate.BindValueChanged(enabled => TimeBetweenPolls.Value = enabled.NewValue ? 1000 : 6000, true); } /// diff --git a/osu.Game/Online/Chat/NowPlayingCommand.cs b/osu.Game/Online/Chat/NowPlayingCommand.cs index c0b54812b6..926709694b 100644 --- a/osu.Game/Online/Chat/NowPlayingCommand.cs +++ b/osu.Game/Online/Chat/NowPlayingCommand.cs @@ -46,7 +46,7 @@ namespace osu.Game.Online.Chat break; } - var beatmapString = beatmap.OnlineBeatmapID.HasValue ? $"[https://osu.ppy.sh/b/{beatmap.OnlineBeatmapID} {beatmap}]" : beatmap.ToString(); + var beatmapString = beatmap.OnlineBeatmapID.HasValue ? $"[{api.WebsiteRootUrl}/b/{beatmap.OnlineBeatmapID} {beatmap}]" : beatmap.ToString(); channelManager.PostMessage($"is {verb} {beatmapString}", true); Expire(); diff --git a/osu.Game/Online/DevelopmentEndpointConfiguration.cs b/osu.Game/Online/DevelopmentEndpointConfiguration.cs new file mode 100644 index 0000000000..69531dbe1b --- /dev/null +++ b/osu.Game/Online/DevelopmentEndpointConfiguration.cs @@ -0,0 +1,17 @@ +// 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.Online +{ + public class DevelopmentEndpointConfiguration : EndpointConfiguration + { + public DevelopmentEndpointConfiguration() + { + WebsiteRootUrl = APIEndpointUrl = @"https://dev.ppy.sh"; + APIClientSecret = @"3LP2mhUrV89xxzD1YKNndXHEhWWCRLPNKioZ9ymT"; + APIClientID = "5"; + SpectatorEndpointUrl = $"{APIEndpointUrl}/spectator"; + MultiplayerEndpointUrl = $"{APIEndpointUrl}/multiplayer"; + } + } +} diff --git a/osu.Game/Online/EndpointConfiguration.cs b/osu.Game/Online/EndpointConfiguration.cs new file mode 100644 index 0000000000..e347d3c653 --- /dev/null +++ b/osu.Game/Online/EndpointConfiguration.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. + +namespace osu.Game.Online +{ + /// + /// Holds configuration for API endpoints. + /// + public class EndpointConfiguration + { + /// + /// The base URL for the website. + /// + public string WebsiteRootUrl { get; set; } + + /// + /// The endpoint for the main (osu-web) API. + /// + public string APIEndpointUrl { get; set; } + + /// + /// The OAuth client secret. + /// + public string APIClientSecret { get; set; } + + /// + /// The OAuth client ID. + /// + public string APIClientID { get; set; } + + /// + /// The endpoint for the SignalR spectator server. + /// + public string SpectatorEndpointUrl { get; set; } + + /// + /// The endpoint for the SignalR multiplayer server. + /// + public string MultiplayerEndpointUrl { get; set; } + } +} diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index dcd0cb435a..5608002513 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -24,8 +24,8 @@ using osu.Game.Scoring; using osu.Game.Users.Drawables; using osuTK; using osuTK.Graphics; -using Humanizer; using osu.Game.Online.API; +using osu.Game.Utils; namespace osu.Game.Online.Leaderboards { @@ -78,7 +78,7 @@ namespace osu.Game.Online.Leaderboards statisticsLabels = GetStatistics(score).Select(s => new ScoreComponentLabel(s)).ToList(); - DrawableAvatar innerAvatar; + ClickableAvatar innerAvatar; Children = new Drawable[] { @@ -115,7 +115,7 @@ namespace osu.Game.Online.Leaderboards Children = new[] { avatar = new DelayedLoadWrapper( - innerAvatar = new DrawableAvatar(user) + innerAvatar = new ClickableAvatar(user) { RelativeSizeAxes = Axes.Both, CornerRadius = corner_radius, @@ -358,7 +358,7 @@ namespace osu.Game.Online.Leaderboards Anchor = Anchor.Centre, Origin = Anchor.Centre, Font = OsuFont.GetFont(size: 20, italics: true), - Text = rank == null ? "-" : rank.Value.ToMetric(decimals: rank < 100000 ? 1 : 0), + Text = rank == null ? "-" : rank.Value.FormatRank() }; } diff --git a/osu.Game/Online/RealtimeMultiplayer/IMultiplayerClient.cs b/osu.Game/Online/Multiplayer/IMultiplayerClient.cs similarity index 98% rename from osu.Game/Online/RealtimeMultiplayer/IMultiplayerClient.cs rename to osu.Game/Online/Multiplayer/IMultiplayerClient.cs index 9af0047137..b97fcc9ae7 100644 --- a/osu.Game/Online/RealtimeMultiplayer/IMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/IMultiplayerClient.cs @@ -3,7 +3,7 @@ using System.Threading.Tasks; -namespace osu.Game.Online.RealtimeMultiplayer +namespace osu.Game.Online.Multiplayer { /// /// An interface defining a multiplayer client instance. diff --git a/osu.Game/Online/RealtimeMultiplayer/IMultiplayerLoungeServer.cs b/osu.Game/Online/Multiplayer/IMultiplayerLoungeServer.cs similarity index 93% rename from osu.Game/Online/RealtimeMultiplayer/IMultiplayerLoungeServer.cs rename to osu.Game/Online/Multiplayer/IMultiplayerLoungeServer.cs index eecb61bcb0..4640640c5f 100644 --- a/osu.Game/Online/RealtimeMultiplayer/IMultiplayerLoungeServer.cs +++ b/osu.Game/Online/Multiplayer/IMultiplayerLoungeServer.cs @@ -3,7 +3,7 @@ using System.Threading.Tasks; -namespace osu.Game.Online.RealtimeMultiplayer +namespace osu.Game.Online.Multiplayer { /// /// Interface for an out-of-room multiplayer server. diff --git a/osu.Game/Online/RealtimeMultiplayer/IMultiplayerRoomServer.cs b/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs similarity index 98% rename from osu.Game/Online/RealtimeMultiplayer/IMultiplayerRoomServer.cs rename to osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs index 12dfe481c4..481e3fb1de 100644 --- a/osu.Game/Online/RealtimeMultiplayer/IMultiplayerRoomServer.cs +++ b/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs @@ -3,7 +3,7 @@ using System.Threading.Tasks; -namespace osu.Game.Online.RealtimeMultiplayer +namespace osu.Game.Online.Multiplayer { /// /// Interface for an in-room multiplayer server. diff --git a/osu.Game/Online/RealtimeMultiplayer/IMultiplayerServer.cs b/osu.Game/Online/Multiplayer/IMultiplayerServer.cs similarity index 88% rename from osu.Game/Online/RealtimeMultiplayer/IMultiplayerServer.cs rename to osu.Game/Online/Multiplayer/IMultiplayerServer.cs index 1d093af743..d3a070af6d 100644 --- a/osu.Game/Online/RealtimeMultiplayer/IMultiplayerServer.cs +++ b/osu.Game/Online/Multiplayer/IMultiplayerServer.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. -namespace osu.Game.Online.RealtimeMultiplayer +namespace osu.Game.Online.Multiplayer { /// /// An interface defining the multiplayer server instance. diff --git a/osu.Game/Online/RealtimeMultiplayer/InvalidStateChangeException.cs b/osu.Game/Online/Multiplayer/InvalidStateChangeException.cs similarity index 93% rename from osu.Game/Online/RealtimeMultiplayer/InvalidStateChangeException.cs rename to osu.Game/Online/Multiplayer/InvalidStateChangeException.cs index d9a276fc19..69b6d4bc13 100644 --- a/osu.Game/Online/RealtimeMultiplayer/InvalidStateChangeException.cs +++ b/osu.Game/Online/Multiplayer/InvalidStateChangeException.cs @@ -5,7 +5,7 @@ using System; using System.Runtime.Serialization; using Microsoft.AspNetCore.SignalR; -namespace osu.Game.Online.RealtimeMultiplayer +namespace osu.Game.Online.Multiplayer { [Serializable] public class InvalidStateChangeException : HubException diff --git a/osu.Game/Online/RealtimeMultiplayer/InvalidStateException.cs b/osu.Game/Online/Multiplayer/InvalidStateException.cs similarity index 92% rename from osu.Game/Online/RealtimeMultiplayer/InvalidStateException.cs rename to osu.Game/Online/Multiplayer/InvalidStateException.cs index 7791bfc69f..77a3533dd3 100644 --- a/osu.Game/Online/RealtimeMultiplayer/InvalidStateException.cs +++ b/osu.Game/Online/Multiplayer/InvalidStateException.cs @@ -5,7 +5,7 @@ using System; using System.Runtime.Serialization; using Microsoft.AspNetCore.SignalR; -namespace osu.Game.Online.RealtimeMultiplayer +namespace osu.Game.Online.Multiplayer { [Serializable] public class InvalidStateException : HubException diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs new file mode 100644 index 0000000000..24ea6abc4a --- /dev/null +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -0,0 +1,183 @@ +// 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 System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.SignalR.Client; +using Microsoft.Extensions.DependencyInjection; +using Newtonsoft.Json; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Logging; +using osu.Game.Online.API; + +namespace osu.Game.Online.Multiplayer +{ + public class MultiplayerClient : StatefulMultiplayerClient + { + public override IBindable IsConnected => isConnected; + + private readonly Bindable isConnected = new Bindable(); + private readonly IBindable apiState = new Bindable(); + + [Resolved] + private IAPIProvider api { get; set; } = null!; + + private HubConnection? connection; + + private readonly string endpoint; + + public MultiplayerClient(EndpointConfiguration endpoints) + { + endpoint = endpoints.MultiplayerEndpointUrl; + } + + [BackgroundDependencyLoader] + private void load() + { + apiState.BindTo(api.State); + apiState.BindValueChanged(apiStateChanged, true); + } + + private void apiStateChanged(ValueChangedEvent state) + { + switch (state.NewValue) + { + case APIState.Failing: + case APIState.Offline: + connection?.StopAsync(); + connection = null; + break; + + case APIState.Online: + Task.Run(Connect); + break; + } + } + + protected virtual async Task Connect() + { + if (connection != null) + return; + + connection = new HubConnectionBuilder() + .WithUrl(endpoint, options => + { + options.Headers.Add("Authorization", $"Bearer {api.AccessToken}"); + }) + .AddNewtonsoftJsonProtocol(options => { options.PayloadSerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; }) + .Build(); + + // this is kind of SILLY + // https://github.com/dotnet/aspnetcore/issues/15198 + connection.On(nameof(IMultiplayerClient.RoomStateChanged), ((IMultiplayerClient)this).RoomStateChanged); + connection.On(nameof(IMultiplayerClient.UserJoined), ((IMultiplayerClient)this).UserJoined); + connection.On(nameof(IMultiplayerClient.UserLeft), ((IMultiplayerClient)this).UserLeft); + connection.On(nameof(IMultiplayerClient.HostChanged), ((IMultiplayerClient)this).HostChanged); + connection.On(nameof(IMultiplayerClient.SettingsChanged), ((IMultiplayerClient)this).SettingsChanged); + connection.On(nameof(IMultiplayerClient.UserStateChanged), ((IMultiplayerClient)this).UserStateChanged); + connection.On(nameof(IMultiplayerClient.LoadRequested), ((IMultiplayerClient)this).LoadRequested); + connection.On(nameof(IMultiplayerClient.MatchStarted), ((IMultiplayerClient)this).MatchStarted); + connection.On(nameof(IMultiplayerClient.ResultsReady), ((IMultiplayerClient)this).ResultsReady); + + connection.Closed += async ex => + { + isConnected.Value = false; + + if (ex != null) + { + Logger.Log($"Multiplayer client lost connection: {ex}", LoggingTarget.Network); + await tryUntilConnected(); + } + }; + + await tryUntilConnected(); + + async Task tryUntilConnected() + { + Logger.Log("Multiplayer client connecting...", LoggingTarget.Network); + + while (api.State.Value == APIState.Online) + { + try + { + Debug.Assert(connection != null); + + // reconnect on any failure + await connection.StartAsync(); + Logger.Log("Multiplayer client connected!", LoggingTarget.Network); + + // Success. + isConnected.Value = true; + break; + } + catch (Exception e) + { + Logger.Log($"Multiplayer client connection error: {e}", LoggingTarget.Network); + await Task.Delay(5000); + } + } + } + } + + protected override Task JoinRoom(long roomId) + { + if (!isConnected.Value) + return Task.FromCanceled(new CancellationToken(true)); + + return connection.InvokeAsync(nameof(IMultiplayerServer.JoinRoom), roomId); + } + + public override async Task LeaveRoom() + { + if (!isConnected.Value) + { + // even if not connected, make sure the local room state can be cleaned up. + await base.LeaveRoom(); + return; + } + + if (Room == null) + return; + + await base.LeaveRoom(); + await connection.InvokeAsync(nameof(IMultiplayerServer.LeaveRoom)); + } + + public override Task TransferHost(int userId) + { + if (!isConnected.Value) + return Task.CompletedTask; + + return connection.InvokeAsync(nameof(IMultiplayerServer.TransferHost), userId); + } + + public override Task ChangeSettings(MultiplayerRoomSettings settings) + { + if (!isConnected.Value) + return Task.CompletedTask; + + return connection.InvokeAsync(nameof(IMultiplayerServer.ChangeSettings), settings); + } + + public override Task ChangeState(MultiplayerUserState newState) + { + if (!isConnected.Value) + return Task.CompletedTask; + + return connection.InvokeAsync(nameof(IMultiplayerServer.ChangeState), newState); + } + + public override Task StartMatch() + { + if (!isConnected.Value) + return Task.CompletedTask; + + return connection.InvokeAsync(nameof(IMultiplayerServer.StartMatch)); + } + } +} diff --git a/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoom.cs b/osu.Game/Online/Multiplayer/MultiplayerRoom.cs similarity index 98% rename from osu.Game/Online/RealtimeMultiplayer/MultiplayerRoom.cs rename to osu.Game/Online/Multiplayer/MultiplayerRoom.cs index e009a34707..2134e50d72 100644 --- a/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoom.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerRoom.cs @@ -9,7 +9,7 @@ using System.Threading; using Newtonsoft.Json; using osu.Framework.Allocation; -namespace osu.Game.Online.RealtimeMultiplayer +namespace osu.Game.Online.Multiplayer { /// /// A multiplayer room. diff --git a/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomSettings.cs b/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs similarity index 96% rename from osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomSettings.cs rename to osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs index 60e0d1292e..857b38ea60 100644 --- a/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomSettings.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs @@ -9,7 +9,7 @@ using System.Linq; using JetBrains.Annotations; using osu.Game.Online.API; -namespace osu.Game.Online.RealtimeMultiplayer +namespace osu.Game.Online.Multiplayer { [Serializable] public class MultiplayerRoomSettings : IEquatable diff --git a/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomState.cs b/osu.Game/Online/Multiplayer/MultiplayerRoomState.cs similarity index 86% rename from osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomState.cs rename to osu.Game/Online/Multiplayer/MultiplayerRoomState.cs index 69c04b09a8..48f25d7ca2 100644 --- a/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomState.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerRoomState.cs @@ -3,10 +3,10 @@ #nullable enable -namespace osu.Game.Online.RealtimeMultiplayer +namespace osu.Game.Online.Multiplayer { /// - /// The current overall state of a realtime multiplayer room. + /// The current overall state of a multiplayer room. /// public enum MultiplayerRoomState { diff --git a/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomUser.cs b/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs similarity index 96% rename from osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomUser.cs rename to osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs index caf1a70197..99624dc3e7 100644 --- a/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomUser.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs @@ -7,7 +7,7 @@ using System; using Newtonsoft.Json; using osu.Game.Users; -namespace osu.Game.Online.RealtimeMultiplayer +namespace osu.Game.Online.Multiplayer { [Serializable] public class MultiplayerRoomUser : IEquatable diff --git a/osu.Game/Online/RealtimeMultiplayer/MultiplayerUserState.cs b/osu.Game/Online/Multiplayer/MultiplayerUserState.cs similarity index 98% rename from osu.Game/Online/RealtimeMultiplayer/MultiplayerUserState.cs rename to osu.Game/Online/Multiplayer/MultiplayerUserState.cs index ed9acd146e..e54c71cd85 100644 --- a/osu.Game/Online/RealtimeMultiplayer/MultiplayerUserState.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerUserState.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. -namespace osu.Game.Online.RealtimeMultiplayer +namespace osu.Game.Online.Multiplayer { public enum MultiplayerUserState { diff --git a/osu.Game/Online/RealtimeMultiplayer/NotHostException.cs b/osu.Game/Online/Multiplayer/NotHostException.cs similarity index 92% rename from osu.Game/Online/RealtimeMultiplayer/NotHostException.cs rename to osu.Game/Online/Multiplayer/NotHostException.cs index 56095043f0..051cde45a0 100644 --- a/osu.Game/Online/RealtimeMultiplayer/NotHostException.cs +++ b/osu.Game/Online/Multiplayer/NotHostException.cs @@ -5,7 +5,7 @@ using System; using System.Runtime.Serialization; using Microsoft.AspNetCore.SignalR; -namespace osu.Game.Online.RealtimeMultiplayer +namespace osu.Game.Online.Multiplayer { [Serializable] public class NotHostException : HubException diff --git a/osu.Game/Online/RealtimeMultiplayer/NotJoinedRoomException.cs b/osu.Game/Online/Multiplayer/NotJoinedRoomException.cs similarity index 92% rename from osu.Game/Online/RealtimeMultiplayer/NotJoinedRoomException.cs rename to osu.Game/Online/Multiplayer/NotJoinedRoomException.cs index 7a6e089d0b..0e9902f002 100644 --- a/osu.Game/Online/RealtimeMultiplayer/NotJoinedRoomException.cs +++ b/osu.Game/Online/Multiplayer/NotJoinedRoomException.cs @@ -5,7 +5,7 @@ using System; using System.Runtime.Serialization; using Microsoft.AspNetCore.SignalR; -namespace osu.Game.Online.RealtimeMultiplayer +namespace osu.Game.Online.Multiplayer { [Serializable] public class NotJoinedRoomException : HubException diff --git a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs new file mode 100644 index 0000000000..fcb0977f53 --- /dev/null +++ b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs @@ -0,0 +1,458 @@ +// 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 System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Graphics; +using osu.Framework.Logging; +using osu.Game.Beatmaps; +using osu.Game.Database; +using osu.Game.Extensions; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Rooms; +using osu.Game.Online.Rooms.RoomStatuses; +using osu.Game.Rulesets; +using osu.Game.Users; +using osu.Game.Utils; + +namespace osu.Game.Online.Multiplayer +{ + public abstract class StatefulMultiplayerClient : Component, IMultiplayerClient, IMultiplayerRoomServer + { + /// + /// Invoked when any change occurs to the multiplayer room. + /// + public event Action? RoomUpdated; + + /// + /// Invoked when the multiplayer server requests the current beatmap to be loaded into play. + /// + public event Action? LoadRequested; + + /// + /// Invoked when the multiplayer server requests gameplay to be started. + /// + public event Action? MatchStarted; + + /// + /// Invoked when the multiplayer server has finished collating results. + /// + public event Action? ResultsReady; + + /// + /// Whether the is currently connected. + /// + public abstract IBindable IsConnected { get; } + + /// + /// The joined . + /// + public MultiplayerRoom? Room { get; private set; } + + /// + /// The users currently in gameplay. + /// + public readonly BindableList PlayingUsers = new BindableList(); + + [Resolved] + private UserLookupCache userLookupCache { get; set; } = null!; + + [Resolved] + private IAPIProvider api { get; set; } = null!; + + [Resolved] + private RulesetStore rulesets { get; set; } = null!; + + private Room? apiRoom; + + // Todo: This is temporary, until the multiplayer server returns the item id on match start or otherwise. + private int playlistItemId; + + protected StatefulMultiplayerClient() + { + IsConnected.BindValueChanged(connected => + { + // clean up local room state on server disconnect. + if (!connected.NewValue) + { + Logger.Log("Connection to multiplayer server was lost.", LoggingTarget.Runtime, LogLevel.Important); + LeaveRoom().CatchUnobservedExceptions(); + } + }); + } + + /// + /// Joins the for a given API . + /// + /// The API . + public async Task JoinRoom(Room room) + { + if (Room != null) + throw new InvalidOperationException("Cannot join a multiplayer room while already in one."); + + Debug.Assert(room.RoomID.Value != null); + + apiRoom = room; + playlistItemId = room.Playlist.SingleOrDefault()?.ID ?? 0; + + Room = await JoinRoom(room.RoomID.Value.Value); + + Debug.Assert(Room != null); + + var users = getRoomUsers(); + + await Task.WhenAll(users.Select(PopulateUser)); + + updateLocalRoomSettings(Room.Settings); + } + + /// + /// Joins the with a given ID. + /// + /// The room ID. + /// The joined . + protected abstract Task JoinRoom(long roomId); + + public virtual Task LeaveRoom() + { + Scheduler.Add(() => + { + if (Room == null) + return; + + apiRoom = null; + Room = null; + + RoomUpdated?.Invoke(); + }, false); + + return Task.CompletedTask; + } + + /// + /// Change the current settings. + /// + /// + /// A room must be joined for this to have any effect. + /// + /// The new room name, if any. + /// The new room playlist item, if any. + public Task ChangeSettings(Optional name = default, Optional item = default) + { + if (Room == null) + throw new InvalidOperationException("Must be joined to a match to change settings."); + + // A dummy playlist item filled with the current room settings (except mods). + var existingPlaylistItem = new PlaylistItem + { + Beatmap = + { + Value = new BeatmapInfo + { + OnlineBeatmapID = Room.Settings.BeatmapID, + MD5Hash = Room.Settings.BeatmapChecksum + } + }, + RulesetID = Room.Settings.RulesetID + }; + + return ChangeSettings(new MultiplayerRoomSettings + { + Name = name.GetOr(Room.Settings.Name), + BeatmapID = item.GetOr(existingPlaylistItem).BeatmapID, + BeatmapChecksum = item.GetOr(existingPlaylistItem).Beatmap.Value.MD5Hash, + RulesetID = item.GetOr(existingPlaylistItem).RulesetID, + Mods = item.HasValue ? item.Value.AsNonNull().RequiredMods.Select(m => new APIMod(m)).ToList() : Room.Settings.Mods + }); + } + + public abstract Task TransferHost(int userId); + + public abstract Task ChangeSettings(MultiplayerRoomSettings settings); + + public abstract Task ChangeState(MultiplayerUserState newState); + + public abstract Task StartMatch(); + + Task IMultiplayerClient.RoomStateChanged(MultiplayerRoomState state) + { + if (Room == null) + return Task.CompletedTask; + + Scheduler.Add(() => + { + if (Room == null) + return; + + Debug.Assert(apiRoom != null); + + Room.State = state; + + switch (state) + { + case MultiplayerRoomState.Open: + apiRoom.Status.Value = new RoomStatusOpen(); + break; + + case MultiplayerRoomState.Playing: + apiRoom.Status.Value = new RoomStatusPlaying(); + break; + + case MultiplayerRoomState.Closed: + apiRoom.Status.Value = new RoomStatusEnded(); + break; + } + + RoomUpdated?.Invoke(); + }, false); + + return Task.CompletedTask; + } + + async Task IMultiplayerClient.UserJoined(MultiplayerRoomUser user) + { + if (Room == null) + return; + + await PopulateUser(user); + + Scheduler.Add(() => + { + if (Room == null) + return; + + // for sanity, ensure that there can be no duplicate users in the room user list. + if (Room.Users.Any(existing => existing.UserID == user.UserID)) + return; + + Room.Users.Add(user); + + RoomUpdated?.Invoke(); + }, false); + } + + Task IMultiplayerClient.UserLeft(MultiplayerRoomUser user) + { + if (Room == null) + return Task.CompletedTask; + + Scheduler.Add(() => + { + if (Room == null) + return; + + Room.Users.Remove(user); + PlayingUsers.Remove(user.UserID); + + RoomUpdated?.Invoke(); + }, false); + + return Task.CompletedTask; + } + + Task IMultiplayerClient.HostChanged(int userId) + { + if (Room == null) + return Task.CompletedTask; + + Scheduler.Add(() => + { + if (Room == null) + return; + + Debug.Assert(apiRoom != null); + + var user = Room.Users.FirstOrDefault(u => u.UserID == userId); + + Room.Host = user; + apiRoom.Host.Value = user?.User; + + RoomUpdated?.Invoke(); + }, false); + + return Task.CompletedTask; + } + + Task IMultiplayerClient.SettingsChanged(MultiplayerRoomSettings newSettings) + { + updateLocalRoomSettings(newSettings); + return Task.CompletedTask; + } + + Task IMultiplayerClient.UserStateChanged(int userId, MultiplayerUserState state) + { + if (Room == null) + return Task.CompletedTask; + + Scheduler.Add(() => + { + if (Room == null) + return; + + Room.Users.Single(u => u.UserID == userId).State = state; + + if (state != MultiplayerUserState.Playing) + PlayingUsers.Remove(userId); + + RoomUpdated?.Invoke(); + }, false); + + return Task.CompletedTask; + } + + Task IMultiplayerClient.LoadRequested() + { + if (Room == null) + return Task.CompletedTask; + + Scheduler.Add(() => + { + if (Room == null) + return; + + LoadRequested?.Invoke(); + }, false); + + return Task.CompletedTask; + } + + Task IMultiplayerClient.MatchStarted() + { + if (Room == null) + return Task.CompletedTask; + + Scheduler.Add(() => + { + if (Room == null) + return; + + PlayingUsers.AddRange(Room.Users.Where(u => u.State == MultiplayerUserState.Playing).Select(u => u.UserID)); + + MatchStarted?.Invoke(); + }, false); + + return Task.CompletedTask; + } + + Task IMultiplayerClient.ResultsReady() + { + if (Room == null) + return Task.CompletedTask; + + Scheduler.Add(() => + { + if (Room == null) + return; + + ResultsReady?.Invoke(); + }, false); + + return Task.CompletedTask; + } + + /// + /// Populates the for a given . + /// + /// The to populate. + protected async Task PopulateUser(MultiplayerRoomUser multiplayerUser) => multiplayerUser.User ??= await userLookupCache.GetUserAsync(multiplayerUser.UserID); + + /// + /// Retrieve a copy of users currently in the joined in a thread-safe manner. + /// This should be used whenever accessing users from outside of an Update thread context (ie. when not calling ). + /// + /// A copy of users in the current room, or null if unavailable. + private List? getRoomUsers() + { + List? users = null; + + ManualResetEventSlim resetEvent = new ManualResetEventSlim(); + + // at some point we probably want to replace all these schedule calls with Room.LockForUpdate. + // for now, as this would require quite some consideration due to the number of accesses to the room instance, + // let's just add a manual schedule for the non-scheduled usages instead. + Scheduler.Add(() => + { + users = Room?.Users.ToList(); + resetEvent.Set(); + }, false); + + resetEvent.Wait(100); + + return users; + } + + /// + /// Updates the local room settings with the given . + /// + /// + /// This updates both the joined and the respective API . + /// + /// The new to update from. + private void updateLocalRoomSettings(MultiplayerRoomSettings settings) + { + if (Room == null) + return; + + Scheduler.Add(() => + { + if (Room == null) + return; + + Debug.Assert(apiRoom != null); + + // Update a few properties of the room instantaneously. + Room.Settings = settings; + apiRoom.Name.Value = Room.Settings.Name; + + // The playlist update is delayed until an online beatmap lookup (below) succeeds. + // In-order for the client to not display an outdated beatmap, the playlist is forcefully cleared here. + apiRoom.Playlist.Clear(); + + RoomUpdated?.Invoke(); + + var req = new GetBeatmapSetRequest(settings.BeatmapID, BeatmapSetLookupType.BeatmapId); + req.Success += res => updatePlaylist(settings, res); + + api.Queue(req); + }, false); + } + + private void updatePlaylist(MultiplayerRoomSettings settings, APIBeatmapSet onlineSet) + { + if (Room == null || !Room.Settings.Equals(settings)) + return; + + Debug.Assert(apiRoom != null); + + var beatmapSet = onlineSet.ToBeatmapSet(rulesets); + var beatmap = beatmapSet.Beatmaps.Single(b => b.OnlineBeatmapID == settings.BeatmapID); + beatmap.MD5Hash = settings.BeatmapChecksum; + + var ruleset = rulesets.GetRuleset(settings.RulesetID).CreateInstance(); + var mods = settings.Mods.Select(m => m.ToMod(ruleset)); + + PlaylistItem playlistItem = new PlaylistItem + { + ID = playlistItemId, + Beatmap = { Value = beatmap }, + Ruleset = { Value = ruleset.RulesetInfo }, + }; + + playlistItem.RequiredMods.AddRange(mods); + + apiRoom.Playlist.Clear(); // Clearing should be unnecessary, but here for sanity. + apiRoom.Playlist.Add(playlistItem); + } + } +} diff --git a/osu.Game/Online/PollingComponent.cs b/osu.Game/Online/PollingComponent.cs index 228f147835..3d19f2ab09 100644 --- a/osu.Game/Online/PollingComponent.cs +++ b/osu.Game/Online/PollingComponent.cs @@ -3,6 +3,7 @@ using System; using System.Threading.Tasks; +using osu.Framework.Bindables; using osu.Framework.Graphics.Containers; using osu.Framework.Threading; @@ -19,22 +20,11 @@ namespace osu.Game.Online private bool pollingActive; - private double timeBetweenPolls; - /// /// The time in milliseconds to wait between polls. /// Setting to zero stops all polling. /// - public double TimeBetweenPolls - { - get => timeBetweenPolls; - set - { - timeBetweenPolls = value; - scheduledPoll?.Cancel(); - pollIfNecessary(); - } - } + public readonly Bindable TimeBetweenPolls = new Bindable(); /// /// @@ -42,7 +32,13 @@ namespace osu.Game.Online /// The initial time in milliseconds to wait between polls. Setting to zero stops all polling. protected PollingComponent(double timeBetweenPolls = 0) { - TimeBetweenPolls = timeBetweenPolls; + TimeBetweenPolls.BindValueChanged(_ => + { + scheduledPoll?.Cancel(); + pollIfNecessary(); + }); + + TimeBetweenPolls.Value = timeBetweenPolls; } protected override void LoadComplete() @@ -60,7 +56,7 @@ namespace osu.Game.Online if (pollingActive) return false; // don't try polling if the time between polls hasn't been set. - if (timeBetweenPolls == 0) return false; + if (TimeBetweenPolls.Value == 0) return false; if (!lastTimePolled.HasValue) { @@ -68,7 +64,7 @@ namespace osu.Game.Online return true; } - if (Time.Current - lastTimePolled.Value > timeBetweenPolls) + if (Time.Current - lastTimePolled.Value > TimeBetweenPolls.Value) { doPoll(); return true; @@ -99,7 +95,7 @@ namespace osu.Game.Online /// public void PollImmediately() { - lastTimePolled = Time.Current - timeBetweenPolls; + lastTimePolled = Time.Current - TimeBetweenPolls.Value; scheduleNextPoll(); } @@ -121,7 +117,7 @@ namespace osu.Game.Online double lastPollDuration = lastTimePolled.HasValue ? Time.Current - lastTimePolled.Value : 0; - scheduledPoll = Scheduler.AddDelayed(doPoll, Math.Max(0, timeBetweenPolls - lastPollDuration)); + scheduledPoll = Scheduler.AddDelayed(doPoll, Math.Max(0, TimeBetweenPolls.Value - lastPollDuration)); } } } diff --git a/osu.Game/Online/ProductionEndpointConfiguration.cs b/osu.Game/Online/ProductionEndpointConfiguration.cs new file mode 100644 index 0000000000..c6ddc03564 --- /dev/null +++ b/osu.Game/Online/ProductionEndpointConfiguration.cs @@ -0,0 +1,17 @@ +// 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.Online +{ + public class ProductionEndpointConfiguration : EndpointConfiguration + { + public ProductionEndpointConfiguration() + { + WebsiteRootUrl = APIEndpointUrl = @"https://osu.ppy.sh"; + APIClientSecret = @"FGc9GAtyHzeQDshWP5Ah7dega8hJACAJpQtw6OXk"; + APIClientID = "5"; + SpectatorEndpointUrl = "https://spectator.ppy.sh/spectator"; + MultiplayerEndpointUrl = "https://spectator.ppy.sh/multiplayer"; + } + } +} diff --git a/osu.Game/Online/Multiplayer/APICreatedRoom.cs b/osu.Game/Online/Rooms/APICreatedRoom.cs similarity index 88% rename from osu.Game/Online/Multiplayer/APICreatedRoom.cs rename to osu.Game/Online/Rooms/APICreatedRoom.cs index 2a3bb39647..d1062b2306 100644 --- a/osu.Game/Online/Multiplayer/APICreatedRoom.cs +++ b/osu.Game/Online/Rooms/APICreatedRoom.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; -namespace osu.Game.Online.Multiplayer +namespace osu.Game.Online.Rooms { public class APICreatedRoom : Room { diff --git a/osu.Game/Online/Multiplayer/APILeaderboard.cs b/osu.Game/Online/Rooms/APILeaderboard.cs similarity index 92% rename from osu.Game/Online/Multiplayer/APILeaderboard.cs rename to osu.Game/Online/Rooms/APILeaderboard.cs index 65863d6e0e..c487123906 100644 --- a/osu.Game/Online/Multiplayer/APILeaderboard.cs +++ b/osu.Game/Online/Rooms/APILeaderboard.cs @@ -5,7 +5,7 @@ using System.Collections.Generic; using Newtonsoft.Json; using osu.Game.Online.API.Requests.Responses; -namespace osu.Game.Online.Multiplayer +namespace osu.Game.Online.Rooms { public class APILeaderboard { diff --git a/osu.Game/Online/Multiplayer/APIPlaylistBeatmap.cs b/osu.Game/Online/Rooms/APIPlaylistBeatmap.cs similarity index 94% rename from osu.Game/Online/Multiplayer/APIPlaylistBeatmap.cs rename to osu.Game/Online/Rooms/APIPlaylistBeatmap.cs index 98972ef36d..973dccd528 100644 --- a/osu.Game/Online/Multiplayer/APIPlaylistBeatmap.cs +++ b/osu.Game/Online/Rooms/APIPlaylistBeatmap.cs @@ -6,7 +6,7 @@ using osu.Game.Beatmaps; using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets; -namespace osu.Game.Online.Multiplayer +namespace osu.Game.Online.Rooms { public class APIPlaylistBeatmap : APIBeatmap { diff --git a/osu.Game/Online/Multiplayer/APIScoreToken.cs b/osu.Game/Online/Rooms/APIScoreToken.cs similarity index 88% rename from osu.Game/Online/Multiplayer/APIScoreToken.cs rename to osu.Game/Online/Rooms/APIScoreToken.cs index 1f0063d94e..f652c1720d 100644 --- a/osu.Game/Online/Multiplayer/APIScoreToken.cs +++ b/osu.Game/Online/Rooms/APIScoreToken.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; -namespace osu.Game.Online.Multiplayer +namespace osu.Game.Online.Rooms { public class APIScoreToken { diff --git a/osu.Game/Online/Multiplayer/CreateRoomRequest.cs b/osu.Game/Online/Rooms/CreateRoomRequest.cs similarity index 81% rename from osu.Game/Online/Multiplayer/CreateRoomRequest.cs rename to osu.Game/Online/Rooms/CreateRoomRequest.cs index dcb4ed51ea..f058eb9ba8 100644 --- a/osu.Game/Online/Multiplayer/CreateRoomRequest.cs +++ b/osu.Game/Online/Rooms/CreateRoomRequest.cs @@ -6,15 +6,15 @@ using Newtonsoft.Json; using osu.Framework.IO.Network; using osu.Game.Online.API; -namespace osu.Game.Online.Multiplayer +namespace osu.Game.Online.Rooms { public class CreateRoomRequest : APIRequest { - private readonly Room room; + public readonly Room Room; public CreateRoomRequest(Room room) { - this.room = room; + Room = room; } protected override WebRequest CreateWebRequest() @@ -24,7 +24,7 @@ namespace osu.Game.Online.Multiplayer req.ContentType = "application/json"; req.Method = HttpMethod.Post; - req.AddRaw(JsonConvert.SerializeObject(room)); + req.AddRaw(JsonConvert.SerializeObject(Room)); return req; } diff --git a/osu.Game/Online/Multiplayer/CreateRoomScoreRequest.cs b/osu.Game/Online/Rooms/CreateRoomScoreRequest.cs similarity index 96% rename from osu.Game/Online/Multiplayer/CreateRoomScoreRequest.cs rename to osu.Game/Online/Rooms/CreateRoomScoreRequest.cs index 2d99b12519..afd0dadc7e 100644 --- a/osu.Game/Online/Multiplayer/CreateRoomScoreRequest.cs +++ b/osu.Game/Online/Rooms/CreateRoomScoreRequest.cs @@ -5,7 +5,7 @@ using System.Net.Http; using osu.Framework.IO.Network; using osu.Game.Online.API; -namespace osu.Game.Online.Multiplayer +namespace osu.Game.Online.Rooms { public class CreateRoomScoreRequest : APIRequest { diff --git a/osu.Game/Online/Multiplayer/GameType.cs b/osu.Game/Online/Rooms/GameType.cs similarity index 93% rename from osu.Game/Online/Multiplayer/GameType.cs rename to osu.Game/Online/Rooms/GameType.cs index 10381d93bb..caa352d812 100644 --- a/osu.Game/Online/Multiplayer/GameType.cs +++ b/osu.Game/Online/Rooms/GameType.cs @@ -4,7 +4,7 @@ using osu.Framework.Graphics; using osu.Game.Graphics; -namespace osu.Game.Online.Multiplayer +namespace osu.Game.Online.Rooms { public abstract class GameType { diff --git a/osu.Game/Online/Multiplayer/GameTypes/GameTypeTimeshift.cs b/osu.Game/Online/Rooms/GameTypes/GameTypePlaylists.cs similarity index 80% rename from osu.Game/Online/Multiplayer/GameTypes/GameTypeTimeshift.cs rename to osu.Game/Online/Rooms/GameTypes/GameTypePlaylists.cs index 1a3d2837ce..3425c6c5cd 100644 --- a/osu.Game/Online/Multiplayer/GameTypes/GameTypeTimeshift.cs +++ b/osu.Game/Online/Rooms/GameTypes/GameTypePlaylists.cs @@ -6,11 +6,11 @@ using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; using osuTK; -namespace osu.Game.Online.Multiplayer.GameTypes +namespace osu.Game.Online.Rooms.GameTypes { - public class GameTypeTimeshift : GameType + public class GameTypePlaylists : GameType { - public override string Name => "Timeshift"; + public override string Name => "Playlists"; public override Drawable GetIcon(OsuColour colours, float size) => new SpriteIcon { diff --git a/osu.Game/Online/Multiplayer/GameTypes/GameTypeTag.cs b/osu.Game/Online/Rooms/GameTypes/GameTypeTag.cs similarity index 94% rename from osu.Game/Online/Multiplayer/GameTypes/GameTypeTag.cs rename to osu.Game/Online/Rooms/GameTypes/GameTypeTag.cs index 5ba5f1a415..e468612738 100644 --- a/osu.Game/Online/Multiplayer/GameTypes/GameTypeTag.cs +++ b/osu.Game/Online/Rooms/GameTypes/GameTypeTag.cs @@ -6,7 +6,7 @@ using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; using osuTK; -namespace osu.Game.Online.Multiplayer.GameTypes +namespace osu.Game.Online.Rooms.GameTypes { public class GameTypeTag : GameType { diff --git a/osu.Game/Online/Multiplayer/GameTypes/GameTypeTagTeam.cs b/osu.Game/Online/Rooms/GameTypes/GameTypeTagTeam.cs similarity index 96% rename from osu.Game/Online/Multiplayer/GameTypes/GameTypeTagTeam.cs rename to osu.Game/Online/Rooms/GameTypes/GameTypeTagTeam.cs index ef0a00a9f0..b82f203fac 100644 --- a/osu.Game/Online/Multiplayer/GameTypes/GameTypeTagTeam.cs +++ b/osu.Game/Online/Rooms/GameTypes/GameTypeTagTeam.cs @@ -7,7 +7,7 @@ using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; using osuTK; -namespace osu.Game.Online.Multiplayer.GameTypes +namespace osu.Game.Online.Rooms.GameTypes { public class GameTypeTagTeam : GameType { diff --git a/osu.Game/Online/Multiplayer/GameTypes/GameTypeTeamVersus.cs b/osu.Game/Online/Rooms/GameTypes/GameTypeTeamVersus.cs similarity index 95% rename from osu.Game/Online/Multiplayer/GameTypes/GameTypeTeamVersus.cs rename to osu.Game/Online/Rooms/GameTypes/GameTypeTeamVersus.cs index c25bce1c71..5ad4033dc9 100644 --- a/osu.Game/Online/Multiplayer/GameTypes/GameTypeTeamVersus.cs +++ b/osu.Game/Online/Rooms/GameTypes/GameTypeTeamVersus.cs @@ -6,7 +6,7 @@ using osu.Framework.Graphics.Containers; using osu.Game.Graphics; using osuTK; -namespace osu.Game.Online.Multiplayer.GameTypes +namespace osu.Game.Online.Rooms.GameTypes { public class GameTypeTeamVersus : GameType { diff --git a/osu.Game/Online/Multiplayer/GameTypes/GameTypeVersus.cs b/osu.Game/Online/Rooms/GameTypes/GameTypeVersus.cs similarity index 92% rename from osu.Game/Online/Multiplayer/GameTypes/GameTypeVersus.cs rename to osu.Game/Online/Rooms/GameTypes/GameTypeVersus.cs index 4640c7b361..3783cc67b0 100644 --- a/osu.Game/Online/Multiplayer/GameTypes/GameTypeVersus.cs +++ b/osu.Game/Online/Rooms/GameTypes/GameTypeVersus.cs @@ -4,7 +4,7 @@ using osu.Framework.Graphics; using osu.Game.Graphics; -namespace osu.Game.Online.Multiplayer.GameTypes +namespace osu.Game.Online.Rooms.GameTypes { public class GameTypeVersus : GameType { diff --git a/osu.Game/Online/Multiplayer/GameTypes/VersusRow.cs b/osu.Game/Online/Rooms/GameTypes/VersusRow.cs similarity index 97% rename from osu.Game/Online/Multiplayer/GameTypes/VersusRow.cs rename to osu.Game/Online/Rooms/GameTypes/VersusRow.cs index b6e8e4458f..0bd09a23ac 100644 --- a/osu.Game/Online/Multiplayer/GameTypes/VersusRow.cs +++ b/osu.Game/Online/Rooms/GameTypes/VersusRow.cs @@ -7,7 +7,7 @@ using osu.Framework.Graphics.Shapes; using osuTK; using osuTK.Graphics; -namespace osu.Game.Online.Multiplayer.GameTypes +namespace osu.Game.Online.Rooms.GameTypes { public class VersusRow : FillFlowContainer { diff --git a/osu.Game/Online/Multiplayer/GetRoomLeaderboardRequest.cs b/osu.Game/Online/Rooms/GetRoomLeaderboardRequest.cs similarity index 92% rename from osu.Game/Online/Multiplayer/GetRoomLeaderboardRequest.cs rename to osu.Game/Online/Rooms/GetRoomLeaderboardRequest.cs index 37c21457bc..15f1221a00 100644 --- a/osu.Game/Online/Multiplayer/GetRoomLeaderboardRequest.cs +++ b/osu.Game/Online/Rooms/GetRoomLeaderboardRequest.cs @@ -3,7 +3,7 @@ using osu.Game.Online.API; -namespace osu.Game.Online.Multiplayer +namespace osu.Game.Online.Rooms { public class GetRoomLeaderboardRequest : APIRequest { diff --git a/osu.Game/Online/Multiplayer/GetRoomRequest.cs b/osu.Game/Online/Rooms/GetRoomRequest.cs similarity index 64% rename from osu.Game/Online/Multiplayer/GetRoomRequest.cs rename to osu.Game/Online/Rooms/GetRoomRequest.cs index 2907b49f1d..ce117075c7 100644 --- a/osu.Game/Online/Multiplayer/GetRoomRequest.cs +++ b/osu.Game/Online/Rooms/GetRoomRequest.cs @@ -3,17 +3,17 @@ using osu.Game.Online.API; -namespace osu.Game.Online.Multiplayer +namespace osu.Game.Online.Rooms { public class GetRoomRequest : APIRequest { - private readonly int roomId; + public readonly int RoomId; public GetRoomRequest(int roomId) { - this.roomId = roomId; + RoomId = roomId; } - protected override string Target => $"rooms/{roomId}"; + protected override string Target => $"rooms/{RoomId}"; } } diff --git a/osu.Game/Online/Multiplayer/GetRoomsRequest.cs b/osu.Game/Online/Rooms/GetRoomsRequest.cs similarity index 92% rename from osu.Game/Online/Multiplayer/GetRoomsRequest.cs rename to osu.Game/Online/Rooms/GetRoomsRequest.cs index a0609f77dd..e45365797a 100644 --- a/osu.Game/Online/Multiplayer/GetRoomsRequest.cs +++ b/osu.Game/Online/Rooms/GetRoomsRequest.cs @@ -5,9 +5,9 @@ using System.Collections.Generic; using Humanizer; using osu.Framework.IO.Network; using osu.Game.Online.API; -using osu.Game.Screens.Multi.Lounge.Components; +using osu.Game.Screens.OnlinePlay.Lounge.Components; -namespace osu.Game.Online.Multiplayer +namespace osu.Game.Online.Rooms { public class GetRoomsRequest : APIRequest> { diff --git a/osu.Game/Online/Multiplayer/IndexPlaylistScoresRequest.cs b/osu.Game/Online/Rooms/IndexPlaylistScoresRequest.cs similarity index 97% rename from osu.Game/Online/Multiplayer/IndexPlaylistScoresRequest.cs rename to osu.Game/Online/Rooms/IndexPlaylistScoresRequest.cs index 684d0aecd8..43f80a2dc4 100644 --- a/osu.Game/Online/Multiplayer/IndexPlaylistScoresRequest.cs +++ b/osu.Game/Online/Rooms/IndexPlaylistScoresRequest.cs @@ -8,7 +8,7 @@ using osu.Game.Extensions; using osu.Game.Online.API; using osu.Game.Online.API.Requests; -namespace osu.Game.Online.Multiplayer +namespace osu.Game.Online.Rooms { /// /// Returns a list of scores for the specified playlist item. diff --git a/osu.Game/Online/Multiplayer/IndexScoresParams.cs b/osu.Game/Online/Rooms/IndexScoresParams.cs similarity index 94% rename from osu.Game/Online/Multiplayer/IndexScoresParams.cs rename to osu.Game/Online/Rooms/IndexScoresParams.cs index a511e9a780..3df8c8e753 100644 --- a/osu.Game/Online/Multiplayer/IndexScoresParams.cs +++ b/osu.Game/Online/Rooms/IndexScoresParams.cs @@ -6,7 +6,7 @@ using JetBrains.Annotations; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -namespace osu.Game.Online.Multiplayer +namespace osu.Game.Online.Rooms { /// /// A collection of parameters which should be passed to the index endpoint to fetch the next page. diff --git a/osu.Game/Online/Multiplayer/IndexedMultiplayerScores.cs b/osu.Game/Online/Rooms/IndexedMultiplayerScores.cs similarity index 95% rename from osu.Game/Online/Multiplayer/IndexedMultiplayerScores.cs rename to osu.Game/Online/Rooms/IndexedMultiplayerScores.cs index e237b7e3fb..2008d1aa52 100644 --- a/osu.Game/Online/Multiplayer/IndexedMultiplayerScores.cs +++ b/osu.Game/Online/Rooms/IndexedMultiplayerScores.cs @@ -4,7 +4,7 @@ using JetBrains.Annotations; using Newtonsoft.Json; -namespace osu.Game.Online.Multiplayer +namespace osu.Game.Online.Rooms { /// /// A object returned via a . diff --git a/osu.Game/Online/Multiplayer/JoinRoomRequest.cs b/osu.Game/Online/Rooms/JoinRoomRequest.cs similarity index 94% rename from osu.Game/Online/Multiplayer/JoinRoomRequest.cs rename to osu.Game/Online/Rooms/JoinRoomRequest.cs index 74375af856..faa20a3e6c 100644 --- a/osu.Game/Online/Multiplayer/JoinRoomRequest.cs +++ b/osu.Game/Online/Rooms/JoinRoomRequest.cs @@ -5,7 +5,7 @@ using System.Net.Http; using osu.Framework.IO.Network; using osu.Game.Online.API; -namespace osu.Game.Online.Multiplayer +namespace osu.Game.Online.Rooms { public class JoinRoomRequest : APIRequest { diff --git a/osu.Game/Online/Multiplayer/MultiplayerScore.cs b/osu.Game/Online/Rooms/MultiplayerScore.cs similarity index 98% rename from osu.Game/Online/Multiplayer/MultiplayerScore.cs rename to osu.Game/Online/Rooms/MultiplayerScore.cs index 8191003aad..677a3d3026 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerScore.cs +++ b/osu.Game/Online/Rooms/MultiplayerScore.cs @@ -13,7 +13,7 @@ using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Users; -namespace osu.Game.Online.Multiplayer +namespace osu.Game.Online.Rooms { public class MultiplayerScore { diff --git a/osu.Game/Online/Multiplayer/MultiplayerScores.cs b/osu.Game/Online/Rooms/MultiplayerScores.cs similarity index 95% rename from osu.Game/Online/Multiplayer/MultiplayerScores.cs rename to osu.Game/Online/Rooms/MultiplayerScores.cs index 7b9dcff828..3f970b2f8e 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerScores.cs +++ b/osu.Game/Online/Rooms/MultiplayerScores.cs @@ -5,7 +5,7 @@ using System.Collections.Generic; using Newtonsoft.Json; using osu.Game.Online.API.Requests; -namespace osu.Game.Online.Multiplayer +namespace osu.Game.Online.Rooms { /// /// An object which contains scores and related data for fetching next pages. diff --git a/osu.Game/Online/Multiplayer/MultiplayerScoresAround.cs b/osu.Game/Online/Rooms/MultiplayerScoresAround.cs similarity index 95% rename from osu.Game/Online/Multiplayer/MultiplayerScoresAround.cs rename to osu.Game/Online/Rooms/MultiplayerScoresAround.cs index 2ac62d0300..a99439312a 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerScoresAround.cs +++ b/osu.Game/Online/Rooms/MultiplayerScoresAround.cs @@ -4,7 +4,7 @@ using JetBrains.Annotations; using Newtonsoft.Json; -namespace osu.Game.Online.Multiplayer +namespace osu.Game.Online.Rooms { /// /// An object which stores scores higher and lower than the user's score. diff --git a/osu.Game/Online/Multiplayer/PartRoomRequest.cs b/osu.Game/Online/Rooms/PartRoomRequest.cs similarity index 94% rename from osu.Game/Online/Multiplayer/PartRoomRequest.cs rename to osu.Game/Online/Rooms/PartRoomRequest.cs index 54bb005d96..2f036abc8c 100644 --- a/osu.Game/Online/Multiplayer/PartRoomRequest.cs +++ b/osu.Game/Online/Rooms/PartRoomRequest.cs @@ -5,7 +5,7 @@ using System.Net.Http; using osu.Framework.IO.Network; using osu.Game.Online.API; -namespace osu.Game.Online.Multiplayer +namespace osu.Game.Online.Rooms { public class PartRoomRequest : APIRequest { diff --git a/osu.Game/Online/Multiplayer/PlaylistExtensions.cs b/osu.Game/Online/Rooms/PlaylistExtensions.cs similarity index 93% rename from osu.Game/Online/Multiplayer/PlaylistExtensions.cs rename to osu.Game/Online/Rooms/PlaylistExtensions.cs index fe3d96e295..992011da3c 100644 --- a/osu.Game/Online/Multiplayer/PlaylistExtensions.cs +++ b/osu.Game/Online/Rooms/PlaylistExtensions.cs @@ -6,7 +6,7 @@ using Humanizer; using Humanizer.Localisation; using osu.Framework.Bindables; -namespace osu.Game.Online.Multiplayer +namespace osu.Game.Online.Rooms { public static class PlaylistExtensions { diff --git a/osu.Game/Online/Multiplayer/PlaylistItem.cs b/osu.Game/Online/Rooms/PlaylistItem.cs similarity index 94% rename from osu.Game/Online/Multiplayer/PlaylistItem.cs rename to osu.Game/Online/Rooms/PlaylistItem.cs index 416091a1aa..ada2140ca6 100644 --- a/osu.Game/Online/Multiplayer/PlaylistItem.cs +++ b/osu.Game/Online/Rooms/PlaylistItem.cs @@ -10,7 +10,7 @@ using osu.Game.Online.API; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; -namespace osu.Game.Online.Multiplayer +namespace osu.Game.Online.Rooms { public class PlaylistItem : IEquatable { @@ -64,8 +64,8 @@ namespace osu.Game.Online.Multiplayer public void MapObjects(BeatmapManager beatmaps, RulesetStore rulesets) { - Beatmap.Value = apiBeatmap.ToBeatmap(rulesets); - Ruleset.Value = rulesets.GetRuleset(RulesetID); + Beatmap.Value ??= apiBeatmap.ToBeatmap(rulesets); + Ruleset.Value ??= rulesets.GetRuleset(RulesetID); Ruleset rulesetInstance = Ruleset.Value.CreateInstance(); diff --git a/osu.Game/Online/Multiplayer/Room.cs b/osu.Game/Online/Rooms/Room.cs similarity index 79% rename from osu.Game/Online/Multiplayer/Room.cs rename to osu.Game/Online/Rooms/Room.cs index 9a21543b2e..763ba25d52 100644 --- a/osu.Game/Online/Multiplayer/Room.cs +++ b/osu.Game/Online/Rooms/Room.cs @@ -6,11 +6,12 @@ using System.Linq; using Newtonsoft.Json; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Game.Online.Multiplayer.GameTypes; -using osu.Game.Online.Multiplayer.RoomStatuses; +using osu.Game.IO.Serialization.Converters; +using osu.Game.Online.Rooms.GameTypes; +using osu.Game.Online.Rooms.RoomStatuses; using osu.Game.Users; -namespace osu.Game.Online.Multiplayer +namespace osu.Game.Online.Rooms { public class Room { @@ -35,12 +36,21 @@ namespace osu.Game.Online.Multiplayer public readonly Bindable ChannelId = new Bindable(); [Cached] - [JsonProperty("category")] + [JsonIgnore] public readonly Bindable Category = new Bindable(); + // Todo: osu-framework bug (https://github.com/ppy/osu-framework/issues/4106) + [JsonProperty("category")] + [JsonConverter(typeof(SnakeCaseStringEnumConverter))] + private RoomCategory category + { + get => Category.Value; + set => Category.Value = value; + } + [Cached] [JsonIgnore] - public readonly Bindable Duration = new Bindable(TimeSpan.FromMinutes(30)); + public readonly Bindable Duration = new Bindable(); [Cached] [JsonIgnore] @@ -56,7 +66,7 @@ namespace osu.Game.Online.Multiplayer [Cached] [JsonIgnore] - public readonly Bindable Type = new Bindable(new GameTypeTimeshift()); + public readonly Bindable Type = new Bindable(new GameTypePlaylists()); [Cached] [JsonIgnore] @@ -67,27 +77,26 @@ namespace osu.Game.Online.Multiplayer public readonly BindableList RecentParticipants = new BindableList(); [Cached] + [JsonProperty("participant_count")] public readonly Bindable ParticipantCount = new Bindable(); - // todo: TEMPORARY - [JsonProperty("participant_count")] - private int? participantCount - { - get => ParticipantCount.Value; - set => ParticipantCount.Value = value ?? 0; - } - [JsonProperty("duration")] - private int duration + private int? duration { - get => (int)Duration.Value.TotalMinutes; - set => Duration.Value = TimeSpan.FromMinutes(value); + get => (int?)Duration.Value?.TotalMinutes; + set + { + if (value == null) + Duration.Value = null; + else + Duration.Value = TimeSpan.FromMinutes(value.Value); + } } // Only supports retrieval for now [Cached] [JsonProperty("ends_at")] - public readonly Bindable EndDate = new Bindable(); + public readonly Bindable EndDate = new Bindable(); // Todo: Find a better way to do this (https://github.com/ppy/osu-framework/issues/1930) [JsonProperty("max_attempts", DefaultValueHandling = DefaultValueHandling.Ignore)] @@ -122,6 +131,9 @@ namespace osu.Game.Online.Multiplayer RoomID.Value = other.RoomID.Value; Name.Value = other.Name.Value; + if (other.Category.Value != RoomCategory.Spotlight) + Category.Value = other.Category.Value; + if (other.Host.Value != null && Host.Value?.Id != other.Host.Value.Id) Host.Value = other.Host.Value; @@ -133,7 +145,7 @@ namespace osu.Game.Online.Multiplayer ParticipantCount.Value = other.ParticipantCount.Value; EndDate.Value = other.EndDate.Value; - if (DateTimeOffset.Now >= EndDate.Value) + if (EndDate.Value != null && DateTimeOffset.Now >= EndDate.Value) Status.Value = new RoomStatusEnded(); if (!Playlist.SequenceEqual(other.Playlist)) diff --git a/osu.Game/Online/Multiplayer/RoomAvailability.cs b/osu.Game/Online/Rooms/RoomAvailability.cs similarity index 90% rename from osu.Game/Online/Multiplayer/RoomAvailability.cs rename to osu.Game/Online/Rooms/RoomAvailability.cs index 08fa853562..3aea0e5948 100644 --- a/osu.Game/Online/Multiplayer/RoomAvailability.cs +++ b/osu.Game/Online/Rooms/RoomAvailability.cs @@ -3,7 +3,7 @@ using System.ComponentModel; -namespace osu.Game.Online.Multiplayer +namespace osu.Game.Online.Rooms { public enum RoomAvailability { diff --git a/osu.Game/Online/Multiplayer/RoomCategory.cs b/osu.Game/Online/Rooms/RoomCategory.cs similarity index 69% rename from osu.Game/Online/Multiplayer/RoomCategory.cs rename to osu.Game/Online/Rooms/RoomCategory.cs index d6786a72fe..bb9f1298d3 100644 --- a/osu.Game/Online/Multiplayer/RoomCategory.cs +++ b/osu.Game/Online/Rooms/RoomCategory.cs @@ -1,10 +1,11 @@ // 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.Online.Multiplayer +namespace osu.Game.Online.Rooms { public enum RoomCategory { + // used for osu-web deserialization so names shouldn't be changed. Normal, Spotlight, Realtime, diff --git a/osu.Game/Online/Multiplayer/RoomStatus.cs b/osu.Game/Online/Rooms/RoomStatus.cs similarity index 93% rename from osu.Game/Online/Multiplayer/RoomStatus.cs rename to osu.Game/Online/Rooms/RoomStatus.cs index 3ff2770ab4..87c5aa3fda 100644 --- a/osu.Game/Online/Multiplayer/RoomStatus.cs +++ b/osu.Game/Online/Rooms/RoomStatus.cs @@ -1,10 +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 osuTK.Graphics; using osu.Game.Graphics; +using osuTK.Graphics; -namespace osu.Game.Online.Multiplayer +namespace osu.Game.Online.Rooms { public abstract class RoomStatus { diff --git a/osu.Game/Online/Multiplayer/RoomStatuses/RoomStatusEnded.cs b/osu.Game/Online/Rooms/RoomStatuses/RoomStatusEnded.cs similarity index 88% rename from osu.Game/Online/Multiplayer/RoomStatuses/RoomStatusEnded.cs rename to osu.Game/Online/Rooms/RoomStatuses/RoomStatusEnded.cs index 4177d28a99..c852f86f6b 100644 --- a/osu.Game/Online/Multiplayer/RoomStatuses/RoomStatusEnded.cs +++ b/osu.Game/Online/Rooms/RoomStatuses/RoomStatusEnded.cs @@ -4,7 +4,7 @@ using osu.Game.Graphics; using osuTK.Graphics; -namespace osu.Game.Online.Multiplayer.RoomStatuses +namespace osu.Game.Online.Rooms.RoomStatuses { public class RoomStatusEnded : RoomStatus { diff --git a/osu.Game/Online/Multiplayer/RoomStatuses/RoomStatusOpen.cs b/osu.Game/Online/Rooms/RoomStatuses/RoomStatusOpen.cs similarity index 89% rename from osu.Game/Online/Multiplayer/RoomStatuses/RoomStatusOpen.cs rename to osu.Game/Online/Rooms/RoomStatuses/RoomStatusOpen.cs index 45a1cb1909..4f7f0d6f5d 100644 --- a/osu.Game/Online/Multiplayer/RoomStatuses/RoomStatusOpen.cs +++ b/osu.Game/Online/Rooms/RoomStatuses/RoomStatusOpen.cs @@ -4,7 +4,7 @@ using osu.Game.Graphics; using osuTK.Graphics; -namespace osu.Game.Online.Multiplayer.RoomStatuses +namespace osu.Game.Online.Rooms.RoomStatuses { public class RoomStatusOpen : RoomStatus { diff --git a/osu.Game/Online/Multiplayer/RoomStatuses/RoomStatusPlaying.cs b/osu.Game/Online/Rooms/RoomStatuses/RoomStatusPlaying.cs similarity index 88% rename from osu.Game/Online/Multiplayer/RoomStatuses/RoomStatusPlaying.cs rename to osu.Game/Online/Rooms/RoomStatuses/RoomStatusPlaying.cs index b2cb5c4510..f04f1b23af 100644 --- a/osu.Game/Online/Multiplayer/RoomStatuses/RoomStatusPlaying.cs +++ b/osu.Game/Online/Rooms/RoomStatuses/RoomStatusPlaying.cs @@ -4,7 +4,7 @@ using osu.Game.Graphics; using osuTK.Graphics; -namespace osu.Game.Online.Multiplayer.RoomStatuses +namespace osu.Game.Online.Rooms.RoomStatuses { public class RoomStatusPlaying : RoomStatus { diff --git a/osu.Game/Online/Multiplayer/ShowPlaylistUserScoreRequest.cs b/osu.Game/Online/Rooms/ShowPlaylistUserScoreRequest.cs similarity index 95% rename from osu.Game/Online/Multiplayer/ShowPlaylistUserScoreRequest.cs rename to osu.Game/Online/Rooms/ShowPlaylistUserScoreRequest.cs index 936b8bbe89..3f728a5417 100644 --- a/osu.Game/Online/Multiplayer/ShowPlaylistUserScoreRequest.cs +++ b/osu.Game/Online/Rooms/ShowPlaylistUserScoreRequest.cs @@ -3,7 +3,7 @@ using osu.Game.Online.API; -namespace osu.Game.Online.Multiplayer +namespace osu.Game.Online.Rooms { public class ShowPlaylistUserScoreRequest : APIRequest { diff --git a/osu.Game/Online/Multiplayer/SubmitRoomScoreRequest.cs b/osu.Game/Online/Rooms/SubmitRoomScoreRequest.cs similarity index 97% rename from osu.Game/Online/Multiplayer/SubmitRoomScoreRequest.cs rename to osu.Game/Online/Rooms/SubmitRoomScoreRequest.cs index d31aef2ea5..5a78b9fabd 100644 --- a/osu.Game/Online/Multiplayer/SubmitRoomScoreRequest.cs +++ b/osu.Game/Online/Rooms/SubmitRoomScoreRequest.cs @@ -7,7 +7,7 @@ using osu.Framework.IO.Network; using osu.Game.Online.API; using osu.Game.Scoring; -namespace osu.Game.Online.Multiplayer +namespace osu.Game.Online.Rooms { public class SubmitRoomScoreRequest : APIRequest { diff --git a/osu.Game/Online/Spectator/FrameHeader.cs b/osu.Game/Online/Spectator/FrameHeader.cs index b4988fecf9..135b356eda 100644 --- a/osu.Game/Online/Spectator/FrameHeader.cs +++ b/osu.Game/Online/Spectator/FrameHeader.cs @@ -14,6 +14,11 @@ namespace osu.Game.Online.Spectator [Serializable] public class FrameHeader { + /// + /// The current accuracy of the score. + /// + public double Accuracy { get; set; } + /// /// The current combo of the score. /// @@ -42,16 +47,18 @@ namespace osu.Game.Online.Spectator { Combo = score.Combo; MaxCombo = score.MaxCombo; + Accuracy = score.Accuracy; // copy for safety Statistics = new Dictionary(score.Statistics); } [JsonConstructor] - public FrameHeader(int combo, int maxCombo, Dictionary statistics, DateTimeOffset receivedTime) + public FrameHeader(int combo, int maxCombo, double accuracy, Dictionary statistics, DateTimeOffset receivedTime) { Combo = combo; MaxCombo = maxCombo; + Accuracy = accuracy; Statistics = statistics; ReceivedTime = receivedTime; } diff --git a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs index 0167a5d025..344b73f3d9 100644 --- a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs +++ b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs @@ -81,6 +81,13 @@ namespace osu.Game.Online.Spectator /// public event Action OnUserFinishedPlaying; + private readonly string endpoint; + + public SpectatorStreamingClient(EndpointConfiguration endpoints) + { + endpoint = endpoints.SpectatorEndpointUrl; + } + [BackgroundDependencyLoader] private void load() { @@ -104,8 +111,6 @@ namespace osu.Game.Online.Spectator } } - private const string endpoint = "https://spectator.ppy.sh/spectator"; - protected virtual async Task Connect() { if (connection != null) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index e382ff5d48..14161f71e2 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -80,6 +80,9 @@ namespace osu.Game private BeatmapSetOverlay beatmapSetOverlay; + [Cached] + private readonly DifficultyRecommender difficultyRecommender = new DifficultyRecommender(); + [Cached] private readonly ScreenshotManager screenshotManager = new ScreenshotManager(); @@ -291,7 +294,7 @@ namespace osu.Game public void OpenUrlExternally(string url) => waitForReady(() => externalLinkOpener, _ => { if (url.StartsWith('/')) - url = $"{API.Endpoint}{url}"; + url = $"{API.APIEndpointUrl}{url}"; externalLinkOpener.OpenUrlExternally(url); }); @@ -335,15 +338,17 @@ namespace osu.Game /// The user should have already requested this interactively. /// /// The beatmap to select. - /// - /// Optional predicate used to try and find a difficulty to select. - /// If omitted, this will try to present the first beatmap from the current ruleset. - /// In case of failure the first difficulty of the set will be presented, ignoring the predicate. - /// + /// Optional predicate used to narrow the set of difficulties to select from when presenting. + /// + /// Among items satisfying the predicate, the order of preference is: + /// + /// beatmap with recommended difficulty, as provided by , + /// first beatmap from the current ruleset, + /// first beatmap from any ruleset. + /// + /// public void PresentBeatmap(BeatmapSetInfo beatmap, Predicate difficultyCriteria = null) { - difficultyCriteria ??= b => b.Ruleset.Equals(Ruleset.Value); - var databasedSet = beatmap.OnlineBeatmapSetID != null ? BeatmapManager.QueryBeatmapSet(s => s.OnlineBeatmapSetID == beatmap.OnlineBeatmapSetID) : BeatmapManager.QueryBeatmapSet(s => s.Hash == beatmap.Hash); @@ -361,16 +366,23 @@ namespace osu.Game menuScreen.LoadToSolo(); // we might even already be at the song - if (Beatmap.Value.BeatmapSetInfo.Hash == databasedSet.Hash && difficultyCriteria(Beatmap.Value.BeatmapInfo)) - { + if (Beatmap.Value.BeatmapSetInfo.Hash == databasedSet.Hash && (difficultyCriteria?.Invoke(Beatmap.Value.BeatmapInfo) ?? true)) return; - } - // Find first beatmap that matches our predicate. - var first = databasedSet.Beatmaps.Find(difficultyCriteria) ?? databasedSet.Beatmaps.First(); + // Find beatmaps that match our predicate. + var beatmaps = databasedSet.Beatmaps.Where(b => difficultyCriteria?.Invoke(b) ?? true).ToList(); - Ruleset.Value = first.Ruleset; - Beatmap.Value = BeatmapManager.GetWorkingBeatmap(first); + // Use all beatmaps if predicate matched nothing + if (beatmaps.Count == 0) + beatmaps = databasedSet.Beatmaps; + + // Prefer recommended beatmap if recommendations are available, else fallback to a sane selection. + var selection = difficultyRecommender.GetRecommendedBeatmap(beatmaps) + ?? beatmaps.FirstOrDefault(b => b.Ruleset.Equals(Ruleset.Value)) + ?? beatmaps.First(); + + Ruleset.Value = selection.Ruleset; + Beatmap.Value = BeatmapManager.GetWorkingBeatmap(selection); }, validScreens: new[] { typeof(PlaySongSelect) }); } @@ -630,6 +642,8 @@ namespace osu.Game GetStableStorage = GetStorageForStableInstall }, Add, true); + loadComponentSingleFile(difficultyRecommender, Add); + loadComponentSingleFile(screenshotManager, Add); // dependency on notification overlay, dependent by settings overlay diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 7def93255b..f56850ca82 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -30,6 +30,8 @@ using osu.Game.Database; using osu.Game.Input; using osu.Game.Input.Bindings; using osu.Game.IO; +using osu.Game.Online; +using osu.Game.Online.Multiplayer; using osu.Game.Online.Spectator; using osu.Game.Overlays; using osu.Game.Resources; @@ -53,6 +55,8 @@ namespace osu.Game public const int SAMPLE_CONCURRENCY = 6; + public bool UseDevelopmentServer { get; } + protected OsuConfigManager LocalConfig; protected BeatmapManager BeatmapManager; @@ -78,6 +82,7 @@ namespace osu.Game protected IAPIProvider API; private SpectatorStreamingClient spectatorStreaming; + private StatefulMultiplayerClient multiplayerClient; protected MenuCursorContainer MenuCursorContainer; @@ -130,6 +135,7 @@ namespace osu.Game public OsuGameBase() { + UseDevelopmentServer = DebugUtils.IsDebugBuild; Name = @"osu!lazer"; } @@ -168,7 +174,7 @@ namespace osu.Game dependencies.Cache(largeStore); dependencies.CacheAs(this); - dependencies.Cache(LocalConfig); + dependencies.CacheAs(LocalConfig); AddFont(Resources, @"Fonts/osuFont"); @@ -208,9 +214,12 @@ namespace osu.Game } }); - dependencies.CacheAs(API ??= new APIAccess(LocalConfig)); + EndpointConfiguration endpoints = UseDevelopmentServer ? (EndpointConfiguration)new DevelopmentEndpointConfiguration() : new ProductionEndpointConfiguration(); - dependencies.CacheAs(spectatorStreaming = new SpectatorStreamingClient()); + dependencies.CacheAs(API ??= new APIAccess(LocalConfig, endpoints)); + + dependencies.CacheAs(spectatorStreaming = new SpectatorStreamingClient(endpoints)); + dependencies.CacheAs(multiplayerClient = new MultiplayerClient(endpoints)); var defaultBeatmap = new DummyWorkingBeatmap(Audio, Textures); @@ -277,6 +286,7 @@ namespace osu.Game if (API is APIAccess apiAccess) AddInternal(apiAccess); AddInternal(spectatorStreaming); + AddInternal(multiplayerClient); AddInternal(RulesetConfigCache); @@ -365,7 +375,9 @@ namespace osu.Game // may be non-null for certain tests Storage ??= host.Storage; - LocalConfig ??= new OsuConfigManager(Storage); + LocalConfig ??= UseDevelopmentServer + ? new DevelopmentOsuConfigManager(Storage) + : new OsuConfigManager(Storage); } protected override Storage CreateStorage(GameHost host, Storage defaultStorage) => new OsuStorage(host, defaultStorage); diff --git a/osu.Game/Overlays/BeatmapSet/Header.cs b/osu.Game/Overlays/BeatmapSet/Header.cs index 06e31277dd..321e496511 100644 --- a/osu.Game/Overlays/BeatmapSet/Header.cs +++ b/osu.Game/Overlays/BeatmapSet/Header.cs @@ -15,6 +15,7 @@ using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Online; +using osu.Game.Online.API; using osu.Game.Overlays.BeatmapListing.Panels; using osu.Game.Overlays.BeatmapSet.Buttons; using osu.Game.Rulesets; @@ -40,6 +41,9 @@ namespace osu.Game.Overlays.BeatmapSet public bool DownloadButtonsVisible => downloadButtonsContainer.Any(); + [Resolved] + private IAPIProvider api { get; set; } + public BeatmapRulesetSelector RulesetSelector => beatmapSetHeader.RulesetSelector; public readonly BeatmapPicker Picker; @@ -213,7 +217,7 @@ namespace osu.Game.Overlays.BeatmapSet Picker.Beatmap.ValueChanged += b => { Details.Beatmap = b.NewValue; - externalLink.Link = $@"https://osu.ppy.sh/beatmapsets/{BeatmapSet.Value?.OnlineBeatmapSetID}#{b.NewValue?.Ruleset.ShortName}/{b.NewValue?.OnlineBeatmapID}"; + externalLink.Link = $@"{api.WebsiteRootUrl}/beatmapsets/{BeatmapSet.Value?.OnlineBeatmapSetID}#{b.NewValue?.Ruleset.ShortName}/{b.NewValue?.OnlineBeatmapID}"; }; } diff --git a/osu.Game/Overlays/Changelog/ChangelogBuild.cs b/osu.Game/Overlays/Changelog/ChangelogBuild.cs index 48bf6c2ddd..65ff0fef92 100644 --- a/osu.Game/Overlays/Changelog/ChangelogBuild.cs +++ b/osu.Game/Overlays/Changelog/ChangelogBuild.cs @@ -131,33 +131,36 @@ namespace osu.Game.Overlays.Changelog t.Padding = new MarginPadding { Left = 10 }; }); - if (entry.GithubUser.UserId != null) + if (entry.GithubUser != null) { - title.AddUserLink(new User + if (entry.GithubUser.UserId != null) { - Username = entry.GithubUser.OsuUsername, - Id = entry.GithubUser.UserId.Value - }, t => + title.AddUserLink(new User + { + Username = entry.GithubUser.OsuUsername, + Id = entry.GithubUser.UserId.Value + }, t => + { + t.Font = fontMedium; + t.Colour = entryColour; + }); + } + else if (entry.GithubUser.GithubUrl != null) { - t.Font = fontMedium; - t.Colour = entryColour; - }); - } - else if (entry.GithubUser.GithubUrl != null) - { - title.AddLink(entry.GithubUser.DisplayName, entry.GithubUser.GithubUrl, t => + title.AddLink(entry.GithubUser.DisplayName, entry.GithubUser.GithubUrl, t => + { + t.Font = fontMedium; + t.Colour = entryColour; + }); + } + else { - t.Font = fontMedium; - t.Colour = entryColour; - }); - } - else - { - title.AddText(entry.GithubUser.DisplayName, t => - { - t.Font = fontMedium; - t.Colour = entryColour; - }); + title.AddText(entry.GithubUser.DisplayName, t => + { + t.Font = fontMedium; + t.Colour = entryColour; + }); + } } ChangelogEntries.Add(titleContainer); diff --git a/osu.Game/Overlays/Chat/DrawableChannel.cs b/osu.Game/Overlays/Chat/DrawableChannel.cs index d63faebae4..5926d11c03 100644 --- a/osu.Game/Overlays/Chat/DrawableChannel.cs +++ b/osu.Game/Overlays/Chat/DrawableChannel.cs @@ -103,7 +103,7 @@ namespace osu.Game.Overlays.Chat Colour = colours.ChatBlue.Lighten(0.7f), }; - private void newMessagesArrived(IEnumerable newMessages) + private void newMessagesArrived(IEnumerable newMessages) => Schedule(() => { if (newMessages.Min(m => m.Id) < chatLines.Max(c => c.Message.Id)) { @@ -155,9 +155,9 @@ namespace osu.Game.Overlays.Chat if (shouldScrollToEnd) scrollToEnd(); - } + }); - private void pendingMessageResolved(Message existing, Message updated) + private void pendingMessageResolved(Message existing, Message updated) => Schedule(() => { var found = chatLines.LastOrDefault(c => c.Message == existing); @@ -169,12 +169,12 @@ namespace osu.Game.Overlays.Chat found.Message = updated; ChatLineFlow.Add(found); } - } + }); - private void messageRemoved(Message removed) + private void messageRemoved(Message removed) => Schedule(() => { chatLines.FirstOrDefault(c => c.Message == removed)?.FadeColour(Color4.Red, 400).FadeOut(600).Expire(); - } + }); private IEnumerable chatLines => ChatLineFlow.Children.OfType(); diff --git a/osu.Game/Overlays/Chat/Tabs/PrivateChannelTabItem.cs b/osu.Game/Overlays/Chat/Tabs/PrivateChannelTabItem.cs index 5b428a3825..00f46b0035 100644 --- a/osu.Game/Overlays/Chat/Tabs/PrivateChannelTabItem.cs +++ b/osu.Game/Overlays/Chat/Tabs/PrivateChannelTabItem.cs @@ -25,7 +25,7 @@ namespace osu.Game.Overlays.Chat.Tabs if (value.Type != ChannelType.PM) throw new ArgumentException("Argument value needs to have the targettype user!"); - DrawableAvatar avatar; + ClickableAvatar avatar; AddRange(new Drawable[] { @@ -48,7 +48,7 @@ namespace osu.Game.Overlays.Chat.Tabs Anchor = Anchor.Centre, Origin = Anchor.Centre, Masking = true, - Child = new DelayedLoadWrapper(avatar = new DrawableAvatar(value.Users.First()) + Child = new DelayedLoadWrapper(avatar = new ClickableAvatar(value.Users.First()) { RelativeSizeAxes = Axes.Both, OpenOnClick = { Value = false }, diff --git a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs index d39a81f5e8..c89699f2ee 100644 --- a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs +++ b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs @@ -11,7 +11,7 @@ using osu.Framework.Screens; using osu.Game.Database; using osu.Game.Online.API; using osu.Game.Online.Spectator; -using osu.Game.Screens.Multi.Match.Components; +using osu.Game.Screens.OnlinePlay.Match.Components; using osu.Game.Screens.Play; using osu.Game.Users; using osuTK; diff --git a/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs index ebee377a51..2925107766 100644 --- a/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs +++ b/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs @@ -12,6 +12,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; using osu.Game.Graphics.Containers; +using osu.Game.Online.API; using osu.Game.Users; using osuTK; using osuTK.Graphics; @@ -27,6 +28,9 @@ namespace osu.Game.Overlays.Profile.Header private Color4 iconColour; + [Resolved] + private IAPIProvider api { get; set; } + public BottomHeaderContainer() { AutoSizeAxes = Axes.Y; @@ -109,7 +113,7 @@ namespace osu.Game.Overlays.Profile.Header } topLinkContainer.AddText("Contributed "); - topLinkContainer.AddLink($@"{user.PostCount:#,##0} forum posts", $"https://osu.ppy.sh/users/{user.Id}/posts", creationParameters: embolden); + topLinkContainer.AddLink($@"{user.PostCount:#,##0} forum posts", $"{api.WebsiteRootUrl}/users/{user.Id}/posts", creationParameters: embolden); string websiteWithoutProtocol = user.Website; diff --git a/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs index 2cc1f6533f..e0642d650c 100644 --- a/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs +++ b/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API; using osu.Game.Overlays.Profile.Header.Components; using osu.Game.Users; using osu.Game.Users.Drawables; @@ -23,6 +24,9 @@ namespace osu.Game.Overlays.Profile.Header public readonly Bindable User = new Bindable(); + [Resolved] + private IAPIProvider api { get; set; } + private SupporterIcon supporterTag; private UpdateableAvatar avatar; private OsuSpriteText usernameText; @@ -166,7 +170,7 @@ namespace osu.Game.Overlays.Profile.Header { avatar.User = user; usernameText.Text = user?.Username ?? string.Empty; - openUserExternally.Link = $@"https://osu.ppy.sh/users/{user?.Id ?? 0}"; + openUserExternally.Link = $@"{api.WebsiteRootUrl}/users/{user?.Id ?? 0}"; userFlag.Country = user?.Country; userCountryText.Text = user?.Country?.FullName ?? "Alien"; supporterTag.SupportLevel = user?.SupportLevel ?? 0; diff --git a/osu.Game/Overlays/Profile/Sections/Recent/DrawableRecentActivity.cs b/osu.Game/Overlays/Profile/Sections/Recent/DrawableRecentActivity.cs index 8782e82642..49b46f7e7a 100644 --- a/osu.Game/Overlays/Profile/Sections/Recent/DrawableRecentActivity.cs +++ b/osu.Game/Overlays/Profile/Sections/Recent/DrawableRecentActivity.cs @@ -216,7 +216,7 @@ namespace osu.Game.Overlays.Profile.Sections.Recent private void addBeatmapsetLink() => content.AddLink(activity.Beatmapset?.Title, LinkAction.OpenBeatmapSet, getLinkArgument(activity.Beatmapset?.Url), creationParameters: t => t.Font = getLinkFont()); - private string getLinkArgument(string url) => MessageFormatter.GetLinkDetails($"{api.Endpoint}{url}").Argument; + private string getLinkArgument(string url) => MessageFormatter.GetLinkDetails($"{api.APIEndpointUrl}{url}").Argument; private FontUsage getLinkFont(FontWeight fontWeight = FontWeight.Regular) => OsuFont.GetFont(size: font_size, weight: fontWeight, italics: true); diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs index 62dc1dc806..3d3b543d70 100644 --- a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.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 System.Collections.Generic; +using System; using System.Drawing; using System.Linq; using osu.Framework.Allocation; @@ -25,9 +25,13 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics private FillFlowContainer> scalingSettings; + private readonly IBindable currentDisplay = new Bindable(); + private readonly IBindableList windowModes = new BindableList(); + private Bindable scalingMode; private Bindable sizeFullscreen; - private readonly IBindableList windowModes = new BindableList(); + + private readonly BindableList resolutions = new BindableList(new[] { new Size(9999, 9999) }); [Resolved] private OsuGameBase game { get; set; } @@ -53,22 +57,25 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics scalingPositionY = osuConfig.GetBindable(OsuSetting.ScalingPositionY); if (host.Window != null) + { + currentDisplay.BindTo(host.Window.CurrentDisplayBindable); windowModes.BindTo(host.Window.SupportedWindowModes); - - Container resolutionSettingsContainer; + } Children = new Drawable[] { windowModeDropdown = new SettingsDropdown { LabelText = "Screen mode", - Current = config.GetBindable(FrameworkSetting.WindowMode), ItemSource = windowModes, + Current = config.GetBindable(FrameworkSetting.WindowMode), }, - resolutionSettingsContainer = new Container + resolutionDropdown = new ResolutionSettingsDropdown { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y + LabelText = "Resolution", + ShowsDefaultIndicator = false, + ItemSource = resolutions, + Current = sizeFullscreen }, new SettingsSlider { @@ -126,31 +133,33 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics }, }; - scalingSettings.ForEach(s => bindPreviewEvent(s.Current)); - - var resolutions = getResolutions(); - - if (resolutions.Count > 1) + windowModes.BindCollectionChanged((sender, args) => { - resolutionSettingsContainer.Child = resolutionDropdown = new ResolutionSettingsDropdown - { - LabelText = "Resolution", - ShowsDefaultIndicator = false, - Items = resolutions, - Current = sizeFullscreen - }; + if (windowModes.Count > 1) + windowModeDropdown.Show(); + else + windowModeDropdown.Hide(); + }, true); - windowModeDropdown.Current.BindValueChanged(mode => + windowModeDropdown.Current.ValueChanged += _ => updateResolutionDropdown(); + + currentDisplay.BindValueChanged(display => Schedule(() => + { + resolutions.RemoveRange(1, resolutions.Count - 1); + + if (display.NewValue != null) { - if (mode.NewValue == WindowMode.Fullscreen) - { - resolutionDropdown.Show(); - sizeFullscreen.TriggerChange(); - } - else - resolutionDropdown.Hide(); - }, true); - } + resolutions.AddRange(display.NewValue.DisplayModes + .Where(m => m.Size.Width >= 800 && m.Size.Height >= 600) + .OrderByDescending(m => Math.Max(m.Size.Height, m.Size.Width)) + .Select(m => m.Size) + .Distinct()); + } + + updateResolutionDropdown(); + }), true); + + scalingSettings.ForEach(s => bindPreviewEvent(s.Current)); scalingMode.BindValueChanged(mode => { @@ -163,17 +172,13 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics scalingSettings.ForEach(s => s.TransferValueOnCommit = mode.NewValue == ScalingMode.Everything); }, true); - windowModes.CollectionChanged += (sender, args) => windowModesChanged(); - - windowModesChanged(); - } - - private void windowModesChanged() - { - if (windowModes.Count > 1) - windowModeDropdown.Show(); - else - windowModeDropdown.Hide(); + void updateResolutionDropdown() + { + if (resolutions.Count > 1 && windowModeDropdown.Current.Value == WindowMode.Fullscreen) + resolutionDropdown.Show(); + else + resolutionDropdown.Hide(); + } } /// @@ -205,24 +210,6 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics preview.Expire(); } - private IReadOnlyList getResolutions() - { - var resolutions = new List { new Size(9999, 9999) }; - var currentDisplay = game.Window?.CurrentDisplayBindable.Value; - - if (currentDisplay != null) - { - resolutions.AddRange(currentDisplay.DisplayModes - .Where(m => m.Size.Width >= 800 && m.Size.Height >= 600) - .OrderByDescending(m => m.Size.Width) - .ThenByDescending(m => m.Size.Height) - .Select(m => m.Size) - .Distinct()); - } - - return resolutions; - } - private class ScalingPreview : ScalingContainer { public ScalingPreview() diff --git a/osu.Game/Overlays/Settings/Sections/UserInterface/MainMenuSettings.cs b/osu.Game/Overlays/Settings/Sections/UserInterface/MainMenuSettings.cs index 598b666642..95e2e9da30 100644 --- a/osu.Game/Overlays/Settings/Sections/UserInterface/MainMenuSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/UserInterface/MainMenuSettings.cs @@ -1,8 +1,6 @@ // 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.Allocation; using osu.Framework.Graphics; using osu.Game.Configuration; @@ -28,23 +26,20 @@ namespace osu.Game.Overlays.Settings.Sections.UserInterface LabelText = "osu! music theme", Current = config.GetBindable(OsuSetting.MenuMusic) }, - new SettingsDropdown + new SettingsEnumDropdown { LabelText = "Intro sequence", Current = config.GetBindable(OsuSetting.IntroSequence), - Items = Enum.GetValues(typeof(IntroSequence)).Cast() }, - new SettingsDropdown + new SettingsEnumDropdown { LabelText = "Background source", Current = config.GetBindable(OsuSetting.MenuBackgroundSource), - Items = Enum.GetValues(typeof(BackgroundSource)).Cast() }, - new SettingsDropdown + new SettingsEnumDropdown { LabelText = "Seasonal backgrounds", Current = config.GetBindable(OsuSetting.SeasonalBackgroundMode), - Items = Enum.GetValues(typeof(SeasonalBackgroundMode)).Cast() } }; } diff --git a/osu.Game/Rulesets/ILegacyRuleset.cs b/osu.Game/Rulesets/ILegacyRuleset.cs index 06a85b5261..f4b03baccd 100644 --- a/osu.Game/Rulesets/ILegacyRuleset.cs +++ b/osu.Game/Rulesets/ILegacyRuleset.cs @@ -5,6 +5,8 @@ namespace osu.Game.Rulesets { public interface ILegacyRuleset { + const int MAX_LEGACY_RULESET_ID = 3; + /// /// Identifies the server-side ID of a legacy ruleset. /// diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 644c67ea59..da6da0ea97 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -303,7 +303,8 @@ namespace osu.Game.Rulesets.Objects.Drawables samplesBindable.CollectionChanged -= onSamplesChanged; // Release the samples for other hitobjects to use. - Samples.Samples = null; + if (Samples != null) + Samples.Samples = null; if (nestedHitObjects.IsValueCreated) { diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 499673619f..2024290460 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -68,7 +68,12 @@ namespace osu.Game.Rulesets.Scoring private readonly double comboPortion; private int maxAchievableCombo; + + /// + /// The maximum achievable base score. + /// private double maxBaseScore; + private double rollingMaxBaseScore; private double baseScore; @@ -188,7 +193,7 @@ namespace osu.Game.Rulesets.Scoring private void updateScore() { if (rollingMaxBaseScore != 0) - Accuracy.Value = baseScore / rollingMaxBaseScore; + Accuracy.Value = calculateAccuracyRatio(baseScore, true); TotalScore.Value = getScore(Mode.Value); } @@ -196,8 +201,8 @@ namespace osu.Game.Rulesets.Scoring private double getScore(ScoringMode mode) { return GetScore(mode, maxAchievableCombo, - maxBaseScore > 0 ? baseScore / maxBaseScore : 0, - maxAchievableCombo > 0 ? (double)HighestCombo.Value / maxAchievableCombo : 1, + calculateAccuracyRatio(baseScore), + calculateComboRatio(HighestCombo.Value), scoreResultCounts); } @@ -227,6 +232,45 @@ namespace osu.Game.Rulesets.Scoring } } + /// + /// Given a minimal set of inputs, return the computed score for the tracked beatmap / mods combination, at the current point in time. + /// + /// The to compute the total score in. + /// The maximum combo achievable in the beatmap. + /// Statistics to be used for calculating accuracy, bonus score, etc. + /// The computed score for provided inputs. + public double GetImmediateScore(ScoringMode mode, int maxCombo, Dictionary statistics) + { + // calculate base score from statistics pairs + int computedBaseScore = 0; + + foreach (var pair in statistics) + { + if (!pair.Key.AffectsAccuracy()) + continue; + + computedBaseScore += Judgement.ToNumericResult(pair.Key) * pair.Value; + } + + return GetScore(mode, maxAchievableCombo, calculateAccuracyRatio(computedBaseScore), calculateComboRatio(maxCombo), scoreResultCounts); + } + + /// + /// Get the accuracy fraction for the provided base score. + /// + /// The score to be used for accuracy calculation. + /// Whether the rolling base score should be used (ie. for the current point in time based on Apply/Reverted results). + /// The computed accuracy. + private double calculateAccuracyRatio(double baseScore, bool preferRolling = false) + { + if (preferRolling && rollingMaxBaseScore != 0) + return baseScore / rollingMaxBaseScore; + + return maxBaseScore > 0 ? baseScore / maxBaseScore : 0; + } + + private double calculateComboRatio(int maxCombo) => maxAchievableCombo > 0 ? (double)maxCombo / maxAchievableCombo : 1; + private double getBonusScore(Dictionary statistics) => statistics.GetOrDefault(HitResult.SmallBonus) * Judgement.SMALL_BONUS_SCORE + statistics.GetOrDefault(HitResult.LargeBonus) * Judgement.LARGE_BONUS_SCORE; diff --git a/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs b/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs index a9b2a15b35..b13b20dae2 100644 --- a/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs +++ b/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs @@ -13,6 +13,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Textures; using osu.Framework.IO.Stores; +using osu.Framework.Platform; using osu.Game.Rulesets.Configuration; namespace osu.Game.Rulesets.UI @@ -46,7 +47,7 @@ namespace osu.Game.Rulesets.UI if (resources != null) { - TextureStore = new TextureStore(new TextureLoaderStore(new NamespacedResourceStore(resources, @"Textures"))); + TextureStore = new TextureStore(parent.Get().CreateTextureLoaderStore(new NamespacedResourceStore(resources, @"Textures"))); CacheAs(TextureStore = new FallbackTextureStore(TextureStore, parent.Get())); SampleStore = parent.Get().GetSampleStore(new NamespacedResourceStore(resources, @"Samples")); diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs index 347d9e3ba7..2f4721f63e 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs @@ -92,6 +92,7 @@ namespace osu.Game.Screens.Edit.Compose.Components } } + private Container dragHandles; private FillFlowContainer buttons; public const float BORDER_RADIUS = 3; @@ -151,6 +152,12 @@ namespace osu.Game.Screens.Edit.Compose.Components }, } }, + dragHandles = new Container + { + RelativeSizeAxes = Axes.Both, + // ensures that the centres of all drag handles line up with the middle of the selection box border. + Padding = new MarginPadding(BORDER_RADIUS / 2) + }, buttons = new FillFlowContainer { Y = 20, @@ -232,7 +239,7 @@ namespace osu.Game.Screens.Edit.Compose.Components }); } - private void addDragHandle(Anchor anchor) => AddInternal(new SelectionBoxDragHandle + private void addDragHandle(Anchor anchor) => dragHandles.Add(new SelectionBoxDragHandle { Anchor = anchor, HandleDrag = e => OnScale?.Invoke(e.Delta, anchor), diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs index 2f14c607c2..1fc529910b 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Framework.Utils; +using osu.Game.Graphics; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osu.Game.Screens.Edit.Components.Timelines.Summary.Parts; @@ -25,10 +26,15 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline [Resolved] private EditorBeatmap beatmap { get; set; } + [Resolved] + private OsuColour colours { get; set; } + private DragEvent lastDragEvent; private Bindable placement; private SelectionBlueprint placementBlueprint; + private readonly Box backgroundBox; + public TimelineBlueprintContainer(HitObjectComposer composer) : base(composer) { @@ -36,9 +42,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline Anchor = Anchor.Centre; Origin = Anchor.Centre; - Height = 0.4f; + Height = 0.6f; - AddInternal(new Box + AddInternal(backgroundBox = new Box { Colour = Color4.Black, RelativeSizeAxes = Axes.Both, @@ -77,6 +83,18 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline protected override Container CreateSelectionBlueprintContainer() => new TimelineSelectionBlueprintContainer { RelativeSizeAxes = Axes.Both }; + protected override bool OnHover(HoverEvent e) + { + backgroundBox.FadeColour(colours.BlueLighter, 120, Easing.OutQuint); + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + backgroundBox.FadeColour(Color4.Black, 600, Easing.OutQuint); + base.OnHoverLost(e); + } + protected override void OnDrag(DragEvent e) { handleScrollViaDrag(e); diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs index 657c5834b2..ae2a82fa10 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs @@ -29,7 +29,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { private const float thickness = 5; private const float shadow_radius = 5; - private const float circle_size = 24; + private const float circle_size = 34; public Action OnDragHandled; diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index ca7e5fbf20..223c678fba 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -461,7 +461,7 @@ namespace osu.Game.Screens.Edit if (dialogOverlay == null || dialogOverlay.CurrentDialog is PromptForSaveDialog) { confirmExit(); - return false; + return base.OnExiting(next); } if (isNewBeatmap || HasUnsavedChanges) diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index 4becdd58cd..474cbde192 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -42,8 +42,8 @@ namespace osu.Game.Screens.Menu public Action OnBeatmapListing; public Action OnSolo; public Action OnSettings; - public Action OnMulti; - public Action OnChart; + public Action OnMultiplayer; + public Action OnPlaylists; public const float BUTTON_WIDTH = 140f; public const float WEDGE_WIDTH = 20; @@ -124,8 +124,8 @@ namespace osu.Game.Screens.Menu private void load(AudioManager audio, IdleTracker idleTracker, GameHost host) { buttonsPlay.Add(new Button(@"solo", @"button-solo-select", FontAwesome.Solid.User, new Color4(102, 68, 204, 255), () => OnSolo?.Invoke(), WEDGE_WIDTH, Key.P)); - buttonsPlay.Add(new Button(@"multi", @"button-generic-select", FontAwesome.Solid.Users, new Color4(94, 63, 186, 255), onMulti, 0, Key.M)); - buttonsPlay.Add(new Button(@"chart", @"button-generic-select", OsuIcon.Charts, new Color4(80, 53, 160, 255), () => OnChart?.Invoke())); + buttonsPlay.Add(new Button(@"multi", @"button-generic-select", FontAwesome.Solid.Users, new Color4(94, 63, 186, 255), onMultiplayer, 0, Key.M)); + buttonsPlay.Add(new Button(@"playlists", @"button-generic-select", OsuIcon.Charts, new Color4(94, 63, 186, 255), onPlaylists, 0, Key.L)); buttonsPlay.ForEach(b => b.VisibleState = ButtonSystemState.Play); buttonsTopLevel.Add(new Button(@"play", @"button-play-select", OsuIcon.Logo, new Color4(102, 68, 204, 255), () => State = ButtonSystemState.Play, WEDGE_WIDTH, Key.P)); @@ -154,7 +154,7 @@ namespace osu.Game.Screens.Menu sampleBack = audio.Samples.Get(@"Menu/button-back-select"); } - private void onMulti() + private void onMultiplayer() { if (!api.IsLoggedIn) { @@ -172,7 +172,28 @@ namespace osu.Game.Screens.Menu return; } - OnMulti?.Invoke(); + OnMultiplayer?.Invoke(); + } + + private void onPlaylists() + { + if (!api.IsLoggedIn) + { + notifications?.Post(new SimpleNotification + { + Text = "You gotta be logged in to multi 'yo!", + Icon = FontAwesome.Solid.Globe, + Activated = () => + { + loginOverlay?.Show(); + return true; + } + }); + + return; + } + + OnPlaylists?.Invoke(); } private void updateIdleState(bool isIdle) diff --git a/osu.Game/Screens/Menu/Disclaimer.cs b/osu.Game/Screens/Menu/Disclaimer.cs index ceec12c967..46fddabb26 100644 --- a/osu.Game/Screens/Menu/Disclaimer.cs +++ b/osu.Game/Screens/Menu/Disclaimer.cs @@ -208,7 +208,7 @@ namespace osu.Game.Screens.Menu "Most of the web content (profiles, rankings, etc.) are available natively in-game from the icons on the toolbar!", "Get more details, hide or delete a beatmap by right-clicking on its panel at song select!", "All delete operations are temporary until exiting. Restore accidentally deleted content from the maintenance settings!", - "Check out the \"timeshift\" multiplayer system, which has local permanent leaderboards and playlist support!", + "Check out the \"playlists\" system, which lets users create their own custom and permanent leaderboards!", "Toggle advanced frame / thread statistics with Ctrl-F11!", "Take a look under the hood at performance counters and enable verbose performance logging with Ctrl-F2!", }; diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index c3ecd75963..9d5720ff34 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -17,7 +17,8 @@ using osu.Game.Online.API; using osu.Game.Overlays; using osu.Game.Screens.Backgrounds; using osu.Game.Screens.Edit; -using osu.Game.Screens.Multi; +using osu.Game.Screens.OnlinePlay.Multiplayer; +using osu.Game.Screens.OnlinePlay.Playlists; using osu.Game.Screens.Select; namespace osu.Game.Screens.Menu @@ -104,7 +105,8 @@ namespace osu.Game.Screens.Menu this.Push(new Editor()); }, OnSolo = onSolo, - OnMulti = delegate { this.Push(new Multiplayer()); }, + OnMultiplayer = () => this.Push(new Multiplayer()), + OnPlaylists = () => this.Push(new Playlists()), OnExit = confirmAndExit, } } @@ -136,7 +138,6 @@ namespace osu.Game.Screens.Menu buttons.OnSettings = () => settings?.ToggleVisibility(); buttons.OnBeatmapListing = () => beatmapListing?.ToggleVisibility(); - buttons.OnChart = () => rankings?.ShowSpotlights(); LoadComponentAsync(background = new BackgroundScreenDefault()); preloadSongSelect(); diff --git a/osu.Game/Screens/Multi/RoomManager.cs b/osu.Game/Screens/Multi/RoomManager.cs deleted file mode 100644 index fb0cf73bb9..0000000000 --- a/osu.Game/Screens/Multi/RoomManager.cs +++ /dev/null @@ -1,337 +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 System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Threading.Tasks; -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Logging; -using osu.Game.Beatmaps; -using osu.Game.Online; -using osu.Game.Online.API; -using osu.Game.Online.Multiplayer; -using osu.Game.Rulesets; -using osu.Game.Screens.Multi.Lounge.Components; - -namespace osu.Game.Screens.Multi -{ - public class RoomManager : CompositeDrawable, IRoomManager - { - public event Action RoomsUpdated; - - private readonly BindableList rooms = new BindableList(); - - public Bindable InitialRoomsReceived { get; } = new Bindable(); - - public IBindableList Rooms => rooms; - - public double TimeBetweenListingPolls - { - get => listingPollingComponent.TimeBetweenPolls; - set => listingPollingComponent.TimeBetweenPolls = value; - } - - public double TimeBetweenSelectionPolls - { - get => selectionPollingComponent.TimeBetweenPolls; - set => selectionPollingComponent.TimeBetweenPolls = value; - } - - [Resolved] - private RulesetStore rulesets { get; set; } - - [Resolved] - private BeatmapManager beatmaps { get; set; } - - [Resolved] - private IAPIProvider api { get; set; } - - [Resolved] - private Bindable selectedRoom { get; set; } - - private readonly ListingPollingComponent listingPollingComponent; - private readonly SelectionPollingComponent selectionPollingComponent; - - private Room joinedRoom; - - public RoomManager() - { - RelativeSizeAxes = Axes.Both; - - InternalChildren = new Drawable[] - { - listingPollingComponent = new ListingPollingComponent - { - InitialRoomsReceived = { BindTarget = InitialRoomsReceived }, - RoomsReceived = onListingReceived - }, - selectionPollingComponent = new SelectionPollingComponent { RoomReceived = onSelectedRoomReceived } - }; - } - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - PartRoom(); - } - - public void CreateRoom(Room room, Action onSuccess = null, Action onError = null) - { - room.Host.Value = api.LocalUser.Value; - - var req = new CreateRoomRequest(room); - - req.Success += result => - { - joinedRoom = room; - - update(room, result); - addRoom(room); - - RoomsUpdated?.Invoke(); - onSuccess?.Invoke(room); - }; - - req.Failure += exception => - { - if (req.Result != null) - onError?.Invoke(req.Result.Error); - else - Logger.Log($"Failed to create the room: {exception}", level: LogLevel.Important); - }; - - api.Queue(req); - } - - private JoinRoomRequest currentJoinRoomRequest; - - public void JoinRoom(Room room, Action onSuccess = null, Action onError = null) - { - currentJoinRoomRequest?.Cancel(); - currentJoinRoomRequest = new JoinRoomRequest(room); - - currentJoinRoomRequest.Success += () => - { - joinedRoom = room; - onSuccess?.Invoke(room); - }; - - currentJoinRoomRequest.Failure += exception => - { - if (!(exception is OperationCanceledException)) - Logger.Log($"Failed to join room: {exception}", level: LogLevel.Important); - onError?.Invoke(exception.ToString()); - }; - - api.Queue(currentJoinRoomRequest); - } - - public void PartRoom() - { - currentJoinRoomRequest?.Cancel(); - - if (joinedRoom == null) - return; - - api.Queue(new PartRoomRequest(joinedRoom)); - joinedRoom = null; - } - - private readonly HashSet ignoredRooms = new HashSet(); - - /// - /// Invoked when the listing of all s is received from the server. - /// - /// The listing. - private void onListingReceived(List listing) - { - // Remove past matches - foreach (var r in rooms.ToList()) - { - if (listing.All(e => e.RoomID.Value != r.RoomID.Value)) - rooms.Remove(r); - } - - for (int i = 0; i < listing.Count; i++) - { - if (selectedRoom.Value?.RoomID?.Value == listing[i].RoomID.Value) - { - // The listing request contains less data than the selection request, so data from the selection request is always preferred while the room is selected. - continue; - } - - var room = listing[i]; - - Debug.Assert(room.RoomID.Value != null); - - if (ignoredRooms.Contains(room.RoomID.Value.Value)) - continue; - - room.Position.Value = i; - - try - { - update(room, room); - addRoom(room); - } - catch (Exception ex) - { - Logger.Error(ex, $"Failed to update room: {room.Name.Value}."); - - ignoredRooms.Add(room.RoomID.Value.Value); - rooms.Remove(room); - } - } - - RoomsUpdated?.Invoke(); - } - - /// - /// Invoked when a is received from the server. - /// - /// The received . - private void onSelectedRoomReceived(Room toUpdate) - { - foreach (var room in rooms) - { - if (room.RoomID.Value == toUpdate.RoomID.Value) - { - toUpdate.Position.Value = room.Position.Value; - update(room, toUpdate); - break; - } - } - } - - /// - /// Updates a local with a remote copy. - /// - /// The local to update. - /// The remote to update with. - private void update(Room local, Room remote) - { - foreach (var pi in remote.Playlist) - pi.MapObjects(beatmaps, rulesets); - - local.CopyFrom(remote); - } - - /// - /// Adds a to the list of available rooms. - /// - /// The to add. - private void addRoom(Room room) - { - var existing = rooms.FirstOrDefault(e => e.RoomID.Value == room.RoomID.Value); - if (existing == null) - rooms.Add(room); - else - existing.CopyFrom(room); - } - - private class SelectionPollingComponent : PollingComponent - { - public Action RoomReceived; - - [Resolved] - private IAPIProvider api { get; set; } - - [Resolved] - private Bindable selectedRoom { get; set; } - - [BackgroundDependencyLoader] - private void load() - { - selectedRoom.BindValueChanged(_ => - { - if (IsLoaded) - PollImmediately(); - }); - } - - private GetRoomRequest pollReq; - - protected override Task Poll() - { - if (!api.IsLoggedIn) - return base.Poll(); - - if (selectedRoom.Value?.RoomID.Value == null) - return base.Poll(); - - var tcs = new TaskCompletionSource(); - - pollReq?.Cancel(); - pollReq = new GetRoomRequest(selectedRoom.Value.RoomID.Value.Value); - - pollReq.Success += result => - { - RoomReceived?.Invoke(result); - tcs.SetResult(true); - }; - - pollReq.Failure += _ => tcs.SetResult(false); - - api.Queue(pollReq); - - return tcs.Task; - } - } - - private class ListingPollingComponent : PollingComponent - { - public Action> RoomsReceived; - - public readonly Bindable InitialRoomsReceived = new Bindable(); - - [Resolved] - private IAPIProvider api { get; set; } - - [Resolved] - private Bindable currentFilter { get; set; } - - [BackgroundDependencyLoader] - private void load() - { - currentFilter.BindValueChanged(_ => - { - InitialRoomsReceived.Value = false; - - if (IsLoaded) - PollImmediately(); - }); - } - - private GetRoomsRequest pollReq; - - protected override Task Poll() - { - if (!api.IsLoggedIn) - return base.Poll(); - - var tcs = new TaskCompletionSource(); - - pollReq?.Cancel(); - pollReq = new GetRoomsRequest(currentFilter.Value.Status, currentFilter.Value.Category); - - pollReq.Success += result => - { - InitialRoomsReceived.Value = true; - RoomsReceived?.Invoke(result); - tcs.SetResult(true); - }; - - pollReq.Failure += _ => tcs.SetResult(false); - - api.Queue(pollReq); - - return tcs.Task; - } - } - } -} diff --git a/osu.Game/Screens/Multi/Components/BeatmapDetailAreaPlaylistTabItem.cs b/osu.Game/Screens/OnlinePlay/Components/BeatmapDetailAreaPlaylistTabItem.cs similarity index 86% rename from osu.Game/Screens/Multi/Components/BeatmapDetailAreaPlaylistTabItem.cs rename to osu.Game/Screens/OnlinePlay/Components/BeatmapDetailAreaPlaylistTabItem.cs index 3f2ab28f1a..fb927411e6 100644 --- a/osu.Game/Screens/Multi/Components/BeatmapDetailAreaPlaylistTabItem.cs +++ b/osu.Game/Screens/OnlinePlay/Components/BeatmapDetailAreaPlaylistTabItem.cs @@ -3,7 +3,7 @@ using osu.Game.Screens.Select; -namespace osu.Game.Screens.Multi.Components +namespace osu.Game.Screens.OnlinePlay.Components { public class BeatmapDetailAreaPlaylistTabItem : BeatmapDetailAreaTabItem { diff --git a/osu.Game/Screens/Multi/Components/BeatmapTitle.cs b/osu.Game/Screens/OnlinePlay/Components/BeatmapTitle.cs similarity index 96% rename from osu.Game/Screens/Multi/Components/BeatmapTitle.cs rename to osu.Game/Screens/OnlinePlay/Components/BeatmapTitle.cs index 9e7a59d7d2..acb82360b3 100644 --- a/osu.Game/Screens/Multi/Components/BeatmapTitle.cs +++ b/osu.Game/Screens/OnlinePlay/Components/BeatmapTitle.cs @@ -10,9 +10,9 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Online.Chat; -namespace osu.Game.Screens.Multi.Components +namespace osu.Game.Screens.OnlinePlay.Components { - public class BeatmapTitle : MultiplayerComposite + public class BeatmapTitle : OnlinePlayComposite { private readonly LinkFlowContainer textFlow; diff --git a/osu.Game/Screens/Multi/Components/BeatmapTypeInfo.cs b/osu.Game/Screens/OnlinePlay/Components/BeatmapTypeInfo.cs similarity index 95% rename from osu.Game/Screens/Multi/Components/BeatmapTypeInfo.cs rename to osu.Game/Screens/OnlinePlay/Components/BeatmapTypeInfo.cs index ce3b612262..3aa13458a4 100644 --- a/osu.Game/Screens/Multi/Components/BeatmapTypeInfo.cs +++ b/osu.Game/Screens/OnlinePlay/Components/BeatmapTypeInfo.cs @@ -9,9 +9,9 @@ using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osuTK; -namespace osu.Game.Screens.Multi.Components +namespace osu.Game.Screens.OnlinePlay.Components { - public class BeatmapTypeInfo : MultiplayerComposite + public class BeatmapTypeInfo : OnlinePlayComposite { private LinkFlowContainer beatmapAuthor; diff --git a/osu.Game/Screens/Multi/Components/DisableableTabControl.cs b/osu.Game/Screens/OnlinePlay/Components/DisableableTabControl.cs similarity index 95% rename from osu.Game/Screens/Multi/Components/DisableableTabControl.cs rename to osu.Game/Screens/OnlinePlay/Components/DisableableTabControl.cs index 27b5aec4d3..bbc407e926 100644 --- a/osu.Game/Screens/Multi/Components/DisableableTabControl.cs +++ b/osu.Game/Screens/OnlinePlay/Components/DisableableTabControl.cs @@ -5,7 +5,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; -namespace osu.Game.Screens.Multi.Components +namespace osu.Game.Screens.OnlinePlay.Components { public abstract class DisableableTabControl : TabControl { diff --git a/osu.Game/Screens/Multi/Components/DrawableGameType.cs b/osu.Game/Screens/OnlinePlay/Components/DrawableGameType.cs similarity index 93% rename from osu.Game/Screens/Multi/Components/DrawableGameType.cs rename to osu.Game/Screens/OnlinePlay/Components/DrawableGameType.cs index 28240f0796..c4dc2a2b8f 100644 --- a/osu.Game/Screens/Multi/Components/DrawableGameType.cs +++ b/osu.Game/Screens/OnlinePlay/Components/DrawableGameType.cs @@ -8,9 +8,9 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; -using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; -namespace osu.Game.Screens.Multi.Components +namespace osu.Game.Screens.OnlinePlay.Components { public class DrawableGameType : CircularContainer, IHasTooltip { diff --git a/osu.Game/Screens/OnlinePlay/Components/ListingPollingComponent.cs b/osu.Game/Screens/OnlinePlay/Components/ListingPollingComponent.cs new file mode 100644 index 0000000000..e50784fcbe --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Components/ListingPollingComponent.cs @@ -0,0 +1,69 @@ +// 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.Tasks; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Game.Online.Rooms; +using osu.Game.Screens.OnlinePlay.Lounge.Components; + +namespace osu.Game.Screens.OnlinePlay.Components +{ + /// + /// A that polls for the lounge listing. + /// + public class ListingPollingComponent : RoomPollingComponent + { + [Resolved] + private Bindable currentFilter { get; set; } + + [Resolved] + private Bindable selectedRoom { get; set; } + + [BackgroundDependencyLoader] + private void load() + { + currentFilter.BindValueChanged(_ => + { + NotifyRoomsReceived(null); + if (IsLoaded) + PollImmediately(); + }); + } + + private GetRoomsRequest pollReq; + + protected override Task Poll() + { + if (!API.IsLoggedIn) + return base.Poll(); + + var tcs = new TaskCompletionSource(); + + pollReq?.Cancel(); + pollReq = new GetRoomsRequest(currentFilter.Value.Status, currentFilter.Value.Category); + + pollReq.Success += result => + { + for (int i = 0; i < result.Count; i++) + { + if (result[i].RoomID.Value == selectedRoom.Value?.RoomID.Value) + { + // The listing request always has less information than the opened room, so don't include it. + result[i] = selectedRoom.Value; + break; + } + } + + NotifyRoomsReceived(result); + tcs.SetResult(true); + }; + + pollReq.Failure += _ => tcs.SetResult(false); + + API.Queue(pollReq); + + return tcs.Task; + } + } +} diff --git a/osu.Game/Screens/Multi/Components/MatchBeatmapDetailArea.cs b/osu.Game/Screens/OnlinePlay/Components/MatchBeatmapDetailArea.cs similarity index 97% rename from osu.Game/Screens/Multi/Components/MatchBeatmapDetailArea.cs rename to osu.Game/Screens/OnlinePlay/Components/MatchBeatmapDetailArea.cs index 2c5fd2d397..b013cbafd8 100644 --- a/osu.Game/Screens/Multi/Components/MatchBeatmapDetailArea.cs +++ b/osu.Game/Screens/OnlinePlay/Components/MatchBeatmapDetailArea.cs @@ -8,11 +8,11 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics.UserInterface; -using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; using osu.Game.Screens.Select; using osuTK; -namespace osu.Game.Screens.Multi.Components +namespace osu.Game.Screens.OnlinePlay.Components { public class MatchBeatmapDetailArea : BeatmapDetailArea { diff --git a/osu.Game/Screens/Multi/Components/ModeTypeInfo.cs b/osu.Game/Screens/OnlinePlay/Components/ModeTypeInfo.cs similarity index 95% rename from osu.Game/Screens/Multi/Components/ModeTypeInfo.cs rename to osu.Game/Screens/OnlinePlay/Components/ModeTypeInfo.cs index f07bd8c3b2..03b27b605c 100644 --- a/osu.Game/Screens/Multi/Components/ModeTypeInfo.cs +++ b/osu.Game/Screens/OnlinePlay/Components/ModeTypeInfo.cs @@ -8,9 +8,9 @@ using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps.Drawables; using osuTK; -namespace osu.Game.Screens.Multi.Components +namespace osu.Game.Screens.OnlinePlay.Components { - public class ModeTypeInfo : MultiplayerComposite + public class ModeTypeInfo : OnlinePlayComposite { private const float height = 30; private const float transition_duration = 100; diff --git a/osu.Game/Screens/Multi/Components/MultiplayerBackgroundSprite.cs b/osu.Game/Screens/OnlinePlay/Components/OnlinePlayBackgroundSprite.cs similarity index 82% rename from osu.Game/Screens/Multi/Components/MultiplayerBackgroundSprite.cs rename to osu.Game/Screens/OnlinePlay/Components/OnlinePlayBackgroundSprite.cs index 2240e55e2f..d8dfac496d 100644 --- a/osu.Game/Screens/Multi/Components/MultiplayerBackgroundSprite.cs +++ b/osu.Game/Screens/OnlinePlay/Components/OnlinePlayBackgroundSprite.cs @@ -6,14 +6,14 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Beatmaps.Drawables; -namespace osu.Game.Screens.Multi.Components +namespace osu.Game.Screens.OnlinePlay.Components { - public class MultiplayerBackgroundSprite : MultiplayerComposite + public class OnlinePlayBackgroundSprite : OnlinePlayComposite { private readonly BeatmapSetCoverType beatmapSetCoverType; private UpdateableBeatmapBackgroundSprite sprite; - public MultiplayerBackgroundSprite(BeatmapSetCoverType beatmapSetCoverType = BeatmapSetCoverType.Cover) + public OnlinePlayBackgroundSprite(BeatmapSetCoverType beatmapSetCoverType = BeatmapSetCoverType.Cover) { this.beatmapSetCoverType = beatmapSetCoverType; } diff --git a/osu.Game/Screens/Multi/Components/OverlinedHeader.cs b/osu.Game/Screens/OnlinePlay/Components/OverlinedHeader.cs similarity index 96% rename from osu.Game/Screens/Multi/Components/OverlinedHeader.cs rename to osu.Game/Screens/OnlinePlay/Components/OverlinedHeader.cs index 7ec20c8cae..08a0a3405e 100644 --- a/osu.Game/Screens/Multi/Components/OverlinedHeader.cs +++ b/osu.Game/Screens/OnlinePlay/Components/OverlinedHeader.cs @@ -10,12 +10,12 @@ using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osuTK; -namespace osu.Game.Screens.Multi.Components +namespace osu.Game.Screens.OnlinePlay.Components { /// /// A header used in the multiplayer interface which shows text / details beneath a line. /// - public class OverlinedHeader : MultiplayerComposite + public class OverlinedHeader : OnlinePlayComposite { private bool showLine = true; diff --git a/osu.Game/Screens/Multi/Components/OverlinedPlaylistHeader.cs b/osu.Game/Screens/OnlinePlay/Components/OverlinedPlaylistHeader.cs similarity index 86% rename from osu.Game/Screens/Multi/Components/OverlinedPlaylistHeader.cs rename to osu.Game/Screens/OnlinePlay/Components/OverlinedPlaylistHeader.cs index 5552c1cb72..45b822d20a 100644 --- a/osu.Game/Screens/Multi/Components/OverlinedPlaylistHeader.cs +++ b/osu.Game/Screens/OnlinePlay/Components/OverlinedPlaylistHeader.cs @@ -1,9 +1,9 @@ // 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.Online.Multiplayer; +using osu.Game.Online.Rooms; -namespace osu.Game.Screens.Multi.Components +namespace osu.Game.Screens.OnlinePlay.Components { public class OverlinedPlaylistHeader : OverlinedHeader { diff --git a/osu.Game/Screens/Multi/Components/ParticipantCountDisplay.cs b/osu.Game/Screens/OnlinePlay/Components/ParticipantCountDisplay.cs similarity index 95% rename from osu.Game/Screens/Multi/Components/ParticipantCountDisplay.cs rename to osu.Game/Screens/OnlinePlay/Components/ParticipantCountDisplay.cs index 498eeb09b3..53821da8fd 100644 --- a/osu.Game/Screens/Multi/Components/ParticipantCountDisplay.cs +++ b/osu.Game/Screens/OnlinePlay/Components/ParticipantCountDisplay.cs @@ -7,9 +7,9 @@ using osu.Framework.Graphics.Containers; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; -namespace osu.Game.Screens.Multi.Components +namespace osu.Game.Screens.OnlinePlay.Components { - public class ParticipantCountDisplay : MultiplayerComposite + public class ParticipantCountDisplay : OnlinePlayComposite { private const float text_size = 30; private const float transition_duration = 100; diff --git a/osu.Game/Screens/Multi/Components/ParticipantsDisplay.cs b/osu.Game/Screens/OnlinePlay/Components/ParticipantsDisplay.cs similarity index 94% rename from osu.Game/Screens/Multi/Components/ParticipantsDisplay.cs rename to osu.Game/Screens/OnlinePlay/Components/ParticipantsDisplay.cs index 6ea4283379..c36d1a2e76 100644 --- a/osu.Game/Screens/Multi/Components/ParticipantsDisplay.cs +++ b/osu.Game/Screens/OnlinePlay/Components/ParticipantsDisplay.cs @@ -6,9 +6,9 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Graphics.Containers; -namespace osu.Game.Screens.Multi.Components +namespace osu.Game.Screens.OnlinePlay.Components { - public class ParticipantsDisplay : MultiplayerComposite + public class ParticipantsDisplay : OnlinePlayComposite { public Bindable Details = new Bindable(); diff --git a/osu.Game/Screens/Multi/Components/ParticipantsList.cs b/osu.Game/Screens/OnlinePlay/Components/ParticipantsList.cs similarity index 97% rename from osu.Game/Screens/Multi/Components/ParticipantsList.cs rename to osu.Game/Screens/OnlinePlay/Components/ParticipantsList.cs index 7978b4eaab..9aceb39a27 100644 --- a/osu.Game/Screens/Multi/Components/ParticipantsList.cs +++ b/osu.Game/Screens/OnlinePlay/Components/ParticipantsList.cs @@ -12,9 +12,9 @@ using osu.Game.Users; using osu.Game.Users.Drawables; using osuTK; -namespace osu.Game.Screens.Multi.Components +namespace osu.Game.Screens.OnlinePlay.Components { - public class ParticipantsList : MultiplayerComposite + public class ParticipantsList : OnlinePlayComposite { public const float TILE_SIZE = 35; diff --git a/osu.Game/Screens/Multi/Match/Components/ReadyButton.cs b/osu.Game/Screens/OnlinePlay/Components/ReadyButton.cs similarity index 76% rename from osu.Game/Screens/Multi/Match/Components/ReadyButton.cs rename to osu.Game/Screens/OnlinePlay/Components/ReadyButton.cs index a64f24dd7e..08f89d8ed8 100644 --- a/osu.Game/Screens/Multi/Match/Components/ReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Components/ReadyButton.cs @@ -9,30 +9,24 @@ using osu.Framework.Bindables; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; -using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; -namespace osu.Game.Screens.Multi.Match.Components +namespace osu.Game.Screens.OnlinePlay.Components { - public class ReadyButton : TriangleButton + public abstract class ReadyButton : TriangleButton { public readonly Bindable SelectedItem = new Bindable(); - [Resolved(typeof(Room), nameof(Room.EndDate))] - private Bindable endDate { get; set; } + public new readonly BindableBool Enabled = new BindableBool(); [Resolved] - private IBindable gameBeatmap { get; set; } + protected IBindable GameBeatmap { get; private set; } [Resolved] private BeatmapManager beatmaps { get; set; } private bool hasBeatmap; - public ReadyButton() - { - Text = "Start"; - } - private IBindable> managerUpdated; private IBindable> managerRemoved; @@ -45,10 +39,6 @@ namespace osu.Game.Screens.Multi.Match.Components managerRemoved.BindValueChanged(beatmapRemoved); SelectedItem.BindValueChanged(item => updateSelectedItem(item.NewValue), true); - - BackgroundColour = colours.Green; - Triangles.ColourDark = colours.Green; - Triangles.ColourLight = colours.GreenLight; } private void updateSelectedItem(PlaylistItem item) @@ -94,15 +84,13 @@ namespace osu.Game.Screens.Multi.Match.Components private void updateEnabledState() { - if (gameBeatmap.Value == null || SelectedItem.Value == null) + if (GameBeatmap.Value == null || SelectedItem.Value == null) { - Enabled.Value = false; + base.Enabled.Value = false; return; } - bool hasEnoughTime = DateTimeOffset.UtcNow.AddSeconds(30).AddMilliseconds(gameBeatmap.Value.Track.Length) < endDate.Value; - - Enabled.Value = hasBeatmap && hasEnoughTime; + base.Enabled.Value = hasBeatmap && Enabled.Value; } } } diff --git a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs b/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs new file mode 100644 index 0000000000..2ed259e2b8 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs @@ -0,0 +1,201 @@ +// 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.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Logging; +using osu.Game.Beatmaps; +using osu.Game.Online.API; +using osu.Game.Online.Rooms; +using osu.Game.Rulesets; + +namespace osu.Game.Screens.OnlinePlay.Components +{ + public abstract class RoomManager : CompositeDrawable, IRoomManager + { + public event Action RoomsUpdated; + + private readonly BindableList rooms = new BindableList(); + + public IBindable InitialRoomsReceived => initialRoomsReceived; + private readonly Bindable initialRoomsReceived = new Bindable(); + + public IBindableList Rooms => rooms; + + protected IBindable JoinedRoom => joinedRoom; + private readonly Bindable joinedRoom = new Bindable(); + + [Resolved] + private RulesetStore rulesets { get; set; } + + [Resolved] + private BeatmapManager beatmaps { get; set; } + + [Resolved] + private IAPIProvider api { get; set; } + + protected RoomManager() + { + RelativeSizeAxes = Axes.Both; + + InternalChildren = CreatePollingComponents().Select(p => + { + p.RoomsReceived = onRoomsReceived; + return p; + }).ToList(); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + PartRoom(); + } + + public virtual void CreateRoom(Room room, Action onSuccess = null, Action onError = null) + { + room.Host.Value = api.LocalUser.Value; + + var req = new CreateRoomRequest(room); + + req.Success += result => + { + joinedRoom.Value = room; + + update(room, result); + addRoom(room); + + RoomsUpdated?.Invoke(); + onSuccess?.Invoke(room); + }; + + req.Failure += exception => + { + onError?.Invoke(req.Result?.Error ?? exception.Message); + }; + + api.Queue(req); + } + + private JoinRoomRequest currentJoinRoomRequest; + + public virtual void JoinRoom(Room room, Action onSuccess = null, Action onError = null) + { + currentJoinRoomRequest?.Cancel(); + currentJoinRoomRequest = new JoinRoomRequest(room); + + currentJoinRoomRequest.Success += () => + { + joinedRoom.Value = room; + onSuccess?.Invoke(room); + }; + + currentJoinRoomRequest.Failure += exception => + { + if (!(exception is OperationCanceledException)) + Logger.Log($"Failed to join room: {exception}", level: LogLevel.Important); + onError?.Invoke(exception.ToString()); + }; + + api.Queue(currentJoinRoomRequest); + } + + public virtual void PartRoom() + { + currentJoinRoomRequest?.Cancel(); + + if (JoinedRoom.Value == null) + return; + + api.Queue(new PartRoomRequest(joinedRoom.Value)); + joinedRoom.Value = null; + } + + private readonly HashSet ignoredRooms = new HashSet(); + + private void onRoomsReceived(List received) + { + if (received == null) + { + ClearRooms(); + return; + } + + // Remove past matches + foreach (var r in rooms.ToList()) + { + if (received.All(e => e.RoomID.Value != r.RoomID.Value)) + rooms.Remove(r); + } + + for (int i = 0; i < received.Count; i++) + { + var room = received[i]; + + Debug.Assert(room.RoomID.Value != null); + + if (ignoredRooms.Contains(room.RoomID.Value.Value)) + continue; + + room.Position.Value = i; + + try + { + update(room, room); + addRoom(room); + } + catch (Exception ex) + { + Logger.Error(ex, $"Failed to update room: {room.Name.Value}."); + + ignoredRooms.Add(room.RoomID.Value.Value); + rooms.Remove(room); + } + } + + RoomsUpdated?.Invoke(); + initialRoomsReceived.Value = true; + } + + protected void RemoveRoom(Room room) => rooms.Remove(room); + + protected void ClearRooms() + { + rooms.Clear(); + initialRoomsReceived.Value = false; + } + + /// + /// Updates a local with a remote copy. + /// + /// The local to update. + /// The remote to update with. + private void update(Room local, Room remote) + { + foreach (var pi in remote.Playlist) + pi.MapObjects(beatmaps, rulesets); + + local.CopyFrom(remote); + } + + /// + /// Adds a to the list of available rooms. + /// + /// The to add. + private void addRoom(Room room) + { + var existing = rooms.FirstOrDefault(e => e.RoomID.Value == room.RoomID.Value); + if (existing == null) + rooms.Add(room); + else + existing.CopyFrom(room); + } + + protected abstract IEnumerable CreatePollingComponents(); + } +} diff --git a/osu.Game/Screens/OnlinePlay/Components/RoomPollingComponent.cs b/osu.Game/Screens/OnlinePlay/Components/RoomPollingComponent.cs new file mode 100644 index 0000000000..b2ea3a05d6 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Components/RoomPollingComponent.cs @@ -0,0 +1,29 @@ +// 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.Game.Online; +using osu.Game.Online.API; +using osu.Game.Online.Rooms; + +namespace osu.Game.Screens.OnlinePlay.Components +{ + public abstract class RoomPollingComponent : PollingComponent + { + /// + /// Invoked when any s have been received from the API. + /// + /// Any s present locally but not returned by this event are to be removed from display. + /// If null, the display of local rooms is reset to an initial state. + /// + /// + public Action> RoomsReceived; + + [Resolved] + protected IAPIProvider API { get; private set; } + + protected void NotifyRoomsReceived(List rooms) => RoomsReceived?.Invoke(rooms); + } +} diff --git a/osu.Game/Screens/Multi/Components/RoomStatusInfo.cs b/osu.Game/Screens/OnlinePlay/Components/RoomStatusInfo.cs similarity index 83% rename from osu.Game/Screens/Multi/Components/RoomStatusInfo.cs rename to osu.Game/Screens/OnlinePlay/Components/RoomStatusInfo.cs index d799f846c2..bcc256bcff 100644 --- a/osu.Game/Screens/Multi/Components/RoomStatusInfo.cs +++ b/osu.Game/Screens/OnlinePlay/Components/RoomStatusInfo.cs @@ -8,12 +8,12 @@ using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics; -using osu.Game.Online.Multiplayer; -using osu.Game.Online.Multiplayer.RoomStatuses; +using osu.Game.Online.Rooms; +using osu.Game.Online.Rooms.RoomStatuses; -namespace osu.Game.Screens.Multi.Components +namespace osu.Game.Screens.OnlinePlay.Components { - public class RoomStatusInfo : MultiplayerComposite + public class RoomStatusInfo : OnlinePlayComposite { public RoomStatusInfo() { @@ -48,16 +48,23 @@ namespace osu.Game.Screens.Multi.Components private class EndDatePart : DrawableDate { - public readonly IBindable EndDate = new Bindable(); + public readonly IBindable EndDate = new Bindable(); public EndDatePart() : base(DateTimeOffset.UtcNow) { - EndDate.BindValueChanged(date => Date = date.NewValue); + EndDate.BindValueChanged(date => + { + // If null, set a very large future date to prevent unnecessary schedules. + Date = date.NewValue ?? DateTimeOffset.Now.AddYears(1); + }, true); } protected override string Format() { + if (EndDate.Value == null) + return string.Empty; + var diffToNow = Date.Subtract(DateTimeOffset.Now); if (diffToNow.TotalSeconds < -5) diff --git a/osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs b/osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs new file mode 100644 index 0000000000..0eec155060 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs @@ -0,0 +1,69 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Threading.Tasks; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Game.Online.Rooms; + +namespace osu.Game.Screens.OnlinePlay.Components +{ + /// + /// A that polls for the currently-selected room. + /// + public class SelectionPollingComponent : RoomPollingComponent + { + [Resolved] + private Bindable selectedRoom { get; set; } + + [Resolved] + private IRoomManager roomManager { get; set; } + + [BackgroundDependencyLoader] + private void load() + { + selectedRoom.BindValueChanged(_ => + { + if (IsLoaded) + PollImmediately(); + }); + } + + private GetRoomRequest pollReq; + + protected override Task Poll() + { + if (!API.IsLoggedIn) + return base.Poll(); + + if (selectedRoom.Value?.RoomID.Value == null) + return base.Poll(); + + var tcs = new TaskCompletionSource(); + + pollReq?.Cancel(); + pollReq = new GetRoomRequest(selectedRoom.Value.RoomID.Value.Value); + + pollReq.Success += result => + { + var rooms = new List(roomManager.Rooms); + + int index = rooms.FindIndex(r => r.RoomID.Value == result.RoomID.Value); + if (index < 0) + return; + + rooms[index] = result; + + NotifyRoomsReceived(rooms); + tcs.SetResult(true); + }; + + pollReq.Failure += _ => tcs.SetResult(false); + + API.Queue(pollReq); + + return tcs.Task; + } + } +} diff --git a/osu.Game/Screens/Multi/Components/StatusColouredContainer.cs b/osu.Game/Screens/OnlinePlay/Components/StatusColouredContainer.cs similarity index 93% rename from osu.Game/Screens/Multi/Components/StatusColouredContainer.cs rename to osu.Game/Screens/OnlinePlay/Components/StatusColouredContainer.cs index a115f06e7b..760de354dc 100644 --- a/osu.Game/Screens/Multi/Components/StatusColouredContainer.cs +++ b/osu.Game/Screens/OnlinePlay/Components/StatusColouredContainer.cs @@ -6,9 +6,9 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics; -using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; -namespace osu.Game.Screens.Multi.Components +namespace osu.Game.Screens.OnlinePlay.Components { public class StatusColouredContainer : Container { diff --git a/osu.Game/Screens/Multi/DrawableRoomPlaylist.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs similarity index 97% rename from osu.Game/Screens/Multi/DrawableRoomPlaylist.cs rename to osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs index 89c335183b..a08d9edb34 100644 --- a/osu.Game/Screens/Multi/DrawableRoomPlaylist.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs @@ -7,10 +7,10 @@ using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics.Containers; -using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; using osuTK; -namespace osu.Game.Screens.Multi +namespace osu.Game.Screens.OnlinePlay { public class DrawableRoomPlaylist : OsuRearrangeableListContainer { diff --git a/osu.Game/Screens/Multi/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs similarity index 99% rename from osu.Game/Screens/Multi/DrawableRoomPlaylistItem.cs rename to osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs index bda00b65b5..e3bce4029f 100644 --- a/osu.Game/Screens/Multi/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs @@ -21,7 +21,7 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Online; using osu.Game.Online.Chat; -using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; using osu.Game.Overlays.BeatmapListing.Panels; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; @@ -29,7 +29,7 @@ using osu.Game.Screens.Play.HUD; using osuTK; using osuTK.Graphics; -namespace osu.Game.Screens.Multi +namespace osu.Game.Screens.OnlinePlay { public class DrawableRoomPlaylistItem : OsuRearrangeableListItem { diff --git a/osu.Game/Screens/Multi/DrawableRoomPlaylistWithResults.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistWithResults.cs similarity index 96% rename from osu.Game/Screens/Multi/DrawableRoomPlaylistWithResults.cs rename to osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistWithResults.cs index 439aaaa275..575f336e58 100644 --- a/osu.Game/Screens/Multi/DrawableRoomPlaylistWithResults.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistWithResults.cs @@ -11,9 +11,9 @@ using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; -using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; -namespace osu.Game.Screens.Multi +namespace osu.Game.Screens.OnlinePlay { public class DrawableRoomPlaylistWithResults : DrawableRoomPlaylist { diff --git a/osu.Game/Screens/Multi/Header.cs b/osu.Game/Screens/OnlinePlay/Header.cs similarity index 92% rename from osu.Game/Screens/Multi/Header.cs rename to osu.Game/Screens/OnlinePlay/Header.cs index cd8695286b..bf0a53cbb6 100644 --- a/osu.Game/Screens/Multi/Header.cs +++ b/osu.Game/Screens/OnlinePlay/Header.cs @@ -10,19 +10,19 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; using osu.Framework.Screens; using osu.Game.Graphics; -using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; using osuTK; using osuTK.Graphics; -namespace osu.Game.Screens.Multi +namespace osu.Game.Screens.OnlinePlay { public class Header : Container { public const float HEIGHT = 80; - public Header(ScreenStack stack) + public Header(string mainTitle, ScreenStack stack) { RelativeSizeAxes = Axes.X; Height = HEIGHT; @@ -45,7 +45,7 @@ namespace osu.Game.Screens.Multi Padding = new MarginPadding { Left = WaveOverlayContainer.WIDTH_PADDING + OsuScreen.HORIZONTAL_OVERFLOW_PADDING }, Children = new Drawable[] { - title = new MultiHeaderTitle + title = new MultiHeaderTitle(mainTitle) { Anchor = Anchor.CentreLeft, Origin = Anchor.BottomLeft, @@ -61,8 +61,8 @@ namespace osu.Game.Screens.Multi breadcrumbs.Current.ValueChanged += screen => { - if (screen.NewValue is IMultiplayerSubScreen multiScreen) - title.Screen = multiScreen; + if (screen.NewValue is IOnlinePlaySubScreen onlineSubScreen) + title.Screen = onlineSubScreen; }; breadcrumbs.Current.TriggerChange(); @@ -75,12 +75,12 @@ namespace osu.Game.Screens.Multi private readonly OsuSpriteText dot; private readonly OsuSpriteText pageTitle; - public IMultiplayerSubScreen Screen + public IOnlinePlaySubScreen Screen { set => pageTitle.Text = value.ShortTitle.Titleize(); } - public MultiHeaderTitle() + public MultiHeaderTitle(string mainTitle) { AutoSizeAxes = Axes.Both; @@ -98,7 +98,7 @@ namespace osu.Game.Screens.Multi Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, Font = OsuFont.GetFont(size: 24), - Text = "Multiplayer" + Text = mainTitle }, dot = new OsuSpriteText { diff --git a/osu.Game/Screens/Multi/IMultiplayerSubScreen.cs b/osu.Game/Screens/OnlinePlay/IOnlinePlaySubScreen.cs similarity index 71% rename from osu.Game/Screens/Multi/IMultiplayerSubScreen.cs rename to osu.Game/Screens/OnlinePlay/IOnlinePlaySubScreen.cs index 31ee123f83..a4762292a9 100644 --- a/osu.Game/Screens/Multi/IMultiplayerSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/IOnlinePlaySubScreen.cs @@ -1,9 +1,9 @@ // 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.Multi +namespace osu.Game.Screens.OnlinePlay { - public interface IMultiplayerSubScreen : IOsuScreen + public interface IOnlinePlaySubScreen : IOsuScreen { string Title { get; } diff --git a/osu.Game/Screens/Multi/IRoomManager.cs b/osu.Game/Screens/OnlinePlay/IRoomManager.cs similarity index 89% rename from osu.Game/Screens/Multi/IRoomManager.cs rename to osu.Game/Screens/OnlinePlay/IRoomManager.cs index bf75843c3e..8ff02536f3 100644 --- a/osu.Game/Screens/Multi/IRoomManager.cs +++ b/osu.Game/Screens/OnlinePlay/IRoomManager.cs @@ -2,11 +2,13 @@ // See the LICENCE file in the repository root for full licence text. using System; +using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; -namespace osu.Game.Screens.Multi +namespace osu.Game.Screens.OnlinePlay { + [Cached(typeof(IRoomManager))] public interface IRoomManager { /// @@ -17,7 +19,7 @@ namespace osu.Game.Screens.Multi /// /// Whether an initial listing of rooms has been received. /// - Bindable InitialRoomsReceived { get; } + IBindable InitialRoomsReceived { get; } /// /// All the active s. diff --git a/osu.Game/Screens/Multi/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs similarity index 95% rename from osu.Game/Screens/Multi/Lounge/Components/DrawableRoom.cs rename to osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs index 01a85382e4..0a7198a7fa 100644 --- a/osu.Game/Screens/Multi/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs @@ -9,22 +9,22 @@ using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.UserInterface; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; -using osu.Game.Online.Multiplayer; -using osu.Game.Screens.Multi.Components; +using osu.Game.Online.Rooms; +using osu.Game.Screens.OnlinePlay.Components; using osuTK; using osuTK.Graphics; -using osu.Framework.Graphics.Cursor; -using osu.Framework.Graphics.UserInterface; -namespace osu.Game.Screens.Multi.Lounge.Components +namespace osu.Game.Screens.OnlinePlay.Lounge.Components { public class DrawableRoom : OsuClickableContainer, IStateful, IFilterable, IHasContextMenu { @@ -42,7 +42,7 @@ namespace osu.Game.Screens.Multi.Lounge.Components private CachedModelDependencyContainer dependencies; [Resolved(canBeNull: true)] - private Multiplayer multiplayer { get; set; } + private OnlinePlayScreen parentScreen { get; set; } [Resolved] private BeatmapManager beatmaps { get; set; } @@ -155,7 +155,7 @@ namespace osu.Game.Screens.Multi.Lounge.Components Width = cover_width, Masking = true, Margin = new MarginPadding { Left = stripWidth }, - Child = new MultiplayerBackgroundSprite(BeatmapSetCoverType.List) { RelativeSizeAxes = Axes.Both } + Child = new OnlinePlayBackgroundSprite(BeatmapSetCoverType.List) { RelativeSizeAxes = Axes.Both } }, new Container { @@ -228,7 +228,7 @@ namespace osu.Game.Screens.Multi.Lounge.Components private class RoomName : OsuSpriteText { - [Resolved(typeof(Room), nameof(Online.Multiplayer.Room.Name))] + [Resolved(typeof(Room), nameof(Online.Rooms.Room.Name))] private Bindable name { get; set; } [BackgroundDependencyLoader] @@ -242,7 +242,7 @@ namespace osu.Game.Screens.Multi.Lounge.Components { new OsuMenuItem("Create copy", MenuItemType.Standard, () => { - multiplayer?.CreateRoom(Room.CreateCopy()); + parentScreen?.OpenNewRoom(Room.CreateCopy()); }) }; } diff --git a/osu.Game/Screens/Multi/Lounge/Components/FilterControl.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/FilterControl.cs similarity index 96% rename from osu.Game/Screens/Multi/Lounge/Components/FilterControl.cs rename to osu.Game/Screens/OnlinePlay/Lounge/Components/FilterControl.cs index 896c215c42..7fc1c670ca 100644 --- a/osu.Game/Screens/Multi/Lounge/Components/FilterControl.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/FilterControl.cs @@ -12,7 +12,7 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets; using osuTK.Graphics; -namespace osu.Game.Screens.Multi.Lounge.Components +namespace osu.Game.Screens.OnlinePlay.Lounge.Components { public abstract class FilterControl : CompositeDrawable { @@ -98,7 +98,9 @@ namespace osu.Game.Screens.Multi.Lounge.Components scheduledFilterUpdate = Scheduler.AddDelayed(UpdateFilter, 200); } - protected void UpdateFilter() + protected void UpdateFilter() => Scheduler.AddOnce(updateFilter); + + private void updateFilter() { scheduledFilterUpdate?.Cancel(); diff --git a/osu.Game/Screens/Multi/Lounge/Components/FilterCriteria.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/FilterCriteria.cs similarity index 86% rename from osu.Game/Screens/Multi/Lounge/Components/FilterCriteria.cs rename to osu.Game/Screens/OnlinePlay/Lounge/Components/FilterCriteria.cs index 7b04be86b1..488af5d4de 100644 --- a/osu.Game/Screens/Multi/Lounge/Components/FilterCriteria.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/FilterCriteria.cs @@ -3,7 +3,7 @@ using osu.Game.Rulesets; -namespace osu.Game.Screens.Multi.Lounge.Components +namespace osu.Game.Screens.OnlinePlay.Lounge.Components { public class FilterCriteria { diff --git a/osu.Game/Screens/Multi/Lounge/Components/ParticipantInfo.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/ParticipantInfo.cs similarity index 96% rename from osu.Game/Screens/Multi/Lounge/Components/ParticipantInfo.cs rename to osu.Game/Screens/OnlinePlay/Lounge/Components/ParticipantInfo.cs index 4152a9a3b2..0d5ce65d5a 100644 --- a/osu.Game/Screens/Multi/Lounge/Components/ParticipantInfo.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/ParticipantInfo.cs @@ -11,9 +11,9 @@ using osu.Game.Graphics.Sprites; using osu.Game.Users.Drawables; using osuTK; -namespace osu.Game.Screens.Multi.Lounge.Components +namespace osu.Game.Screens.OnlinePlay.Lounge.Components { - public class ParticipantInfo : MultiplayerComposite + public class ParticipantInfo : OnlinePlayComposite { public ParticipantInfo() { diff --git a/osu.Game/Screens/Multi/Lounge/Components/TimeshiftFilterControl.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/PlaylistsFilterControl.cs similarity index 73% rename from osu.Game/Screens/Multi/Lounge/Components/TimeshiftFilterControl.cs rename to osu.Game/Screens/OnlinePlay/Lounge/Components/PlaylistsFilterControl.cs index 68cab283a0..a463742097 100644 --- a/osu.Game/Screens/Multi/Lounge/Components/TimeshiftFilterControl.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/PlaylistsFilterControl.cs @@ -5,15 +5,15 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics.UserInterface; -namespace osu.Game.Screens.Multi.Lounge.Components +namespace osu.Game.Screens.OnlinePlay.Lounge.Components { - public class TimeshiftFilterControl : FilterControl + public class PlaylistsFilterControl : FilterControl { - private readonly Dropdown dropdown; + private readonly Dropdown dropdown; - public TimeshiftFilterControl() + public PlaylistsFilterControl() { - AddInternal(dropdown = new SlimEnumDropdown + AddInternal(dropdown = new SlimEnumDropdown { Anchor = Anchor.BottomRight, Origin = Anchor.TopRight, @@ -37,11 +37,11 @@ namespace osu.Game.Screens.Multi.Lounge.Components switch (dropdown.Current.Value) { - case TimeshiftCategory.Normal: + case PlaylistsCategory.Normal: criteria.Category = "normal"; break; - case TimeshiftCategory.Spotlight: + case PlaylistsCategory.Spotlight: criteria.Category = "spotlight"; break; } @@ -49,7 +49,7 @@ namespace osu.Game.Screens.Multi.Lounge.Components return criteria; } - private enum TimeshiftCategory + private enum PlaylistsCategory { Any, Normal, diff --git a/osu.Game/Screens/Multi/Lounge/Components/RoomInfo.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomInfo.cs similarity index 95% rename from osu.Game/Screens/Multi/Lounge/Components/RoomInfo.cs rename to osu.Game/Screens/OnlinePlay/Lounge/Components/RoomInfo.cs index e6f6ce5ed2..0a17702f2a 100644 --- a/osu.Game/Screens/Multi/Lounge/Components/RoomInfo.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomInfo.cs @@ -6,12 +6,12 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics; using osu.Game.Graphics.Containers; -using osu.Game.Screens.Multi.Components; +using osu.Game.Screens.OnlinePlay.Components; using osuTK; -namespace osu.Game.Screens.Multi.Lounge.Components +namespace osu.Game.Screens.OnlinePlay.Lounge.Components { - public class RoomInfo : MultiplayerComposite + public class RoomInfo : OnlinePlayComposite { private readonly List statusElements = new List(); private readonly OsuTextFlowContainer roomName; diff --git a/osu.Game/Screens/Multi/Lounge/Components/RoomInspector.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomInspector.cs similarity index 95% rename from osu.Game/Screens/Multi/Lounge/Components/RoomInspector.cs rename to osu.Game/Screens/OnlinePlay/Lounge/Components/RoomInspector.cs index dfee278e87..c28354c753 100644 --- a/osu.Game/Screens/Multi/Lounge/Components/RoomInspector.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomInspector.cs @@ -7,12 +7,12 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Beatmaps; using osu.Game.Graphics; -using osu.Game.Screens.Multi.Components; +using osu.Game.Screens.OnlinePlay.Components; using osuTK.Graphics; -namespace osu.Game.Screens.Multi.Lounge.Components +namespace osu.Game.Screens.OnlinePlay.Lounge.Components { - public class RoomInspector : MultiplayerComposite + public class RoomInspector : OnlinePlayComposite { private const float transition_duration = 100; diff --git a/osu.Game/Screens/Multi/Lounge/Components/RoomStatusFilter.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomStatusFilter.cs similarity index 85% rename from osu.Game/Screens/Multi/Lounge/Components/RoomStatusFilter.cs rename to osu.Game/Screens/OnlinePlay/Lounge/Components/RoomStatusFilter.cs index 9da938ac8b..0c8dc8832b 100644 --- a/osu.Game/Screens/Multi/Lounge/Components/RoomStatusFilter.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomStatusFilter.cs @@ -3,7 +3,7 @@ using System.ComponentModel; -namespace osu.Game.Screens.Multi.Lounge.Components +namespace osu.Game.Screens.OnlinePlay.Lounge.Components { public enum RoomStatusFilter { diff --git a/osu.Game/Screens/Multi/Lounge/Components/RoomsContainer.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs similarity index 98% rename from osu.Game/Screens/Multi/Lounge/Components/RoomsContainer.cs rename to osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs index c7c37cbc0d..f70c33babe 100644 --- a/osu.Game/Screens/Multi/Lounge/Components/RoomsContainer.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs @@ -13,13 +13,13 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Input.Bindings; using osu.Framework.Threading; using osu.Game.Extensions; +using osu.Game.Graphics.Cursor; using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; -using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; using osuTK; -using osu.Game.Graphics.Cursor; -namespace osu.Game.Screens.Multi.Lounge.Components +namespace osu.Game.Screens.OnlinePlay.Lounge.Components { public class RoomsContainer : CompositeDrawable, IKeyBindingHandler { diff --git a/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs similarity index 85% rename from osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs rename to osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs index a26a64d86d..79f5dfdee1 100644 --- a/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs @@ -10,25 +10,24 @@ using osu.Framework.Input.Events; using osu.Framework.Screens; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; -using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; using osu.Game.Overlays; -using osu.Game.Screens.Multi.Lounge.Components; -using osu.Game.Screens.Multi.Match; +using osu.Game.Screens.OnlinePlay.Lounge.Components; +using osu.Game.Screens.OnlinePlay.Match; using osu.Game.Users; -namespace osu.Game.Screens.Multi.Lounge +namespace osu.Game.Screens.OnlinePlay.Lounge { [Cached] - public class LoungeSubScreen : MultiplayerSubScreen + public abstract class LoungeSubScreen : OnlinePlaySubScreen { public override string Title => "Lounge"; - protected FilterControl Filter; - protected override UserActivity InitialActivity => new UserActivity.SearchingForLobby(); - private readonly Bindable initialRoomsReceived = new Bindable(); + private readonly IBindable initialRoomsReceived = new Bindable(); + private FilterControl filter; private Container content; private LoadingLayer loadingLayer; @@ -78,11 +77,11 @@ namespace osu.Game.Screens.Multi.Lounge }, }, }, - Filter = new TimeshiftFilterControl + filter = CreateFilterControl().With(d => { - RelativeSizeAxes = Axes.X, - Height = 80, - }, + d.RelativeSizeAxes = Axes.X; + d.Height = 80; + }) }; // scroll selected room into view on selection. @@ -108,7 +107,7 @@ namespace osu.Game.Screens.Multi.Lounge content.Padding = new MarginPadding { - Top = Filter.DrawHeight, + Top = filter.DrawHeight, Left = WaveOverlayContainer.WIDTH_PADDING - DrawableRoom.SELECTION_BORDER_WIDTH + HORIZONTAL_OVERFLOW_PADDING, Right = WaveOverlayContainer.WIDTH_PADDING + HORIZONTAL_OVERFLOW_PADDING, }; @@ -116,7 +115,7 @@ namespace osu.Game.Screens.Multi.Lounge protected override void OnFocus(FocusEvent e) { - Filter.TakeFocus(); + filter.TakeFocus(); } public override void OnEntering(IScreen last) @@ -140,19 +139,19 @@ namespace osu.Game.Screens.Multi.Lounge private void onReturning() { - Filter.HoldFocus = true; + filter.HoldFocus = true; } public override bool OnExiting(IScreen next) { - Filter.HoldFocus = false; + filter.HoldFocus = false; return base.OnExiting(next); } public override void OnSuspending(IScreen next) { base.OnSuspending(next); - Filter.HoldFocus = false; + filter.HoldFocus = false; } private void joinRequested(Room room) @@ -185,7 +184,7 @@ namespace osu.Game.Screens.Multi.Lounge /// /// Push a room as a new subscreen. /// - public void Open(Room room) + public virtual void Open(Room room) { // Handles the case where a room is clicked 3 times in quick succession if (!this.IsCurrentScreen()) @@ -193,7 +192,11 @@ namespace osu.Game.Screens.Multi.Lounge selectedRoom.Value = room; - this.Push(new MatchSubScreen(room)); + this.Push(CreateRoomSubScreen(room)); } + + protected abstract FilterControl CreateFilterControl(); + + protected abstract RoomSubScreen CreateRoomSubScreen(Room room); } } diff --git a/osu.Game/Screens/Multi/Match/Components/Footer.cs b/osu.Game/Screens/OnlinePlay/Match/Components/Footer.cs similarity index 89% rename from osu.Game/Screens/Multi/Match/Components/Footer.cs rename to osu.Game/Screens/OnlinePlay/Match/Components/Footer.cs index be4ee873fa..5c27d78d50 100644 --- a/osu.Game/Screens/Multi/Match/Components/Footer.cs +++ b/osu.Game/Screens/OnlinePlay/Match/Components/Footer.cs @@ -9,10 +9,11 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; -using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; +using osu.Game.Screens.OnlinePlay.Playlists; using osuTK; -namespace osu.Game.Screens.Multi.Match.Components +namespace osu.Game.Screens.OnlinePlay.Match.Components { public class Footer : CompositeDrawable { @@ -31,7 +32,7 @@ namespace osu.Game.Screens.Multi.Match.Components InternalChildren = new[] { background = new Box { RelativeSizeAxes = Axes.Both }, - new ReadyButton + new PlaylistsReadyButton { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game/Screens/Multi/Match/Components/GameTypePicker.cs b/osu.Game/Screens/OnlinePlay/Match/Components/GameTypePicker.cs similarity index 94% rename from osu.Game/Screens/Multi/Match/Components/GameTypePicker.cs rename to osu.Game/Screens/OnlinePlay/Match/Components/GameTypePicker.cs index b69cb9705d..cca1f84bbb 100644 --- a/osu.Game/Screens/Multi/Match/Components/GameTypePicker.cs +++ b/osu.Game/Screens/OnlinePlay/Match/Components/GameTypePicker.cs @@ -8,12 +8,12 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Game.Graphics; -using osu.Game.Online.Multiplayer; -using osu.Game.Online.Multiplayer.GameTypes; -using osu.Game.Screens.Multi.Components; +using osu.Game.Online.Rooms; +using osu.Game.Online.Rooms.GameTypes; +using osu.Game.Screens.OnlinePlay.Components; using osuTK; -namespace osu.Game.Screens.Multi.Match.Components +namespace osu.Game.Screens.OnlinePlay.Match.Components { public class GameTypePicker : DisableableTabControl { @@ -33,7 +33,7 @@ namespace osu.Game.Screens.Multi.Match.Components AddItem(new GameTypeVersus()); AddItem(new GameTypeTagTeam()); AddItem(new GameTypeTeamVersus()); - AddItem(new GameTypeTimeshift()); + AddItem(new GameTypePlaylists()); } private class GameTypePickerItem : DisableableTabItem diff --git a/osu.Game/Screens/Multi/Match/Components/Header.cs b/osu.Game/Screens/OnlinePlay/Match/Components/Header.cs similarity index 96% rename from osu.Game/Screens/Multi/Match/Components/Header.cs rename to osu.Game/Screens/OnlinePlay/Match/Components/Header.cs index 134a0b3f2e..a2d11c54c1 100644 --- a/osu.Game/Screens/Multi/Match/Components/Header.cs +++ b/osu.Game/Screens/OnlinePlay/Match/Components/Header.cs @@ -10,9 +10,9 @@ using osu.Game.Graphics.Sprites; using osu.Game.Users.Drawables; using osuTK; -namespace osu.Game.Screens.Multi.Match.Components +namespace osu.Game.Screens.OnlinePlay.Match.Components { - public class Header : MultiplayerComposite + public class Header : OnlinePlayComposite { public const float HEIGHT = 50; diff --git a/osu.Game/Screens/Multi/Match/Components/MatchChatDisplay.cs b/osu.Game/Screens/OnlinePlay/Match/Components/MatchChatDisplay.cs similarity index 93% rename from osu.Game/Screens/Multi/Match/Components/MatchChatDisplay.cs rename to osu.Game/Screens/OnlinePlay/Match/Components/MatchChatDisplay.cs index f8b64a54ef..8800215c2e 100644 --- a/osu.Game/Screens/Multi/Match/Components/MatchChatDisplay.cs +++ b/osu.Game/Screens/OnlinePlay/Match/Components/MatchChatDisplay.cs @@ -4,9 +4,9 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Online.Chat; -using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; -namespace osu.Game.Screens.Multi.Match.Components +namespace osu.Game.Screens.OnlinePlay.Match.Components { public class MatchChatDisplay : StandAloneChatDisplay { diff --git a/osu.Game/Screens/Multi/Match/Components/MatchLeaderboard.cs b/osu.Game/Screens/OnlinePlay/Match/Components/MatchLeaderboard.cs similarity index 95% rename from osu.Game/Screens/Multi/Match/Components/MatchLeaderboard.cs rename to osu.Game/Screens/OnlinePlay/Match/Components/MatchLeaderboard.cs index f2409d64e7..50869f42ff 100644 --- a/osu.Game/Screens/Multi/Match/Components/MatchLeaderboard.cs +++ b/osu.Game/Screens/OnlinePlay/Match/Components/MatchLeaderboard.cs @@ -8,9 +8,9 @@ using osu.Framework.Bindables; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Leaderboards; -using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; -namespace osu.Game.Screens.Multi.Match.Components +namespace osu.Game.Screens.OnlinePlay.Match.Components { public class MatchLeaderboard : Leaderboard { diff --git a/osu.Game/Screens/Multi/Match/Components/MatchLeaderboardScore.cs b/osu.Game/Screens/OnlinePlay/Match/Components/MatchLeaderboardScore.cs similarity index 95% rename from osu.Game/Screens/Multi/Match/Components/MatchLeaderboardScore.cs rename to osu.Game/Screens/OnlinePlay/Match/Components/MatchLeaderboardScore.cs index 1fabdbb86a..e8f5b1e826 100644 --- a/osu.Game/Screens/Multi/Match/Components/MatchLeaderboardScore.cs +++ b/osu.Game/Screens/OnlinePlay/Match/Components/MatchLeaderboardScore.cs @@ -8,7 +8,7 @@ using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Leaderboards; using osu.Game.Scoring; -namespace osu.Game.Screens.Multi.Match.Components +namespace osu.Game.Screens.OnlinePlay.Match.Components { public class MatchLeaderboardScore : LeaderboardScore { diff --git a/osu.Game/Screens/OnlinePlay/Match/Components/MatchSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Match/Components/MatchSettingsOverlay.cs new file mode 100644 index 0000000000..ea3951fc3b --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Match/Components/MatchSettingsOverlay.cs @@ -0,0 +1,109 @@ +// 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.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.OnlinePlay.Match.Components +{ + public abstract class MatchSettingsOverlay : FocusedOverlayContainer + { + protected const float TRANSITION_DURATION = 350; + protected const float FIELD_PADDING = 45; + + protected OnlinePlayComposite Settings { get; set; } + + [BackgroundDependencyLoader] + private void load() + { + Masking = true; + } + + protected override void PopIn() + { + Settings.MoveToY(0, TRANSITION_DURATION, Easing.OutQuint); + } + + protected override void PopOut() + { + Settings.MoveToY(-1, TRANSITION_DURATION, Easing.InSine); + } + + protected class SettingsTextBox : OsuTextBox + { + [BackgroundDependencyLoader] + private void load() + { + BackgroundUnfocused = Color4.Black; + BackgroundFocused = Color4.Black; + } + } + + protected class SettingsNumberTextBox : SettingsTextBox + { + protected override bool CanAddCharacter(char character) => char.IsNumber(character); + } + + protected class SettingsPasswordTextBox : OsuPasswordTextBox + { + [BackgroundDependencyLoader] + private void load() + { + BackgroundUnfocused = Color4.Black; + BackgroundFocused = Color4.Black; + } + } + + protected class SectionContainer : FillFlowContainer
+ { + public SectionContainer() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + Width = 0.5f; + Direction = FillDirection.Vertical; + Spacing = new Vector2(FIELD_PADDING); + } + } + + protected class Section : Container + { + private readonly Container content; + + protected override Container Content => content; + + public Section(string title) + { + AutoSizeAxes = Axes.Y; + RelativeSizeAxes = Axes.X; + + InternalChild = new FillFlowContainer + { + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Direction = FillDirection.Vertical, + Spacing = new Vector2(5), + Children = new Drawable[] + { + new OsuSpriteText + { + Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 12), + Text = title.ToUpper(), + }, + content = new Container + { + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + }, + }, + }; + } + } + } +} diff --git a/osu.Game/Screens/Multi/Match/Components/PurpleTriangleButton.cs b/osu.Game/Screens/OnlinePlay/Match/Components/PurpleTriangleButton.cs similarity index 92% rename from osu.Game/Screens/Multi/Match/Components/PurpleTriangleButton.cs rename to osu.Game/Screens/OnlinePlay/Match/Components/PurpleTriangleButton.cs index 1d93116d07..28e8961a9a 100644 --- a/osu.Game/Screens/Multi/Match/Components/PurpleTriangleButton.cs +++ b/osu.Game/Screens/OnlinePlay/Match/Components/PurpleTriangleButton.cs @@ -5,7 +5,7 @@ using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Game.Graphics.UserInterface; -namespace osu.Game.Screens.Multi.Match.Components +namespace osu.Game.Screens.OnlinePlay.Match.Components { public class PurpleTriangleButton : TriangleButton { diff --git a/osu.Game/Screens/Multi/Match/Components/RoomAvailabilityPicker.cs b/osu.Game/Screens/OnlinePlay/Match/Components/RoomAvailabilityPicker.cs similarity index 96% rename from osu.Game/Screens/Multi/Match/Components/RoomAvailabilityPicker.cs rename to osu.Game/Screens/OnlinePlay/Match/Components/RoomAvailabilityPicker.cs index 7ef39c2a74..677a5be0d9 100644 --- a/osu.Game/Screens/Multi/Match/Components/RoomAvailabilityPicker.cs +++ b/osu.Game/Screens/OnlinePlay/Match/Components/RoomAvailabilityPicker.cs @@ -10,12 +10,12 @@ using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; -using osu.Game.Online.Multiplayer; -using osu.Game.Screens.Multi.Components; +using osu.Game.Online.Rooms; +using osu.Game.Screens.OnlinePlay.Components; using osuTK; using osuTK.Graphics; -namespace osu.Game.Screens.Multi.Match.Components +namespace osu.Game.Screens.OnlinePlay.Match.Components { public class RoomAvailabilityPicker : DisableableTabControl { diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs new file mode 100644 index 0000000000..2449563c73 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -0,0 +1,156 @@ +// 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.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; +using osu.Framework.Bindables; +using osu.Framework.Screens; +using osu.Game.Audio; +using osu.Game.Beatmaps; +using osu.Game.Online.Rooms; +using osu.Game.Overlays; +using osu.Game.Rulesets.Mods; +using osu.Game.Screens.Play; + +namespace osu.Game.Screens.OnlinePlay.Match +{ + [Cached(typeof(IPreviewTrackOwner))] + public abstract class RoomSubScreen : OnlinePlaySubScreen, IPreviewTrackOwner + { + protected readonly Bindable SelectedItem = new Bindable(); + + public override bool DisallowExternalBeatmapRulesetChanges => true; + + private SampleChannel sampleStart; + + [Resolved(typeof(Room), nameof(Room.Playlist))] + protected BindableList Playlist { get; private set; } + + [Resolved] + private MusicController music { get; set; } + + [Resolved] + private BeatmapManager beatmapManager { get; set; } + + [Resolved(canBeNull: true)] + protected OnlinePlayScreen ParentScreen { get; private set; } + + private IBindable> managerUpdated; + + [BackgroundDependencyLoader] + private void load(AudioManager audio) + { + sampleStart = audio.Samples.Get(@"SongSelect/confirm-selection"); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + SelectedItem.BindValueChanged(_ => Scheduler.AddOnce(selectedItemChanged)); + SelectedItem.Value = Playlist.FirstOrDefault(); + + managerUpdated = beatmapManager.ItemUpdated.GetBoundCopy(); + managerUpdated.BindValueChanged(beatmapUpdated); + } + + public override void OnEntering(IScreen last) + { + base.OnEntering(last); + beginHandlingTrack(); + } + + public override void OnSuspending(IScreen next) + { + endHandlingTrack(); + base.OnSuspending(next); + } + + public override void OnResuming(IScreen last) + { + base.OnResuming(last); + beginHandlingTrack(); + } + + public override bool OnExiting(IScreen next) + { + RoomManager?.PartRoom(); + Mods.Value = Array.Empty(); + + endHandlingTrack(); + + return base.OnExiting(next); + } + + protected void StartPlay(Func player) + { + sampleStart?.Play(); + ParentScreen?.Push(new PlayerLoader(player)); + } + + private void selectedItemChanged() + { + updateWorkingBeatmap(); + + var item = SelectedItem.Value; + + Mods.Value = item?.RequiredMods?.ToArray() ?? Array.Empty(); + + if (item?.Ruleset != null) + Ruleset.Value = item.Ruleset.Value; + } + + private void beatmapUpdated(ValueChangedEvent> weakSet) => Schedule(updateWorkingBeatmap); + + private void updateWorkingBeatmap() + { + var beatmap = SelectedItem.Value?.Beatmap.Value; + + // Retrieve the corresponding local beatmap, since we can't directly use the playlist's beatmap info + var localBeatmap = beatmap == null ? null : beatmapManager.QueryBeatmap(b => b.OnlineBeatmapID == beatmap.OnlineBeatmapID); + + Beatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap); + } + + private void beginHandlingTrack() + { + Beatmap.BindValueChanged(applyLoopingToTrack, true); + } + + private void endHandlingTrack() + { + Beatmap.ValueChanged -= applyLoopingToTrack; + cancelTrackLooping(); + } + + private void applyLoopingToTrack(ValueChangedEvent _ = null) + { + if (!this.IsCurrentScreen()) + return; + + var track = Beatmap.Value?.Track; + + if (track != null) + { + track.RestartPoint = Beatmap.Value.Metadata.PreviewTime; + track.Looping = true; + + music?.EnsurePlayingSomething(); + } + } + + private void cancelTrackLooping() + { + var track = Beatmap?.Value?.Track; + + if (track != null) + { + track.Looping = false; + track.RestartPoint = 0; + } + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/CreateMultiplayerMatchButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/CreateMultiplayerMatchButton.cs new file mode 100644 index 0000000000..163efd9c20 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/CreateMultiplayerMatchButton.cs @@ -0,0 +1,23 @@ +// 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.Game.Online.Multiplayer; +using osu.Game.Screens.OnlinePlay.Match.Components; + +namespace osu.Game.Screens.OnlinePlay.Multiplayer +{ + public class CreateMultiplayerMatchButton : PurpleTriangleButton + { + [BackgroundDependencyLoader] + private void load(StatefulMultiplayerClient multiplayerClient) + { + Triangles.TriangleScale = 1.5f; + + Text = "Create room"; + + ((IBindable)Enabled).BindTo(multiplayerClient.IsConnected); + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/BeatmapSelectionControl.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/BeatmapSelectionControl.cs new file mode 100644 index 0000000000..f17e04d4d4 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/BeatmapSelectionControl.cs @@ -0,0 +1,81 @@ +// 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.UserInterface; +using osu.Framework.Screens; +using osu.Game.Online.API; +using osu.Game.Screens.OnlinePlay.Match.Components; + +namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match +{ + public class BeatmapSelectionControl : OnlinePlayComposite + { + [Resolved] + private MultiplayerMatchSubScreen matchSubScreen { get; set; } + + [Resolved] + private IAPIProvider api { get; set; } + + private Container beatmapPanelContainer; + private Button selectButton; + + public BeatmapSelectionControl() + { + AutoSizeAxes = Axes.Y; + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChild = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + beatmapPanelContainer = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y + }, + selectButton = new PurpleTriangleButton + { + RelativeSizeAxes = Axes.X, + Height = 40, + Text = "Select beatmap", + Action = () => matchSubScreen.Push(new MultiplayerMatchSongSelect()), + Alpha = 0 + } + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Playlist.BindCollectionChanged(onPlaylistChanged, true); + Host.BindValueChanged(host => + { + if (RoomID.Value == null || host.NewValue?.Equals(api.LocalUser.Value) == true) + selectButton.Show(); + else + selectButton.Hide(); + }, true); + } + + private void onPlaylistChanged(object sender, NotifyCollectionChangedEventArgs e) + { + if (Playlist.Any()) + beatmapPanelContainer.Child = new DrawableRoomPlaylistItem(Playlist.Single(), false, false); + else + beatmapPanelContainer.Clear(); + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchFooter.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchFooter.cs new file mode 100644 index 0000000000..a52f62fe00 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchFooter.cs @@ -0,0 +1,48 @@ +// 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.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; +using osu.Game.Online.Rooms; +using osuTK; + +namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match +{ + public class MultiplayerMatchFooter : CompositeDrawable + { + public const float HEIGHT = 50; + + public readonly Bindable SelectedItem = new Bindable(); + + private readonly Drawable background; + + public MultiplayerMatchFooter() + { + RelativeSizeAxes = Axes.X; + Height = HEIGHT; + + InternalChildren = new[] + { + background = new Box { RelativeSizeAxes = Axes.Both }, + new MultiplayerReadyButton + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(600, 50), + SelectedItem = { BindTarget = SelectedItem } + } + }; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + background.Colour = Color4Extensions.FromHex(@"28242d"); + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchHeader.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchHeader.cs new file mode 100644 index 0000000000..bb351d06d3 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchHeader.cs @@ -0,0 +1,106 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.UserInterface; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Online.API; +using osu.Game.Screens.OnlinePlay.Match.Components; +using osu.Game.Users.Drawables; +using osuTK; +using FontWeight = osu.Game.Graphics.FontWeight; +using OsuColour = osu.Game.Graphics.OsuColour; +using OsuFont = osu.Game.Graphics.OsuFont; + +namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match +{ + public class MultiplayerMatchHeader : OnlinePlayComposite + { + public const float HEIGHT = 50; + + public Action OpenSettings; + + private UpdateableAvatar avatar; + private LinkFlowContainer hostText; + private Button openSettingsButton; + + [Resolved] + private IAPIProvider api { get; set; } + + public MultiplayerMatchHeader() + { + RelativeSizeAxes = Axes.X; + Height = HEIGHT; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + InternalChildren = new Drawable[] + { + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(10, 0), + Children = new Drawable[] + { + avatar = new UpdateableAvatar + { + Size = new Vector2(50), + Masking = true, + CornerRadius = 10, + }, + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new OsuSpriteText + { + Font = OsuFont.GetFont(size: 30), + Current = { BindTarget = RoomName } + }, + hostText = new LinkFlowContainer(s => s.Font = OsuFont.GetFont(size: 20)) + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + } + } + } + } + }, + openSettingsButton = new PurpleTriangleButton + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Size = new Vector2(150, HEIGHT), + Text = "Open settings", + Action = () => OpenSettings?.Invoke(), + Alpha = 0 + } + }; + + Host.BindValueChanged(host => + { + avatar.User = host.NewValue; + + hostText.Clear(); + + if (host.NewValue != null) + { + hostText.AddText("hosted by "); + hostText.AddUserLink(host.NewValue, s => s.Font = s.Font.With(weight: FontWeight.SemiBold)); + } + + openSettingsButton.Alpha = host.NewValue?.Equals(api.LocalUser.Value) == true ? 1 : 0; + }, true); + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs new file mode 100644 index 0000000000..ae03d384f6 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs @@ -0,0 +1,358 @@ +// 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.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Extensions.ExceptionExtensions; +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.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; +using osu.Game.Overlays; +using osu.Game.Rulesets; +using osu.Game.Screens.OnlinePlay.Match.Components; +using osuTK; + +namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match +{ + public class MultiplayerMatchSettingsOverlay : MatchSettingsOverlay + { + [BackgroundDependencyLoader] + private void load() + { + Child = Settings = new MatchSettings + { + RelativeSizeAxes = Axes.Both, + RelativePositionAxes = Axes.Y, + SettingsApplied = Hide + }; + } + + protected class MatchSettings : OnlinePlayComposite + { + private const float disabled_alpha = 0.2f; + + public Action SettingsApplied; + + public OsuTextBox NameField, MaxParticipantsField; + public RoomAvailabilityPicker AvailabilityPicker; + public GameTypePicker TypePicker; + public TriangleButton ApplyButton; + + public OsuSpriteText ErrorText; + + private OsuSpriteText typeLabel; + private LoadingLayer loadingLayer; + private BeatmapSelectionControl initialBeatmapControl; + + [Resolved] + private IRoomManager manager { get; set; } + + [Resolved] + private StatefulMultiplayerClient client { get; set; } + + [Resolved] + private Bindable currentRoom { get; set; } + + [Resolved] + private Bindable beatmap { get; set; } + + [Resolved] + private Bindable ruleset { get; set; } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + Container dimContent; + + InternalChildren = new Drawable[] + { + dimContent = new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex(@"28242d"), + }, + new GridContainer + { + RelativeSizeAxes = Axes.Both, + RowDimensions = new[] + { + new Dimension(GridSizeMode.Distributed), + new Dimension(GridSizeMode.AutoSize), + }, + Content = new[] + { + new Drawable[] + { + new OsuScrollContainer + { + Padding = new MarginPadding + { + Horizontal = OsuScreen.HORIZONTAL_OVERFLOW_PADDING, + Vertical = 10 + }, + RelativeSizeAxes = Axes.Both, + Children = new[] + { + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 10), + Children = new Drawable[] + { + new Container + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Padding = new MarginPadding { Horizontal = WaveOverlayContainer.WIDTH_PADDING }, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + new SectionContainer + { + Padding = new MarginPadding { Right = FIELD_PADDING / 2 }, + Children = new[] + { + new Section("Room name") + { + Child = NameField = new SettingsTextBox + { + RelativeSizeAxes = Axes.X, + TabbableContentContainer = this, + }, + }, + new Section("Room visibility") + { + Alpha = disabled_alpha, + Child = AvailabilityPicker = new RoomAvailabilityPicker + { + Enabled = { Value = false } + }, + }, + new Section("Game type") + { + Alpha = disabled_alpha, + Child = new FillFlowContainer + { + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Direction = FillDirection.Vertical, + Spacing = new Vector2(7), + Children = new Drawable[] + { + TypePicker = new GameTypePicker + { + RelativeSizeAxes = Axes.X, + Enabled = { Value = false } + }, + typeLabel = new OsuSpriteText + { + Font = OsuFont.GetFont(size: 14), + Colour = colours.Yellow + }, + }, + }, + }, + }, + }, + new SectionContainer + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Padding = new MarginPadding { Left = FIELD_PADDING / 2 }, + Children = new[] + { + new Section("Max participants") + { + Alpha = disabled_alpha, + Child = MaxParticipantsField = new SettingsNumberTextBox + { + RelativeSizeAxes = Axes.X, + TabbableContentContainer = this, + ReadOnly = true, + }, + }, + new Section("Password (optional)") + { + Alpha = disabled_alpha, + Child = new SettingsPasswordTextBox + { + RelativeSizeAxes = Axes.X, + TabbableContentContainer = this, + ReadOnly = true, + }, + }, + } + } + }, + }, + initialBeatmapControl = new BeatmapSelectionControl + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.X, + Width = 0.5f + } + } + } + }, + }, + }, + new Drawable[] + { + new Container + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Y = 2, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex(@"28242d").Darken(0.5f).Opacity(1f), + }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 20), + Margin = new MarginPadding { Vertical = 20 }, + Padding = new MarginPadding { Horizontal = OsuScreen.HORIZONTAL_OVERFLOW_PADDING }, + Children = new Drawable[] + { + ApplyButton = new CreateOrUpdateButton + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Size = new Vector2(230, 55), + Enabled = { Value = false }, + Action = apply, + }, + ErrorText = new OsuSpriteText + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Alpha = 0, + Depth = 1, + Colour = colours.RedDark + } + } + } + } + } + } + } + }, + } + }, + loadingLayer = new LoadingLayer(dimContent) + }; + + TypePicker.Current.BindValueChanged(type => typeLabel.Text = type.NewValue?.Name ?? string.Empty, true); + RoomName.BindValueChanged(name => NameField.Text = name.NewValue, true); + Availability.BindValueChanged(availability => AvailabilityPicker.Current.Value = availability.NewValue, true); + Type.BindValueChanged(type => TypePicker.Current.Value = type.NewValue, true); + MaxParticipants.BindValueChanged(count => MaxParticipantsField.Text = count.NewValue?.ToString(), true); + RoomID.BindValueChanged(roomId => initialBeatmapControl.Alpha = roomId.NewValue == null ? 1 : 0, true); + } + + protected override void Update() + { + base.Update(); + + ApplyButton.Enabled.Value = Playlist.Count > 0 && NameField.Text.Length > 0; + } + + private void apply() + { + if (!ApplyButton.Enabled.Value) + return; + + hideError(); + loadingLayer.Show(); + + // If the client is already in a room, update via the client. + // Otherwise, update the room directly in preparation for it to be submitted to the API on match creation. + if (client.Room != null) + { + client.ChangeSettings(name: NameField.Text).ContinueWith(t => Schedule(() => + { + if (t.IsCompletedSuccessfully) + onSuccess(currentRoom.Value); + else + onError(t.Exception?.AsSingular().Message ?? "Error changing settings."); + })); + } + else + { + currentRoom.Value.Name.Value = NameField.Text; + currentRoom.Value.Availability.Value = AvailabilityPicker.Current.Value; + currentRoom.Value.Type.Value = TypePicker.Current.Value; + + if (int.TryParse(MaxParticipantsField.Text, out int max)) + currentRoom.Value.MaxParticipants.Value = max; + else + currentRoom.Value.MaxParticipants.Value = null; + + manager?.CreateRoom(currentRoom.Value, onSuccess, onError); + } + } + + private void hideError() => ErrorText.FadeOut(50); + + private void onSuccess(Room room) + { + loadingLayer.Hide(); + SettingsApplied?.Invoke(); + } + + private void onError(string text) + { + ErrorText.Text = text; + ErrorText.FadeIn(50); + + loadingLayer.Hide(); + } + } + + public class CreateOrUpdateButton : TriangleButton + { + [Resolved(typeof(Room), nameof(Room.RoomID))] + private Bindable roomId { get; set; } + + protected override void LoadComplete() + { + base.LoadComplete(); + roomId.BindValueChanged(id => Text = id.NewValue == null ? "Create" : "Update", true); + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + BackgroundColour = colours.Yellow; + Triangles.ColourLight = colours.YellowLight; + Triangles.ColourDark = colours.YellowDark; + } + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs new file mode 100644 index 0000000000..281e92404c --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs @@ -0,0 +1,156 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Diagnostics; +using System.Linq; +using JetBrains.Annotations; +using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Game.Extensions; +using osu.Game.Graphics; +using osu.Game.Graphics.Backgrounds; +using osu.Game.Online.API; +using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; +using osu.Game.Screens.OnlinePlay.Components; +using osuTK; + +namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match +{ + public class MultiplayerReadyButton : MultiplayerRoomComposite + { + public Bindable SelectedItem => button.SelectedItem; + + [Resolved] + private IAPIProvider api { get; set; } + + [CanBeNull] + private MultiplayerRoomUser localUser; + + [Resolved] + private OsuColour colours { get; set; } + + private SampleChannel sampleReadyCount; + + private readonly ButtonWithTrianglesExposed button; + + private int countReady; + + public MultiplayerReadyButton() + { + InternalChild = button = new ButtonWithTrianglesExposed + { + RelativeSizeAxes = Axes.Both, + Size = Vector2.One, + Enabled = { Value = true }, + Action = onClick + }; + } + + [BackgroundDependencyLoader] + private void load(AudioManager audio) + { + sampleReadyCount = audio.Samples.Get(@"SongSelect/select-difficulty"); + } + + protected override void OnRoomUpdated() + { + base.OnRoomUpdated(); + + // this method is called on leaving the room, so the local user may not exist in the room any more. + localUser = Room?.Users.SingleOrDefault(u => u.User?.Id == api.LocalUser.Value.Id); + + button.Enabled.Value = Client.Room?.State == MultiplayerRoomState.Open; + updateState(); + } + + private void updateState() + { + if (localUser == null) + return; + + Debug.Assert(Room != null); + + int newCountReady = Room.Users.Count(u => u.State == MultiplayerUserState.Ready); + + string countText = $"({newCountReady} / {Room.Users.Count} ready)"; + + switch (localUser.State) + { + case MultiplayerUserState.Idle: + button.Text = "Ready"; + updateButtonColour(true); + break; + + case MultiplayerUserState.Ready: + if (Room?.Host?.Equals(localUser) == true) + { + button.Text = $"Start match {countText}"; + updateButtonColour(true); + } + else + { + button.Text = $"Waiting for host... {countText}"; + updateButtonColour(false); + } + + break; + } + + if (newCountReady != countReady) + { + countReady = newCountReady; + Scheduler.AddOnce(playSound); + } + } + + private void playSound() + { + if (sampleReadyCount == null) + return; + + sampleReadyCount.Frequency.Value = 0.77f + countReady * 0.06f; + sampleReadyCount.Play(); + } + + private void updateButtonColour(bool green) + { + if (green) + { + button.BackgroundColour = colours.Green; + button.Triangles.ColourDark = colours.Green; + button.Triangles.ColourLight = colours.GreenLight; + } + else + { + button.BackgroundColour = colours.YellowDark; + button.Triangles.ColourDark = colours.YellowDark; + button.Triangles.ColourLight = colours.Yellow; + } + } + + private void onClick() + { + if (localUser == null) + return; + + if (localUser.State == MultiplayerUserState.Idle) + Client.ChangeState(MultiplayerUserState.Ready).CatchUnobservedExceptions(true); + else + { + if (Room?.Host?.Equals(localUser) == true) + Client.StartMatch().CatchUnobservedExceptions(true); + else + Client.ChangeState(MultiplayerUserState.Idle).CatchUnobservedExceptions(true); + } + } + + private class ButtonWithTrianglesExposed : ReadyButton + { + public new Triangles Triangles => base.Triangles; + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs new file mode 100644 index 0000000000..76f5c74433 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs @@ -0,0 +1,73 @@ +// 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.Logging; +using osu.Framework.Screens; +using osu.Game.Extensions; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; +using osu.Game.Screens.OnlinePlay.Components; +using osu.Game.Screens.OnlinePlay.Lounge; + +namespace osu.Game.Screens.OnlinePlay.Multiplayer +{ + public class Multiplayer : OnlinePlayScreen + { + [Resolved] + private StatefulMultiplayerClient client { get; set; } + + public override void OnResuming(IScreen last) + { + base.OnResuming(last); + + if (client.Room != null) + client.ChangeState(MultiplayerUserState.Idle).CatchUnobservedExceptions(true); + } + + protected override void UpdatePollingRate(bool isIdle) + { + var multiplayerRoomManager = (MultiplayerRoomManager)RoomManager; + + if (!this.IsCurrentScreen()) + { + multiplayerRoomManager.TimeBetweenListingPolls.Value = 0; + multiplayerRoomManager.TimeBetweenSelectionPolls.Value = 0; + } + else + { + switch (CurrentSubScreen) + { + case LoungeSubScreen _: + multiplayerRoomManager.TimeBetweenListingPolls.Value = isIdle ? 120000 : 15000; + multiplayerRoomManager.TimeBetweenSelectionPolls.Value = isIdle ? 120000 : 15000; + break; + + // Don't poll inside the match or anywhere else. + default: + multiplayerRoomManager.TimeBetweenListingPolls.Value = 0; + multiplayerRoomManager.TimeBetweenSelectionPolls.Value = 0; + break; + } + } + + Logger.Log($"Polling adjusted (listing: {multiplayerRoomManager.TimeBetweenListingPolls.Value}, selection: {multiplayerRoomManager.TimeBetweenSelectionPolls.Value})"); + } + + protected override Room CreateNewRoom() + { + var room = new Room { Name = { Value = $"{API.LocalUser}'s awesome room" } }; + room.Category.Value = RoomCategory.Realtime; + return room; + } + + protected override string ScreenTitle => "Multiplayer"; + + protected override RoomManager CreateRoomManager() => new MultiplayerRoomManager(); + + protected override LoungeSubScreen CreateLounge() => new MultiplayerLoungeSubScreen(); + + protected override OsuButton CreateNewMultiplayerGameButton() => new CreateMultiplayerMatchButton(); + } +} diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerFilterControl.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerFilterControl.cs new file mode 100644 index 0000000000..37e0fd109a --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerFilterControl.cs @@ -0,0 +1,17 @@ +// 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.Screens.OnlinePlay.Lounge.Components; + +namespace osu.Game.Screens.OnlinePlay.Multiplayer +{ + public class MultiplayerFilterControl : FilterControl + { + protected override FilterCriteria CreateCriteria() + { + var criteria = base.CreateCriteria(); + criteria.Category = "realtime"; + return criteria; + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs new file mode 100644 index 0000000000..0a9a3f680f --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs @@ -0,0 +1,34 @@ +// 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.Logging; +using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; +using osu.Game.Screens.OnlinePlay.Lounge; +using osu.Game.Screens.OnlinePlay.Lounge.Components; +using osu.Game.Screens.OnlinePlay.Match; + +namespace osu.Game.Screens.OnlinePlay.Multiplayer +{ + public class MultiplayerLoungeSubScreen : LoungeSubScreen + { + protected override FilterControl CreateFilterControl() => new MultiplayerFilterControl(); + + protected override RoomSubScreen CreateRoomSubScreen(Room room) => new MultiplayerMatchSubScreen(room); + + [Resolved] + private StatefulMultiplayerClient client { get; set; } + + public override void Open(Room room) + { + if (!client.IsConnected.Value) + { + Logger.Log("Not currently connected to the multiplayer server.", LoggingTarget.Runtime, LogLevel.Important); + return; + } + + base.Open(room); + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs new file mode 100644 index 0000000000..0842574f54 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs @@ -0,0 +1,87 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using Humanizer; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Logging; +using osu.Framework.Screens; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; +using osu.Game.Screens.Select; + +namespace osu.Game.Screens.OnlinePlay.Multiplayer +{ + public class MultiplayerMatchSongSelect : SongSelect, IOnlinePlaySubScreen + { + public string ShortTitle => "song selection"; + + public override string Title => ShortTitle.Humanize(); + + [Resolved(typeof(Room), nameof(Room.Playlist))] + private BindableList playlist { get; set; } + + [Resolved] + private StatefulMultiplayerClient client { get; set; } + + private LoadingLayer loadingLayer; + + public MultiplayerMatchSongSelect() + { + Padding = new MarginPadding { Horizontal = HORIZONTAL_OVERFLOW_PADDING }; + } + + [BackgroundDependencyLoader] + private void load() + { + AddInternal(loadingLayer = new LoadingLayer(Carousel)); + } + + protected override bool OnStart() + { + var item = new PlaylistItem(); + + item.Beatmap.Value = Beatmap.Value.BeatmapInfo; + item.Ruleset.Value = Ruleset.Value; + + item.RequiredMods.Clear(); + item.RequiredMods.AddRange(Mods.Value.Select(m => m.CreateCopy())); + + // If the client is already in a room, update via the client. + // Otherwise, update the playlist directly in preparation for it to be submitted to the API on match creation. + if (client.Room != null) + { + loadingLayer.Show(); + + client.ChangeSettings(item: item).ContinueWith(t => + { + Schedule(() => + { + loadingLayer.Hide(); + + if (t.IsCompletedSuccessfully) + this.Exit(); + else + { + Logger.Log($"Could not use current beatmap ({t.Exception?.Message})", level: LogLevel.Important); + Carousel.AllowSelection = true; + } + }); + }); + } + else + { + playlist.Clear(); + playlist.Add(item); + this.Exit(); + } + + return true; + } + + protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea(); + } +} diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs new file mode 100644 index 0000000000..58314c3774 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -0,0 +1,216 @@ +// 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.Diagnostics; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Screens; +using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; +using osu.Game.Screens.OnlinePlay.Components; +using osu.Game.Screens.OnlinePlay.Match; +using osu.Game.Screens.OnlinePlay.Match.Components; +using osu.Game.Screens.OnlinePlay.Multiplayer.Match; +using osu.Game.Screens.OnlinePlay.Multiplayer.Participants; +using osu.Game.Users; +using ParticipantsList = osu.Game.Screens.OnlinePlay.Multiplayer.Participants.ParticipantsList; + +namespace osu.Game.Screens.OnlinePlay.Multiplayer +{ + [Cached] + public class MultiplayerMatchSubScreen : RoomSubScreen + { + public override string Title { get; } + + public override string ShortTitle => "room"; + + [Resolved] + private StatefulMultiplayerClient client { get; set; } + + private MultiplayerMatchSettingsOverlay settingsOverlay; + + private IBindable isConnected; + + public MultiplayerMatchSubScreen(Room room) + { + Title = room.RoomID.Value == null ? "New room" : room.Name.Value; + Activity.Value = new UserActivity.InLobby(room); + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChildren = new Drawable[] + { + new GridContainer + { + RelativeSizeAxes = Axes.Both, + Content = new[] + { + new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding + { + Horizontal = 105, + Vertical = 20 + }, + Child = new GridContainer + { + RelativeSizeAxes = Axes.Both, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension(), + }, + Content = new[] + { + new Drawable[] + { + new MultiplayerMatchHeader + { + OpenSettings = () => settingsOverlay.Show() + } + }, + new Drawable[] + { + new GridContainer + { + RelativeSizeAxes = Axes.Both, + Content = new[] + { + new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Horizontal = 5, Vertical = 10 }, + Child = new GridContainer + { + RelativeSizeAxes = Axes.Both, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize) + }, + Content = new[] + { + new Drawable[] { new ParticipantsListHeader() }, + new Drawable[] + { + new ParticipantsList + { + RelativeSizeAxes = Axes.Both + }, + } + } + } + }, + new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Horizontal = 5 }, + Children = new Drawable[] + { + new OverlinedHeader("Beatmap"), + new BeatmapSelectionControl { RelativeSizeAxes = Axes.X } + } + } + } + } + } + }, + new Drawable[] + { + new GridContainer + { + RelativeSizeAxes = Axes.Both, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize) + }, + Content = new[] + { + new Drawable[] { new OverlinedHeader("Chat") }, + new Drawable[] { new MatchChatDisplay { RelativeSizeAxes = Axes.Both } } + } + } + } + }, + } + } + }, + new Drawable[] + { + new MultiplayerMatchFooter { SelectedItem = { BindTarget = SelectedItem } } + } + }, + RowDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.AutoSize), + } + }, + settingsOverlay = new MultiplayerMatchSettingsOverlay + { + RelativeSizeAxes = Axes.Both, + State = { Value = client.Room == null ? Visibility.Visible : Visibility.Hidden } + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Playlist.BindCollectionChanged(onPlaylistChanged, true); + + client.LoadRequested += onLoadRequested; + + isConnected = client.IsConnected.GetBoundCopy(); + isConnected.BindValueChanged(connected => + { + if (!connected.NewValue) + Schedule(this.Exit); + }, true); + } + + public override bool OnBackButton() + { + if (client.Room != null && settingsOverlay.State.Value == Visibility.Visible) + { + settingsOverlay.Hide(); + return true; + } + + return base.OnBackButton(); + } + + private void onPlaylistChanged(object sender, NotifyCollectionChangedEventArgs e) => SelectedItem.Value = Playlist.FirstOrDefault(); + + private void onLoadRequested() + { + Debug.Assert(client.Room != null); + + int[] userIds = client.Room.Users.Where(u => u.State >= MultiplayerUserState.WaitingForLoad).Select(u => u.UserID).ToArray(); + + StartPlay(() => new MultiplayerPlayer(SelectedItem.Value, userIds)); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (client != null) + client.LoadRequested -= onLoadRequested; + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs new file mode 100644 index 0000000000..4247e954bd --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs @@ -0,0 +1,159 @@ +// 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.Diagnostics; +using System.Threading.Tasks; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Logging; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; +using osu.Game.Scoring; +using osu.Game.Screens.OnlinePlay.Playlists; +using osu.Game.Screens.Play; +using osu.Game.Screens.Play.HUD; +using osu.Game.Screens.Ranking; +using osuTK; + +namespace osu.Game.Screens.OnlinePlay.Multiplayer +{ + // Todo: The "room" part of PlaylistsPlayer should be split out into an abstract player class to be inherited instead. + public class MultiplayerPlayer : PlaylistsPlayer + { + protected override bool PauseOnFocusLost => false; + + // Disallow fails in multiplayer for now. + protected override bool CheckModsAllowFailure() => false; + + [Resolved] + private StatefulMultiplayerClient client { get; set; } + + private IBindable isConnected; + + private readonly TaskCompletionSource resultsReady = new TaskCompletionSource(); + + private MultiplayerGameplayLeaderboard leaderboard; + + private readonly int[] userIds; + + private LoadingLayer loadingDisplay; + + /// + /// Construct a multiplayer player. + /// + /// The playlist item to be played. + /// The users which are participating in this game. + public MultiplayerPlayer(PlaylistItem playlistItem, int[] userIds) + : base(playlistItem, new PlayerConfiguration + { + AllowPause = false, + AllowRestart = false, + AllowSkippingIntro = false, + }) + { + this.userIds = userIds; + } + + [BackgroundDependencyLoader] + private void load() + { + // todo: this should be implemented via a custom HUD implementation, and correctly masked to the main content area. + LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(ScoreProcessor, userIds), HUDOverlay.Add); + + HUDOverlay.Add(loadingDisplay = new LoadingLayer(DrawableRuleset) { Depth = float.MaxValue }); + + if (Token == null) + return; // Todo: Somehow handle token retrieval failure. + + client.MatchStarted += onMatchStarted; + client.ResultsReady += onResultsReady; + + ScoreProcessor.HasCompleted.BindValueChanged(completed => + { + // wait for server to tell us that results are ready (see SubmitScore implementation) + loadingDisplay.Show(); + }); + + isConnected = client.IsConnected.GetBoundCopy(); + isConnected.BindValueChanged(connected => + { + if (!connected.NewValue) + { + // messaging to the user about this disconnect will be provided by the MultiplayerMatchSubScreen. + failAndBail(); + } + }, true); + + Debug.Assert(client.Room != null); + } + + protected override void StartGameplay() + { + // block base call, but let the server know we are ready to start. + loadingDisplay.Show(); + + client.ChangeState(MultiplayerUserState.Loaded).ContinueWith(task => failAndBail(task.Exception?.Message ?? "Server error"), TaskContinuationOptions.NotOnRanToCompletion); + } + + private void failAndBail(string message = null) + { + if (!string.IsNullOrEmpty(message)) + Logger.Log(message, LoggingTarget.Runtime, LogLevel.Important); + + Schedule(() => PerformExit(false)); + } + + protected override void Update() + { + base.Update(); + adjustLeaderboardPosition(); + } + + private void adjustLeaderboardPosition() + { + const float padding = 44; // enough margin to avoid the hit error display. + + leaderboard.Position = new Vector2( + padding, + padding + HUDOverlay.TopScoringElementsHeight); + } + + private void onMatchStarted() => Scheduler.Add(() => + { + loadingDisplay.Hide(); + base.StartGameplay(); + }); + + private void onResultsReady() => resultsReady.SetResult(true); + + protected override async Task SubmitScore(Score score) + { + await base.SubmitScore(score); + + await client.ChangeState(MultiplayerUserState.FinishedPlay); + + // Await up to 60 seconds for results to become available (6 api request timeouts). + // This is arbitrary just to not leave the player in an essentially deadlocked state if any connection issues occur. + await Task.WhenAny(resultsReady.Task, Task.Delay(TimeSpan.FromSeconds(60))); + } + + protected override ResultsScreen CreateResults(ScoreInfo score) + { + Debug.Assert(RoomId.Value != null); + return new MultiplayerResultsScreen(score, RoomId.Value.Value, PlaylistItem); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (client != null) + { + client.MatchStarted -= onMatchStarted; + client.ResultsReady -= onResultsReady; + } + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerResultsScreen.cs new file mode 100644 index 0000000000..e3b47b3254 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerResultsScreen.cs @@ -0,0 +1,17 @@ +// 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.Online.Rooms; +using osu.Game.Scoring; +using osu.Game.Screens.OnlinePlay.Playlists; + +namespace osu.Game.Screens.OnlinePlay.Multiplayer +{ + public class MultiplayerResultsScreen : PlaylistsResultsScreen + { + public MultiplayerResultsScreen(ScoreInfo score, int roomId, PlaylistItem playlistItem) + : base(score, roomId, playlistItem, false, false) + { + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomComposite.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomComposite.cs new file mode 100644 index 0000000000..8030107ad8 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomComposite.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 JetBrains.Annotations; +using osu.Framework.Allocation; +using osu.Game.Online.Multiplayer; + +namespace osu.Game.Screens.OnlinePlay.Multiplayer +{ + public abstract class MultiplayerRoomComposite : OnlinePlayComposite + { + [CanBeNull] + protected MultiplayerRoom Room => Client.Room; + + [Resolved] + protected StatefulMultiplayerClient Client { get; private set; } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Client.RoomUpdated += OnRoomUpdated; + OnRoomUpdated(); + } + + /// + /// Invoked when any change occurs to the multiplayer room. + /// + protected virtual void OnRoomUpdated() + { + } + + protected override void Dispose(bool isDisposing) + { + if (Client != null) + Client.RoomUpdated -= OnRoomUpdated; + + base.Dispose(isDisposing); + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomManager.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomManager.cs new file mode 100644 index 0000000000..3cb263298f --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomManager.cs @@ -0,0 +1,171 @@ +// 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.Threading.Tasks; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.ExceptionExtensions; +using osu.Framework.Logging; +using osu.Game.Extensions; +using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; +using osu.Game.Online.Rooms.RoomStatuses; +using osu.Game.Screens.OnlinePlay.Components; + +namespace osu.Game.Screens.OnlinePlay.Multiplayer +{ + public class MultiplayerRoomManager : RoomManager + { + [Resolved] + private StatefulMultiplayerClient multiplayerClient { get; set; } + + public readonly Bindable TimeBetweenListingPolls = new Bindable(); + public readonly Bindable TimeBetweenSelectionPolls = new Bindable(); + private readonly IBindable isConnected = new Bindable(); + private readonly Bindable allowPolling = new Bindable(); + + private ListingPollingComponent listingPollingComponent; + + protected override void LoadComplete() + { + base.LoadComplete(); + + isConnected.BindTo(multiplayerClient.IsConnected); + isConnected.BindValueChanged(_ => Schedule(updatePolling)); + JoinedRoom.BindValueChanged(_ => updatePolling()); + + updatePolling(); + } + + public override void CreateRoom(Room room, Action onSuccess = null, Action onError = null) + => base.CreateRoom(room, r => joinMultiplayerRoom(r, onSuccess, onError), onError); + + public override void JoinRoom(Room room, Action onSuccess = null, Action onError = null) + { + if (!multiplayerClient.IsConnected.Value) + { + onError?.Invoke("Not currently connected to the multiplayer server."); + return; + } + + // this is done here as a pre-check to avoid clicking on already closed rooms in the lounge from triggering a server join. + // should probably be done at a higher level, but due to the current structure of things this is the easiest place for now. + if (room.Status.Value is RoomStatusEnded) + { + onError?.Invoke("Cannot join an ended room."); + return; + } + + base.JoinRoom(room, r => joinMultiplayerRoom(r, onSuccess, onError), onError); + } + + public override void PartRoom() + { + if (JoinedRoom.Value == null) + return; + + var joinedRoom = JoinedRoom.Value; + + base.PartRoom(); + + multiplayerClient.LeaveRoom().CatchUnobservedExceptions(); + + // Todo: This is not the way to do this. Basically when we're the only participant and the room closes, there's no way to know if this is actually the case. + // This is delayed one frame because upon exiting the match subscreen, multiplayer updates the polling rate and messes with polling. + Schedule(() => + { + RemoveRoom(joinedRoom); + listingPollingComponent.PollImmediately(); + }); + } + + private void joinMultiplayerRoom(Room room, Action onSuccess = null, Action onError = null) + { + Debug.Assert(room.RoomID.Value != null); + + multiplayerClient.JoinRoom(room).ContinueWith(t => + { + if (t.IsCompletedSuccessfully) + Schedule(() => onSuccess?.Invoke(room)); + else + { + const string message = "Failed to join multiplayer room."; + + if (t.Exception != null) + Logger.Error(t.Exception, message); + + PartRoom(); + Schedule(() => onError?.Invoke(t.Exception?.AsSingular().Message ?? message)); + } + }); + } + + private void updatePolling() + { + if (!isConnected.Value) + ClearRooms(); + + // Don't poll when not connected or when a room has been joined. + allowPolling.Value = isConnected.Value && JoinedRoom.Value == null; + } + + protected override IEnumerable CreatePollingComponents() => new RoomPollingComponent[] + { + listingPollingComponent = new MultiplayerListingPollingComponent + { + TimeBetweenPolls = { BindTarget = TimeBetweenListingPolls }, + AllowPolling = { BindTarget = allowPolling } + }, + new MultiplayerSelectionPollingComponent + { + TimeBetweenPolls = { BindTarget = TimeBetweenSelectionPolls }, + AllowPolling = { BindTarget = allowPolling } + } + }; + + private class MultiplayerListingPollingComponent : ListingPollingComponent + { + public readonly IBindable AllowPolling = new Bindable(); + + protected override void LoadComplete() + { + base.LoadComplete(); + + AllowPolling.BindValueChanged(allowPolling => + { + if (!allowPolling.NewValue) + return; + + if (IsLoaded) + PollImmediately(); + }); + } + + protected override Task Poll() => !AllowPolling.Value ? Task.CompletedTask : base.Poll(); + } + + private class MultiplayerSelectionPollingComponent : SelectionPollingComponent + { + public readonly IBindable AllowPolling = new Bindable(); + + protected override void LoadComplete() + { + base.LoadComplete(); + + AllowPolling.BindValueChanged(allowPolling => + { + if (!allowPolling.NewValue) + return; + + if (IsLoaded) + PollImmediately(); + }); + } + + protected override Task Poll() => !AllowPolling.Value ? Task.CompletedTask : base.Poll(); + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs new file mode 100644 index 0000000000..de3069b2f6 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs @@ -0,0 +1,186 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Diagnostics; +using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; +using osu.Game.Extensions; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API; +using osu.Game.Online.Multiplayer; +using osu.Game.Users; +using osu.Game.Users.Drawables; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants +{ + public class ParticipantPanel : MultiplayerRoomComposite, IHasContextMenu + { + public readonly MultiplayerRoomUser User; + + [Resolved] + private IAPIProvider api { get; set; } + + private StateDisplay userStateDisplay; + private SpriteIcon crown; + + public ParticipantPanel(MultiplayerRoomUser user) + { + User = user; + + RelativeSizeAxes = Axes.X; + Height = 40; + } + + [BackgroundDependencyLoader] + private void load() + { + Debug.Assert(User.User != null); + + var backgroundColour = Color4Extensions.FromHex("#33413C"); + + InternalChildren = new Drawable[] + { + crown = new SpriteIcon + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Icon = FontAwesome.Solid.Crown, + Size = new Vector2(14), + Colour = Color4Extensions.FromHex("#F7E65D"), + Alpha = 0 + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Left = 24 }, + Child = new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + CornerRadius = 5, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = backgroundColour + }, + new UserCoverBackground + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + RelativeSizeAxes = Axes.Both, + Width = 0.75f, + User = User.User, + Colour = ColourInfo.GradientHorizontal(Color4.White.Opacity(0), Color4.White.Opacity(0.25f)) + }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.Both, + Spacing = new Vector2(10), + Direction = FillDirection.Horizontal, + Children = new Drawable[] + { + new UpdateableAvatar + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + RelativeSizeAxes = Axes.Both, + FillMode = FillMode.Fit, + User = User.User + }, + new UpdateableFlag + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Size = new Vector2(30, 20), + Country = User.User.Country + }, + new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 18), + Text = User.User.Username + }, + new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Font = OsuFont.GetFont(size: 14), + Text = User.User.CurrentModeRank != null ? $"#{User.User.CurrentModeRank}" : string.Empty + } + } + }, + userStateDisplay = new StateDisplay + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Margin = new MarginPadding { Right = 10 }, + } + } + } + } + }; + } + + protected override void OnRoomUpdated() + { + base.OnRoomUpdated(); + + if (Room == null) + return; + + const double fade_time = 50; + + userStateDisplay.Status = User.State; + + if (Room.Host?.Equals(User) == true) + crown.FadeIn(fade_time); + else + crown.FadeOut(fade_time); + } + + public MenuItem[] ContextMenuItems + { + get + { + if (Room == null) + return null; + + // If the local user is targetted. + if (User.UserID == api.LocalUser.Value.Id) + return null; + + // If the local user is not the host of the room. + if (Room.Host?.UserID != api.LocalUser.Value.Id) + return null; + + int targetUser = User.UserID; + + return new MenuItem[] + { + new OsuMenuItem("Give host", MenuItemType.Standard, () => + { + // Ensure the local user is still host. + if (Room.Host?.UserID != api.LocalUser.Value.Id) + return; + + Client.TransferHost(targetUser).CatchUnobservedExceptions(true); + }) + }; + } + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantsList.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantsList.cs new file mode 100644 index 0000000000..3759e45f18 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantsList.cs @@ -0,0 +1,56 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Cursor; +using osuTK; + +namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants +{ + public class ParticipantsList : MultiplayerRoomComposite + { + private FillFlowContainer panels; + + [BackgroundDependencyLoader] + private void load() + { + InternalChild = new OsuContextMenuContainer + { + RelativeSizeAxes = Axes.Both, + Child = new OsuScrollContainer + { + RelativeSizeAxes = Axes.Both, + ScrollbarVisible = false, + Child = panels = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 2) + } + } + }; + } + + protected override void OnRoomUpdated() + { + base.OnRoomUpdated(); + + if (Room == null) + panels.Clear(); + else + { + // Remove panels for users no longer in the room. + panels.RemoveAll(p => !Room.Users.Contains(p.User)); + + // Add panels for all users new to the room. + foreach (var user in Room.Users.Except(panels.Select(p => p.User))) + panels.Add(new ParticipantPanel(user)); + } + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantsListHeader.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantsListHeader.cs new file mode 100644 index 0000000000..6c1a55a0eb --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantsListHeader.cs @@ -0,0 +1,31 @@ +// 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.Game.Online.Multiplayer; +using osu.Game.Screens.OnlinePlay.Components; + +namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants +{ + public class ParticipantsListHeader : OverlinedHeader + { + [Resolved] + private StatefulMultiplayerClient client { get; set; } + + public ParticipantsListHeader() + : base("Participants") + { + } + + protected override void Update() + { + base.Update(); + + var room = client.Room; + if (room == null) + return; + + Details.Value = room.Users.Count.ToString(); + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/StateDisplay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/StateDisplay.cs new file mode 100644 index 0000000000..8d2879fc93 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/StateDisplay.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 osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Online.Multiplayer; +using osuTK; + +namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants +{ + public class StateDisplay : CompositeDrawable + { + public StateDisplay() + { + AutoSizeAxes = Axes.Both; + Alpha = 0; + } + + private MultiplayerUserState status; + + private OsuSpriteText text; + private SpriteIcon icon; + + private const double fade_time = 50; + + public MultiplayerUserState Status + { + set + { + if (value == status) + return; + + status = value; + + if (IsLoaded) + updateStatus(); + } + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChild = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Spacing = new Vector2(5), + Children = new Drawable[] + { + text = new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Font = OsuFont.GetFont(weight: FontWeight.Regular, size: 12), + Colour = Color4Extensions.FromHex("#DDFFFF") + }, + icon = new SpriteIcon + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Icon = FontAwesome.Solid.CheckCircle, + Size = new Vector2(12), + } + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + updateStatus(); + } + + [Resolved] + private OsuColour colours { get; set; } + + private void updateStatus() + { + switch (status) + { + default: + this.FadeOut(fade_time); + return; + + case MultiplayerUserState.Ready: + text.Text = "ready"; + icon.Icon = FontAwesome.Solid.CheckCircle; + icon.Colour = Color4Extensions.FromHex("#AADD00"); + break; + + case MultiplayerUserState.WaitingForLoad: + text.Text = "loading"; + icon.Icon = FontAwesome.Solid.PauseCircle; + icon.Colour = colours.Yellow; + break; + + case MultiplayerUserState.Loaded: + text.Text = "loaded"; + icon.Icon = FontAwesome.Solid.DotCircle; + icon.Colour = colours.YellowLight; + break; + + case MultiplayerUserState.Playing: + text.Text = "playing"; + icon.Icon = FontAwesome.Solid.PlayCircle; + icon.Colour = colours.BlueLight; + break; + + case MultiplayerUserState.FinishedPlay: + text.Text = "results pending"; + icon.Icon = FontAwesome.Solid.ArrowAltCircleUp; + icon.Colour = colours.BlueLighter; + break; + + case MultiplayerUserState.Results: + text.Text = "results"; + icon.Icon = FontAwesome.Solid.ArrowAltCircleUp; + icon.Colour = colours.BlueLighter; + break; + } + + this.FadeIn(fade_time); + } + } +} diff --git a/osu.Game/Screens/Multi/MultiplayerComposite.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs similarity index 84% rename from osu.Game/Screens/Multi/MultiplayerComposite.cs rename to osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs index e612e77748..64792a32f3 100644 --- a/osu.Game/Screens/Multi/MultiplayerComposite.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs @@ -5,12 +5,12 @@ using System; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics.Containers; -using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; using osu.Game.Users; -namespace osu.Game.Screens.Multi +namespace osu.Game.Screens.OnlinePlay { - public class MultiplayerComposite : CompositeDrawable + public class OnlinePlayComposite : CompositeDrawable { [Resolved(typeof(Room))] protected Bindable RoomID { get; private set; } @@ -40,12 +40,12 @@ namespace osu.Game.Screens.Multi protected Bindable MaxParticipants { get; private set; } [Resolved(typeof(Room))] - protected Bindable EndDate { get; private set; } + protected Bindable EndDate { get; private set; } [Resolved(typeof(Room))] protected Bindable Availability { get; private set; } [Resolved(typeof(Room))] - protected Bindable Duration { get; private set; } + protected Bindable Duration { get; private set; } } } diff --git a/osu.Game/Screens/Multi/Multiplayer.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs similarity index 64% rename from osu.Game/Screens/Multi/Multiplayer.cs rename to osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs index a323faeea1..4074dd1573 100644 --- a/osu.Game/Screens/Multi/Multiplayer.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs @@ -8,31 +8,28 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; -using osu.Framework.Logging; using osu.Framework.Screens; -using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Input; using osu.Game.Online.API; -using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Screens.Menu; -using osu.Game.Screens.Multi.Components; -using osu.Game.Screens.Multi.Lounge; -using osu.Game.Screens.Multi.Lounge.Components; -using osu.Game.Screens.Multi.Match; -using osu.Game.Screens.Multi.Match.Components; +using osu.Game.Screens.OnlinePlay.Components; +using osu.Game.Screens.OnlinePlay.Lounge; +using osu.Game.Screens.OnlinePlay.Lounge.Components; +using osu.Game.Screens.OnlinePlay.Match; using osu.Game.Users; using osuTK; -namespace osu.Game.Screens.Multi +namespace osu.Game.Screens.OnlinePlay { [Cached] - public class Multiplayer : OsuScreen + public abstract class OnlinePlayScreen : OsuScreen { - public override bool CursorVisible => (screenStack.CurrentScreen as IMultiplayerSubScreen)?.CursorVisible ?? true; + public override bool CursorVisible => (screenStack.CurrentScreen as IOnlinePlaySubScreen)?.CursorVisible ?? true; // this is required due to PlayerLoader eventually being pushed to the main stack // while leases may be taken out by a subscreen. @@ -46,6 +43,9 @@ namespace osu.Game.Screens.Multi private readonly IBindable isIdle = new BindableBool(); + [Cached(Type = typeof(IRoomManager))] + protected RoomManager RoomManager { get; private set; } + [Cached] private readonly Bindable selectedRoom = new Bindable(); @@ -55,14 +55,11 @@ namespace osu.Game.Screens.Multi [Resolved(CanBeNull = true)] private MusicController music { get; set; } - [Cached(Type = typeof(IRoomManager))] - private RoomManager roomManager; - [Resolved] private OsuGameBase game { get; set; } [Resolved] - private IAPIProvider api { get; set; } + protected IAPIProvider API { get; private set; } [Resolved(CanBeNull = true)] private OsuLogo logo { get; set; } @@ -70,7 +67,7 @@ namespace osu.Game.Screens.Multi private readonly Drawable header; private readonly Drawable headerBackground; - public Multiplayer() + protected OnlinePlayScreen() { Anchor = Anchor.Centre; Origin = Anchor.Centre; @@ -127,24 +124,30 @@ namespace osu.Game.Screens.Multi } } }, - screenStack = new MultiplayerSubScreenStack { RelativeSizeAxes = Axes.Both } + screenStack = new OnlinePlaySubScreenStack { RelativeSizeAxes = Axes.Both } } }, - new Header(screenStack), - createButton = new CreateRoomButton + new Header(ScreenTitle, screenStack), + createButton = CreateNewMultiplayerGameButton().With(button => { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - Action = () => CreateRoom() - }, - roomManager = new RoomManager() + button.Anchor = Anchor.TopRight; + button.Origin = Anchor.TopRight; + button.Size = new Vector2(150, Header.HEIGHT - 20); + button.Margin = new MarginPadding + { + Top = 10, + Right = 10 + HORIZONTAL_OVERFLOW_PADDING, + }; + button.Action = () => OpenNewRoom(); + }), + RoomManager = CreateRoomManager() } }; screenStack.ScreenPushed += screenPushed; screenStack.ScreenExited += screenExited; - screenStack.Push(loungeSubScreen = new LoungeSubScreen()); + screenStack.Push(loungeSubScreen = CreateLounge()); } private readonly IBindable apiState = new Bindable(); @@ -152,7 +155,7 @@ namespace osu.Game.Screens.Multi [BackgroundDependencyLoader(true)] private void load(IdleTracker idleTracker) { - apiState.BindTo(api.State); + apiState.BindTo(API.State); apiState.BindValueChanged(onlineStateChanged, true); if (idleTracker != null) @@ -168,7 +171,7 @@ namespace osu.Game.Screens.Multi protected override void LoadComplete() { base.LoadComplete(); - isIdle.BindValueChanged(idle => updatePollingRate(idle.NewValue), true); + isIdle.BindValueChanged(idle => UpdatePollingRate(idle.NewValue), true); } protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) @@ -178,36 +181,7 @@ namespace osu.Game.Screens.Multi return dependencies; } - private void updatePollingRate(bool idle) - { - if (!this.IsCurrentScreen()) - { - roomManager.TimeBetweenListingPolls = 0; - roomManager.TimeBetweenSelectionPolls = 0; - } - else - { - switch (screenStack.CurrentScreen) - { - case LoungeSubScreen _: - roomManager.TimeBetweenListingPolls = idle ? 120000 : 15000; - roomManager.TimeBetweenSelectionPolls = idle ? 120000 : 15000; - break; - - case MatchSubScreen _: - roomManager.TimeBetweenListingPolls = 0; - roomManager.TimeBetweenSelectionPolls = idle ? 30000 : 5000; - break; - - default: - roomManager.TimeBetweenListingPolls = 0; - roomManager.TimeBetweenSelectionPolls = 0; - break; - } - } - - Logger.Log($"Polling adjusted (listing: {roomManager.TimeBetweenListingPolls}, selection: {roomManager.TimeBetweenSelectionPolls})"); - } + protected abstract void UpdatePollingRate(bool isIdle); private void forcefullyExit() { @@ -229,7 +203,10 @@ namespace osu.Game.Screens.Multi this.FadeIn(); waves.Show(); - beginHandlingTrack(); + if (loungeSubScreen.IsCurrentScreen()) + loungeSubScreen.OnEntering(last); + else + loungeSubScreen.MakeCurrent(); } public override void OnResuming(IScreen last) @@ -237,11 +214,10 @@ namespace osu.Game.Screens.Multi this.FadeIn(250); this.ScaleTo(1, 250, Easing.OutSine); + screenStack.CurrentScreen?.OnResuming(last); base.OnResuming(last); - beginHandlingTrack(); - - updatePollingRate(isIdle.Value); + UpdatePollingRate(isIdle.Value); } public override void OnSuspending(IScreen next) @@ -249,31 +225,27 @@ namespace osu.Game.Screens.Multi this.ScaleTo(1.1f, 250, Easing.InSine); this.FadeOut(250); - endHandlingTrack(); + screenStack.CurrentScreen?.OnSuspending(next); - updatePollingRate(isIdle.Value); + UpdatePollingRate(isIdle.Value); } public override bool OnExiting(IScreen next) { - roomManager.PartRoom(); + RoomManager.PartRoom(); waves.Hide(); this.Delay(WaveContainer.DISAPPEAR_DURATION).FadeOut(); - if (screenStack.CurrentScreen != null) - loungeSubScreen.MakeCurrent(); - - endHandlingTrack(); - + screenStack.CurrentScreen?.OnExiting(next); base.OnExiting(next); return false; } public override bool OnBackButton() { - if ((screenStack.CurrentScreen as IMultiplayerSubScreen)?.OnBackButton() == true) + if ((screenStack.CurrentScreen as IOnlinePlaySubScreen)?.OnBackButton() == true) return true; if (screenStack.CurrentScreen != null && !(screenStack.CurrentScreen is LoungeSubScreen)) @@ -294,21 +266,16 @@ namespace osu.Game.Screens.Multi } /// - /// Create a new room. + /// Creates and opens the newly-created room. /// /// An optional template to use when creating the room. - public void CreateRoom(Room room = null) => loungeSubScreen.Open(room ?? new Room { Name = { Value = $"{api.LocalUser}'s awesome room" } }); + public void OpenNewRoom(Room room = null) => loungeSubScreen.Open(room ?? CreateNewRoom()); - private void beginHandlingTrack() - { - Beatmap.BindValueChanged(updateTrack, true); - } - - private void endHandlingTrack() - { - cancelLooping(); - Beatmap.ValueChanged -= updateTrack; - } + /// + /// Creates a new room. + /// + /// The created . + protected abstract Room CreateNewRoom(); private void screenPushed(IScreen lastScreen, IScreen newScreen) { @@ -328,13 +295,13 @@ namespace osu.Game.Screens.Multi switch (newScreen) { case LoungeSubScreen _: - header.Delay(MultiplayerSubScreen.RESUME_TRANSITION_DELAY).ResizeHeightTo(400, MultiplayerSubScreen.APPEAR_DURATION, Easing.OutQuint); - headerBackground.MoveToX(0, MultiplayerSubScreen.X_MOVE_DURATION, Easing.OutQuint); + header.Delay(OnlinePlaySubScreen.RESUME_TRANSITION_DELAY).ResizeHeightTo(400, OnlinePlaySubScreen.APPEAR_DURATION, Easing.OutQuint); + headerBackground.MoveToX(0, OnlinePlaySubScreen.X_MOVE_DURATION, Easing.OutQuint); break; - case MatchSubScreen _: - header.ResizeHeightTo(135, MultiplayerSubScreen.APPEAR_DURATION, Easing.OutQuint); - headerBackground.MoveToX(-MultiplayerSubScreen.X_SHIFT, MultiplayerSubScreen.X_MOVE_DURATION, Easing.OutQuint); + case RoomSubScreen _: + header.ResizeHeightTo(135, OnlinePlaySubScreen.APPEAR_DURATION, Easing.OutQuint); + headerBackground.MoveToX(-OnlinePlaySubScreen.X_SHIFT, OnlinePlaySubScreen.X_MOVE_DURATION, Easing.OutQuint); break; } @@ -344,42 +311,19 @@ namespace osu.Game.Screens.Multi if (newScreen is IOsuScreen newOsuScreen) ((IBindable)Activity).BindTo(newOsuScreen.Activity); - updatePollingRate(isIdle.Value); + UpdatePollingRate(isIdle.Value); createButton.FadeTo(newScreen is LoungeSubScreen ? 1 : 0, 200); - - updateTrack(); } - private void updateTrack(ValueChangedEvent _ = null) - { - if (screenStack.CurrentScreen is MatchSubScreen) - { - var track = Beatmap.Value?.Track; + protected IScreen CurrentSubScreen => screenStack.CurrentScreen; - if (track != null) - { - track.RestartPoint = Beatmap.Value.Metadata.PreviewTime; - track.Looping = true; + protected abstract string ScreenTitle { get; } - music?.EnsurePlayingSomething(); - } - } - else - { - cancelLooping(); - } - } + protected abstract RoomManager CreateRoomManager(); - private void cancelLooping() - { - var track = Beatmap?.Value?.Track; + protected abstract LoungeSubScreen CreateLounge(); - if (track != null) - { - track.Looping = false; - track.RestartPoint = 0; - } - } + protected abstract OsuButton CreateNewMultiplayerGameButton(); private class MultiplayerWaveContainer : WaveContainer { @@ -394,7 +338,7 @@ namespace osu.Game.Screens.Multi } } - private class HeaderBackgroundSprite : MultiplayerBackgroundSprite + private class HeaderBackgroundSprite : OnlinePlayBackgroundSprite { protected override UpdateableBeatmapBackgroundSprite CreateBackgroundSprite() => new BackgroundSprite { RelativeSizeAxes = Axes.Both }; @@ -403,26 +347,5 @@ namespace osu.Game.Screens.Multi protected override double TransformDuration => 200; } } - - public class CreateRoomButton : PurpleTriangleButton - { - public CreateRoomButton() - { - Size = new Vector2(150, Header.HEIGHT - 20); - Margin = new MarginPadding - { - Top = 10, - Right = 10 + HORIZONTAL_OVERFLOW_PADDING, - }; - } - - [BackgroundDependencyLoader] - private void load() - { - Triangles.TriangleScale = 1.5f; - - Text = "Create room"; - } - } } } diff --git a/osu.Game/Screens/Multi/MultiplayerSubScreen.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreen.cs similarity index 92% rename from osu.Game/Screens/Multi/MultiplayerSubScreen.cs rename to osu.Game/Screens/OnlinePlay/OnlinePlaySubScreen.cs index 8e46de1a95..e1bd889088 100644 --- a/osu.Game/Screens/Multi/MultiplayerSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreen.cs @@ -5,9 +5,9 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Screens; -namespace osu.Game.Screens.Multi +namespace osu.Game.Screens.OnlinePlay { - public abstract class MultiplayerSubScreen : OsuScreen, IMultiplayerSubScreen + public abstract class OnlinePlaySubScreen : OsuScreen, IOnlinePlaySubScreen { public override bool DisallowExternalBeatmapRulesetChanges => false; @@ -16,7 +16,7 @@ namespace osu.Game.Screens.Multi [Resolved(CanBeNull = true)] protected IRoomManager RoomManager { get; private set; } - protected MultiplayerSubScreen() + protected OnlinePlaySubScreen() { Anchor = Anchor.Centre; Origin = Anchor.Centre; diff --git a/osu.Game/Screens/Multi/MultiplayerSubScreenStack.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreenStack.cs similarity index 88% rename from osu.Game/Screens/Multi/MultiplayerSubScreenStack.cs rename to osu.Game/Screens/OnlinePlay/OnlinePlaySubScreenStack.cs index 3b0ed0dba1..7f2a0980c1 100644 --- a/osu.Game/Screens/Multi/MultiplayerSubScreenStack.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreenStack.cs @@ -3,9 +3,9 @@ using osu.Framework.Screens; -namespace osu.Game.Screens.Multi +namespace osu.Game.Screens.OnlinePlay { - public class MultiplayerSubScreenStack : OsuScreenStack + public class OnlinePlaySubScreenStack : OsuScreenStack { protected override void ScreenChanged(IScreen prev, IScreen next) { diff --git a/osu.Game/Screens/OnlinePlay/Playlists/CreatePlaylistsRoomButton.cs b/osu.Game/Screens/OnlinePlay/Playlists/CreatePlaylistsRoomButton.cs new file mode 100644 index 0000000000..fcb773f8be --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Playlists/CreatePlaylistsRoomButton.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.Allocation; +using osu.Game.Screens.OnlinePlay.Match.Components; + +namespace osu.Game.Screens.OnlinePlay.Playlists +{ + public class CreatePlaylistsRoomButton : PurpleTriangleButton + { + [BackgroundDependencyLoader] + private void load() + { + Triangles.TriangleScale = 1.5f; + + Text = "Create playlist"; + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Playlists/Playlists.cs b/osu.Game/Screens/OnlinePlay/Playlists/Playlists.cs new file mode 100644 index 0000000000..5b132c97fd --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Playlists/Playlists.cs @@ -0,0 +1,62 @@ +// 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.Logging; +using osu.Framework.Screens; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.Rooms; +using osu.Game.Screens.OnlinePlay.Components; +using osu.Game.Screens.OnlinePlay.Lounge; +using osu.Game.Screens.OnlinePlay.Match; + +namespace osu.Game.Screens.OnlinePlay.Playlists +{ + public class Playlists : OnlinePlayScreen + { + protected override void UpdatePollingRate(bool isIdle) + { + var playlistsManager = (PlaylistsRoomManager)RoomManager; + + if (!this.IsCurrentScreen()) + { + playlistsManager.TimeBetweenListingPolls.Value = 0; + playlistsManager.TimeBetweenSelectionPolls.Value = 0; + } + else + { + switch (CurrentSubScreen) + { + case LoungeSubScreen _: + playlistsManager.TimeBetweenListingPolls.Value = isIdle ? 120000 : 15000; + playlistsManager.TimeBetweenSelectionPolls.Value = isIdle ? 120000 : 15000; + break; + + case RoomSubScreen _: + playlistsManager.TimeBetweenListingPolls.Value = 0; + playlistsManager.TimeBetweenSelectionPolls.Value = isIdle ? 30000 : 5000; + break; + + default: + playlistsManager.TimeBetweenListingPolls.Value = 0; + playlistsManager.TimeBetweenSelectionPolls.Value = 0; + break; + } + } + + Logger.Log($"Polling adjusted (listing: {playlistsManager.TimeBetweenListingPolls.Value}, selection: {playlistsManager.TimeBetweenSelectionPolls.Value})"); + } + + protected override Room CreateNewRoom() + { + return new Room { Name = { Value = $"{API.LocalUser}'s awesome playlist" } }; + } + + protected override string ScreenTitle => "Playlists"; + + protected override RoomManager CreateRoomManager() => new PlaylistsRoomManager(); + + protected override LoungeSubScreen CreateLounge() => new PlaylistsLoungeSubScreen(); + + protected override OsuButton CreateNewMultiplayerGameButton() => new CreatePlaylistsRoomButton(); + } +} diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs new file mode 100644 index 0000000000..bfbff4240c --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs @@ -0,0 +1,17 @@ +// 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.Online.Rooms; +using osu.Game.Screens.OnlinePlay.Lounge; +using osu.Game.Screens.OnlinePlay.Lounge.Components; +using osu.Game.Screens.OnlinePlay.Match; + +namespace osu.Game.Screens.OnlinePlay.Playlists +{ + public class PlaylistsLoungeSubScreen : LoungeSubScreen + { + protected override FilterControl CreateFilterControl() => new PlaylistsFilterControl(); + + protected override RoomSubScreen CreateRoomSubScreen(Room room) => new PlaylistsRoomSubScreen(room); + } +} diff --git a/osu.Game/Screens/Multi/Match/Components/MatchSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsMatchSettingsOverlay.cs similarity index 87% rename from osu.Game/Screens/Multi/Match/Components/MatchSettingsOverlay.cs rename to osu.Game/Screens/OnlinePlay/Playlists/PlaylistsMatchSettingsOverlay.cs index b8003b9774..6b92526f35 100644 --- a/osu.Game/Screens/Multi/Match/Components/MatchSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsMatchSettingsOverlay.cs @@ -1,4 +1,4 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; @@ -14,27 +14,20 @@ using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; -using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; using osu.Game.Overlays; +using osu.Game.Screens.OnlinePlay.Match.Components; using osuTK; -using osuTK.Graphics; -namespace osu.Game.Screens.Multi.Match.Components +namespace osu.Game.Screens.OnlinePlay.Playlists { - public class MatchSettingsOverlay : FocusedOverlayContainer + public class PlaylistsMatchSettingsOverlay : MatchSettingsOverlay { - private const float transition_duration = 350; - private const float field_padding = 45; - public Action EditPlaylist; - protected MatchSettings Settings { get; private set; } - [BackgroundDependencyLoader] private void load() { - Masking = true; - Child = Settings = new MatchSettings { RelativeSizeAxes = Axes.Both, @@ -43,17 +36,7 @@ namespace osu.Game.Screens.Multi.Match.Components }; } - protected override void PopIn() - { - Settings.MoveToY(0, transition_duration, Easing.OutQuint); - } - - protected override void PopOut() - { - Settings.MoveToY(-1, transition_duration, Easing.InSine); - } - - protected class MatchSettings : MultiplayerComposite + protected class MatchSettings : OnlinePlayComposite { private const float disabled_alpha = 0.2f; @@ -126,7 +109,7 @@ namespace osu.Game.Screens.Multi.Match.Components { new SectionContainer { - Padding = new MarginPadding { Right = field_padding / 2 }, + Padding = new MarginPadding { Right = FIELD_PADDING / 2 }, Children = new[] { new Section("Room name") @@ -216,7 +199,7 @@ namespace osu.Game.Screens.Multi.Match.Components { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, - Padding = new MarginPadding { Left = field_padding / 2 }, + Padding = new MarginPadding { Left = FIELD_PADDING / 2 }, Children = new[] { new Section("Playlist") @@ -325,7 +308,7 @@ namespace osu.Game.Screens.Multi.Match.Components Availability.BindValueChanged(availability => AvailabilityPicker.Current.Value = availability.NewValue, true); Type.BindValueChanged(type => TypePicker.Current.Value = type.NewValue, true); MaxParticipants.BindValueChanged(count => MaxParticipantsField.Text = count.NewValue?.ToString(), true); - Duration.BindValueChanged(duration => DurationField.Current.Value = duration.NewValue, true); + Duration.BindValueChanged(duration => DurationField.Current.Value = duration.NewValue ?? TimeSpan.FromMinutes(30), true); playlist.Items.BindTo(Playlist); Playlist.BindCollectionChanged(onPlaylistChanged, true); @@ -379,77 +362,6 @@ namespace osu.Game.Screens.Multi.Match.Components } } - private class SettingsTextBox : OsuTextBox - { - [BackgroundDependencyLoader] - private void load() - { - BackgroundUnfocused = Color4.Black; - BackgroundFocused = Color4.Black; - } - } - - private class SettingsNumberTextBox : SettingsTextBox - { - protected override bool CanAddCharacter(char character) => char.IsNumber(character); - } - - private class SettingsPasswordTextBox : OsuPasswordTextBox - { - [BackgroundDependencyLoader] - private void load() - { - BackgroundUnfocused = Color4.Black; - BackgroundFocused = Color4.Black; - } - } - - private class SectionContainer : FillFlowContainer
- { - public SectionContainer() - { - RelativeSizeAxes = Axes.X; - AutoSizeAxes = Axes.Y; - Width = 0.5f; - Direction = FillDirection.Vertical; - Spacing = new Vector2(field_padding); - } - } - - private class Section : Container - { - private readonly Container content; - - protected override Container Content => content; - - public Section(string title) - { - AutoSizeAxes = Axes.Y; - RelativeSizeAxes = Axes.X; - - InternalChild = new FillFlowContainer - { - AutoSizeAxes = Axes.Y, - RelativeSizeAxes = Axes.X, - Direction = FillDirection.Vertical, - Spacing = new Vector2(5), - Children = new Drawable[] - { - new OsuSpriteText - { - Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 12), - Text = title.ToUpper(), - }, - content = new Container - { - AutoSizeAxes = Axes.Y, - RelativeSizeAxes = Axes.X, - }, - }, - }; - } - } - public class CreateRoomButton : TriangleButton { public CreateRoomButton() diff --git a/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs similarity index 55% rename from osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs rename to osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs index 0efa9c5196..2c3e7a12e2 100644 --- a/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs @@ -5,28 +5,30 @@ using System; using System.Diagnostics; using System.Linq; using System.Threading; +using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Logging; using osu.Framework.Screens; using osu.Game.Online.API; -using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; using osu.Game.Rulesets; using osu.Game.Scoring; -using osu.Game.Screens.Multi.Ranking; using osu.Game.Screens.Play; using osu.Game.Screens.Ranking; -namespace osu.Game.Screens.Multi.Play +namespace osu.Game.Screens.OnlinePlay.Playlists { - public class TimeshiftPlayer : Player + public class PlaylistsPlayer : Player { public Action Exited; [Resolved(typeof(Room), nameof(Room.RoomID))] - private Bindable roomId { get; set; } + protected Bindable RoomId { get; private set; } - private readonly PlaylistItem playlistItem; + protected readonly PlaylistItem PlaylistItem; + + protected int? Token { get; private set; } [Resolved] private IAPIProvider api { get; set; } @@ -34,32 +36,31 @@ namespace osu.Game.Screens.Multi.Play [Resolved] private IBindable ruleset { get; set; } - public TimeshiftPlayer(PlaylistItem playlistItem) + public PlaylistsPlayer(PlaylistItem playlistItem, PlayerConfiguration configuration = null) + : base(configuration) { - this.playlistItem = playlistItem; + PlaylistItem = playlistItem; } - private int? token; - [BackgroundDependencyLoader] private void load() { - token = null; + Token = null; bool failed = false; - // Sanity checks to ensure that TimeshiftPlayer matches the settings for the current PlaylistItem - if (Beatmap.Value.BeatmapInfo.OnlineBeatmapID != playlistItem.Beatmap.Value.OnlineBeatmapID) + // Sanity checks to ensure that PlaylistsPlayer matches the settings for the current PlaylistItem + if (Beatmap.Value.BeatmapInfo.OnlineBeatmapID != PlaylistItem.Beatmap.Value.OnlineBeatmapID) throw new InvalidOperationException("Current Beatmap does not match PlaylistItem's Beatmap"); - if (ruleset.Value.ID != playlistItem.Ruleset.Value.ID) + if (ruleset.Value.ID != PlaylistItem.Ruleset.Value.ID) throw new InvalidOperationException("Current Ruleset does not match PlaylistItem's Ruleset"); - if (!playlistItem.RequiredMods.All(m => Mods.Value.Any(m.Equals))) + if (!PlaylistItem.RequiredMods.All(m => Mods.Value.Any(m.Equals))) throw new InvalidOperationException("Current Mods do not match PlaylistItem's RequiredMods"); - var req = new CreateRoomScoreRequest(roomId.Value ?? 0, playlistItem.ID, Game.VersionHash); - req.Success += r => token = r.ID; + var req = new CreateRoomScoreRequest(RoomId.Value ?? 0, PlaylistItem.ID, Game.VersionHash); + req.Success += r => Token = r.ID; req.Failure += e => { failed = true; @@ -75,7 +76,7 @@ namespace osu.Game.Screens.Multi.Play api.Queue(req); - while (!failed && !token.HasValue) + while (!failed && !Token.HasValue) Thread.Sleep(1000); } @@ -91,25 +92,42 @@ namespace osu.Game.Screens.Multi.Play protected override ResultsScreen CreateResults(ScoreInfo score) { - Debug.Assert(roomId.Value != null); - return new TimeshiftResultsScreen(score, roomId.Value.Value, playlistItem, true); + Debug.Assert(RoomId.Value != null); + return new PlaylistsResultsScreen(score, RoomId.Value.Value, PlaylistItem, true); } - protected override ScoreInfo CreateScore() + protected override Score CreateScore() { var score = base.CreateScore(); - score.TotalScore = (int)Math.Round(ScoreProcessor.GetStandardisedScore()); - - Debug.Assert(token != null); - - var request = new SubmitRoomScoreRequest(token.Value, roomId.Value ?? 0, playlistItem.ID, score); - request.Success += s => score.OnlineScoreID = s.ID; - request.Failure += e => Logger.Error(e, "Failed to submit score"); - api.Queue(request); - + score.ScoreInfo.TotalScore = (int)Math.Round(ScoreProcessor.GetStandardisedScore()); return score; } + protected override async Task SubmitScore(Score score) + { + await base.SubmitScore(score); + + Debug.Assert(Token != null); + + var tcs = new TaskCompletionSource(); + var request = new SubmitRoomScoreRequest(Token.Value, RoomId.Value ?? 0, PlaylistItem.ID, score.ScoreInfo); + + request.Success += s => + { + score.ScoreInfo.OnlineScoreID = s.ID; + tcs.SetResult(true); + }; + + request.Failure += e => + { + Logger.Error(e, "Failed to submit score"); + tcs.SetResult(false); + }; + + api.Queue(request); + await tcs.Task; + } + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsReadyButton.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsReadyButton.cs new file mode 100644 index 0000000000..edee8e571a --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsReadyButton.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 System; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Game.Graphics; +using osu.Game.Online.Rooms; +using osu.Game.Screens.OnlinePlay.Components; + +namespace osu.Game.Screens.OnlinePlay.Playlists +{ + public class PlaylistsReadyButton : ReadyButton + { + [Resolved(typeof(Room), nameof(Room.EndDate))] + private Bindable endDate { get; set; } + + public PlaylistsReadyButton() + { + Text = "Start"; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + BackgroundColour = colours.Green; + Triangles.ColourDark = colours.Green; + Triangles.ColourLight = colours.GreenLight; + } + + protected override void Update() + { + base.Update(); + + Enabled.Value = endDate.Value != null && DateTimeOffset.UtcNow.AddSeconds(30).AddMilliseconds(GameBeatmap.Value.Track.Length) < endDate.Value; + } + } +} diff --git a/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsResultsScreen.cs similarity index 96% rename from osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs rename to osu.Game/Screens/OnlinePlay/Playlists/PlaylistsResultsScreen.cs index 3623208fa7..e13c8a9f82 100644 --- a/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsResultsScreen.cs @@ -11,13 +11,13 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; -using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; using osu.Game.Scoring; using osu.Game.Screens.Ranking; -namespace osu.Game.Screens.Multi.Ranking +namespace osu.Game.Screens.OnlinePlay.Playlists { - public class TimeshiftResultsScreen : ResultsScreen + public class PlaylistsResultsScreen : ResultsScreen { private readonly int roomId; private readonly PlaylistItem playlistItem; @@ -32,8 +32,8 @@ namespace osu.Game.Screens.Multi.Ranking [Resolved] private IAPIProvider api { get; set; } - public TimeshiftResultsScreen(ScoreInfo score, int roomId, PlaylistItem playlistItem, bool allowRetry) - : base(score, allowRetry) + public PlaylistsResultsScreen(ScoreInfo score, int roomId, PlaylistItem playlistItem, bool allowRetry, bool allowWatchingReplay = true) + : base(score, allowRetry, allowWatchingReplay) { this.roomId = roomId; this.playlistItem = playlistItem; diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomManager.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomManager.cs new file mode 100644 index 0000000000..c55d1c3e94 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomManager.cs @@ -0,0 +1,21 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Framework.Bindables; +using osu.Game.Screens.OnlinePlay.Components; + +namespace osu.Game.Screens.OnlinePlay.Playlists +{ + public class PlaylistsRoomManager : RoomManager + { + public readonly Bindable TimeBetweenListingPolls = new Bindable(); + public readonly Bindable TimeBetweenSelectionPolls = new Bindable(); + + protected override IEnumerable CreatePollingComponents() => new RoomPollingComponent[] + { + new ListingPollingComponent { TimeBetweenPolls = { BindTarget = TimeBetweenListingPolls } }, + new SelectionPollingComponent { TimeBetweenPolls = { BindTarget = TimeBetweenSelectionPolls } } + }; + } +} diff --git a/osu.Game/Screens/Multi/Match/MatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs similarity index 74% rename from osu.Game/Screens/Multi/Match/MatchSubScreen.cs rename to osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index 2f8aad4e65..e76ca995bf 100644 --- a/osu.Game/Screens/Multi/Match/MatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -1,7 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Diagnostics; using System.Linq; using osu.Framework.Allocation; @@ -9,58 +8,34 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Screens; -using osu.Game.Audio; -using osu.Game.Beatmaps; using osu.Game.Online.API; -using osu.Game.Online.Multiplayer; -using osu.Game.Online.Multiplayer.GameTypes; -using osu.Game.Rulesets.Mods; -using osu.Game.Screens.Multi.Components; -using osu.Game.Screens.Multi.Match.Components; -using osu.Game.Screens.Multi.Play; -using osu.Game.Screens.Multi.Ranking; -using osu.Game.Screens.Play; +using osu.Game.Online.Rooms; +using osu.Game.Screens.OnlinePlay.Components; +using osu.Game.Screens.OnlinePlay.Match; +using osu.Game.Screens.OnlinePlay.Match.Components; using osu.Game.Screens.Select; using osu.Game.Users; -using Footer = osu.Game.Screens.Multi.Match.Components.Footer; +using Footer = osu.Game.Screens.OnlinePlay.Match.Components.Footer; -namespace osu.Game.Screens.Multi.Match +namespace osu.Game.Screens.OnlinePlay.Playlists { - [Cached(typeof(IPreviewTrackOwner))] - public class MatchSubScreen : MultiplayerSubScreen, IPreviewTrackOwner + public class PlaylistsRoomSubScreen : RoomSubScreen { - public override bool DisallowExternalBeatmapRulesetChanges => true; - public override string Title { get; } - public override string ShortTitle => "room"; + public override string ShortTitle => "playlist"; [Resolved(typeof(Room), nameof(Room.RoomID))] private Bindable roomId { get; set; } - [Resolved(typeof(Room), nameof(Room.Type))] - private Bindable type { get; set; } - - [Resolved(typeof(Room), nameof(Room.Playlist))] - private BindableList playlist { get; set; } - - [Resolved] - private BeatmapManager beatmapManager { get; set; } - - [Resolved(canBeNull: true)] - private Multiplayer multiplayer { get; set; } - - protected readonly Bindable SelectedItem = new Bindable(); - private MatchSettingsOverlay settingsOverlay; private MatchLeaderboard leaderboard; - private IBindable> managerUpdated; private OverlinedHeader participantsHeader; - public MatchSubScreen(Room room) + public PlaylistsRoomSubScreen(Room room) { - Title = room.RoomID.Value == null ? "New room" : room.Name.Value; + Title = room.RoomID.Value == null ? "New playlist" : room.Name.Value; Activity.Value = new UserActivity.InLobby(room); } @@ -96,7 +71,7 @@ namespace osu.Game.Screens.Multi.Match }, Content = new[] { - new Drawable[] { new Components.Header() }, + new Drawable[] { new Match.Components.Header() }, new Drawable[] { participantsHeader = new OverlinedHeader("Participants") @@ -141,12 +116,12 @@ namespace osu.Game.Screens.Multi.Match new DrawableRoomPlaylistWithResults { RelativeSizeAxes = Axes.Both, - Items = { BindTarget = playlist }, + Items = { BindTarget = Playlist }, SelectedItem = { BindTarget = SelectedItem }, RequestShowResults = item => { Debug.Assert(roomId.Value != null); - multiplayer?.Push(new TimeshiftResultsScreen(null, roomId.Value.Value, item, false)); + ParentScreen?.Push(new PlaylistsResultsScreen(null, roomId.Value.Value, item, false)); } } }, @@ -208,7 +183,7 @@ namespace osu.Game.Screens.Multi.Match new Dimension(GridSizeMode.AutoSize), } }, - settingsOverlay = new MatchSettingsOverlay + settingsOverlay = new PlaylistsMatchSettingsOverlay { RelativeSizeAxes = Axes.Both, EditPlaylist = () => this.Push(new MatchSongSelect()), @@ -234,61 +209,14 @@ namespace osu.Game.Screens.Multi.Match // Set the first playlist item. // This is scheduled since updating the room and playlist may happen in an arbitrary order (via Room.CopyFrom()). - Schedule(() => SelectedItem.Value = playlist.FirstOrDefault()); + Schedule(() => SelectedItem.Value = Playlist.FirstOrDefault()); } }, true); - - SelectedItem.BindValueChanged(_ => Scheduler.AddOnce(selectedItemChanged)); - SelectedItem.Value = playlist.FirstOrDefault(); - - managerUpdated = beatmapManager.ItemUpdated.GetBoundCopy(); - managerUpdated.BindValueChanged(beatmapUpdated); } - public override bool OnExiting(IScreen next) + private void onStart() => StartPlay(() => new PlaylistsPlayer(SelectedItem.Value) { - RoomManager?.PartRoom(); - Mods.Value = Array.Empty(); - - return base.OnExiting(next); - } - - private void selectedItemChanged() - { - updateWorkingBeatmap(); - - var item = SelectedItem.Value; - - Mods.Value = item?.RequiredMods?.ToArray() ?? Array.Empty(); - - if (item?.Ruleset != null) - Ruleset.Value = item.Ruleset.Value; - } - - private void beatmapUpdated(ValueChangedEvent> weakSet) => Schedule(updateWorkingBeatmap); - - private void updateWorkingBeatmap() - { - var beatmap = SelectedItem.Value?.Beatmap.Value; - - // Retrieve the corresponding local beatmap, since we can't directly use the playlist's beatmap info - var localBeatmap = beatmap == null ? null : beatmapManager.QueryBeatmap(b => b.OnlineBeatmapID == beatmap.OnlineBeatmapID); - - Beatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap); - } - - private void onStart() - { - switch (type.Value) - { - default: - case GameTypeTimeshift _: - multiplayer?.Push(new PlayerLoader(() => new TimeshiftPlayer(SelectedItem.Value) - { - Exited = () => leaderboard.RefreshScores() - })); - break; - } - } + Exited = () => leaderboard.RefreshScores() + }); } } diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs index e53c56b390..e33cc05e64 100644 --- a/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs @@ -1,9 +1,9 @@ // 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 JetBrains.Annotations; -using osu.Framework.Bindables; +using osu.Framework.Caching; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Users; @@ -13,10 +13,11 @@ namespace osu.Game.Screens.Play.HUD { public class GameplayLeaderboard : FillFlowContainer { + private readonly Cached sorting = new Cached(); + public GameplayLeaderboard() { - RelativeSizeAxes = Axes.X; - AutoSizeAxes = Axes.Y; + Width = GameplayLeaderboardScore.EXTENDED_WIDTH + GameplayLeaderboardScore.SHEAR_WIDTH; Direction = FillDirection.Vertical; @@ -26,41 +27,56 @@ namespace osu.Game.Screens.Play.HUD LayoutEasing = Easing.OutQuint; } + protected override void LoadComplete() + { + base.LoadComplete(); + + Scheduler.AddDelayed(sort, 1000, true); + } + /// /// Adds a player to the leaderboard. /// - /// The bindable current score of the player. /// The player. - public void AddPlayer([NotNull] BindableDouble currentScore, [NotNull] User user) + /// + /// Whether the player should be tracked on the leaderboard. + /// Set to true for the local player or a player whose replay is currently being played. + /// + public ILeaderboardScore AddPlayer(User user, bool isTracked) { - var scoreItem = addScore(currentScore.Value, user); - currentScore.ValueChanged += s => scoreItem.TotalScore = s.NewValue; - } - - private GameplayLeaderboardScore addScore(double totalScore, User user) - { - var scoreItem = new GameplayLeaderboardScore + var drawable = new GameplayLeaderboardScore(user, isTracked) { - User = user, - TotalScore = totalScore, - OnScoreChange = updateScores, + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, }; - Add(scoreItem); - updateScores(); + base.Add(drawable); + drawable.TotalScore.BindValueChanged(_ => sorting.Invalidate(), true); - return scoreItem; + Height = Count * (GameplayLeaderboardScore.PANEL_HEIGHT + Spacing.Y); + + return drawable; } - private void updateScores() + public sealed override void Add(GameplayLeaderboardScore drawable) { - var orderedByScore = this.OrderByDescending(i => i.TotalScore).ToList(); + throw new NotSupportedException($"Use {nameof(AddPlayer)} instead."); + } + + private void sort() + { + if (sorting.IsValid) + return; + + var orderedByScore = this.OrderByDescending(i => i.TotalScore.Value).ToList(); for (int i = 0; i < Count; i++) { SetLayoutPosition(orderedByScore[i], i); orderedByScore[i].ScorePosition = i + 1; } + + sorting.Validate(); } } } diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs index 4c75f422c9..51b19a8d45 100644 --- a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs +++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs @@ -1,25 +1,39 @@ // 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 Humanizer; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Users; +using osu.Game.Users.Drawables; +using osu.Game.Utils; using osuTK; +using osuTK.Graphics; namespace osu.Game.Screens.Play.HUD { - public class GameplayLeaderboardScore : CompositeDrawable + public class GameplayLeaderboardScore : CompositeDrawable, ILeaderboardScore { - private readonly OsuSpriteText positionText, positionSymbol, userString; - private readonly GlowingSpriteText scoreText; + public const float EXTENDED_WIDTH = 255f; - public Action OnScoreChange; + private const float regular_width = 235f; + + public const float PANEL_HEIGHT = 35f; + + public const float SHEAR_WIDTH = PANEL_HEIGHT * panel_shear; + + private const float panel_shear = 0.15f; + + private OsuSpriteText positionText, scoreText, accuracyText, comboText, usernameText; + + public BindableDouble TotalScore { get; } = new BindableDouble(); + public BindableDouble Accuracy { get; } = new BindableDouble(1); + public BindableInt Combo { get; } = new BindableInt(); private int? scorePosition; @@ -28,109 +42,249 @@ namespace osu.Game.Screens.Play.HUD get => scorePosition; set { + if (value == scorePosition) + return; + scorePosition = value; if (scorePosition.HasValue) - positionText.Text = $"#{scorePosition.Value.ToMetric(decimals: scorePosition < 100000 ? 1 : 0)}"; + positionText.Text = $"#{scorePosition.Value.FormatRank()}"; positionText.FadeTo(scorePosition.HasValue ? 1 : 0); - positionSymbol.FadeTo(scorePosition.HasValue ? 1 : 0); + updateColour(); } } - private double totalScore; + public User User { get; } - public double TotalScore + private readonly bool trackedPlayer; + + private Container mainFillContainer; + private Box centralFill; + + /// + /// Creates a new . + /// + /// The score's player. + /// Whether the player is the local user or a replay player. + public GameplayLeaderboardScore(User user, bool trackedPlayer) { - get => totalScore; - set - { - totalScore = value; - scoreText.Text = totalScore.ToString("N0"); + User = user; + this.trackedPlayer = trackedPlayer; - OnScoreChange?.Invoke(); - } - } - - private User user; - - public User User - { - get => user; - set - { - user = value; - userString.Text = user?.Username; - } - } - - public GameplayLeaderboardScore() - { - RelativeSizeAxes = Axes.X; - AutoSizeAxes = Axes.Y; - - InternalChild = new Container - { - Masking = true, - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - AutoSizeAxes = Axes.Both, - Children = new Drawable[] - { - new FillFlowContainer - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopRight, - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Margin = new MarginPadding { Right = 2.5f }, - Spacing = new Vector2(2.5f), - Children = new[] - { - positionText = new OsuSpriteText - { - Alpha = 0, - Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold), - }, - positionSymbol = new OsuSpriteText - { - Alpha = 0, - Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold), - Text = ">", - }, - } - }, - new FillFlowContainer - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopLeft, - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Margin = new MarginPadding { Left = 2.5f }, - Spacing = new Vector2(2.5f), - Children = new Drawable[] - { - userString = new OsuSpriteText - { - Size = new Vector2(80, 16), - Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold, italics: true), - }, - scoreText = new GlowingSpriteText - { - GlowColour = Color4Extensions.FromHex(@"83ccfa"), - Font = OsuFont.Numeric.With(size: 14), - } - } - }, - }, - }; + Size = new Vector2(EXTENDED_WIDTH, PANEL_HEIGHT); } [BackgroundDependencyLoader] private void load(OsuColour colours) { - positionText.Colour = colours.YellowLight; - positionSymbol.Colour = colours.Yellow; + Container avatarContainer; + + InternalChildren = new Drawable[] + { + mainFillContainer = new Container + { + Width = regular_width, + RelativeSizeAxes = Axes.Y, + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Masking = true, + CornerRadius = 5f, + Shear = new Vector2(panel_shear, 0f), + Child = new Box + { + Alpha = 0.5f, + RelativeSizeAxes = Axes.Both, + } + }, + new GridContainer + { + Width = regular_width, + RelativeSizeAxes = Axes.Y, + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.Absolute, 35f), + new Dimension(), + new Dimension(GridSizeMode.Absolute, 85f), + }, + Content = new[] + { + new Drawable[] + { + positionText = new OsuSpriteText + { + Padding = new MarginPadding { Right = SHEAR_WIDTH / 2 }, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Colour = Color4.White, + Font = OsuFont.Torus.With(size: 14, weight: FontWeight.Bold), + Shadow = false, + }, + new Container + { + Padding = new MarginPadding { Horizontal = SHEAR_WIDTH / 3 }, + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Container + { + Masking = true, + CornerRadius = 5f, + Shear = new Vector2(panel_shear, 0f), + RelativeSizeAxes = Axes.Both, + Children = new[] + { + centralFill = new Box + { + Alpha = 0.5f, + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex("3399cc"), + }, + } + }, + new FillFlowContainer + { + Padding = new MarginPadding { Left = SHEAR_WIDTH }, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + RelativeSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(4f, 0f), + Children = new Drawable[] + { + avatarContainer = new CircularContainer + { + Masking = true, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Size = new Vector2(25f), + Children = new Drawable[] + { + new Box + { + Name = "Placeholder while avatar loads", + Alpha = 0.3f, + RelativeSizeAxes = Axes.Both, + Colour = colours.Gray4, + } + } + }, + usernameText = new OsuSpriteText + { + RelativeSizeAxes = Axes.X, + Width = 0.6f, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Colour = Color4.White, + Font = OsuFont.Torus.With(size: 14, weight: FontWeight.SemiBold), + Text = User.Username, + Truncate = true, + Shadow = false, + } + } + }, + } + }, + new Container + { + Padding = new MarginPadding { Top = 2f, Right = 17.5f, Bottom = 5f }, + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Colour = Color4.White, + Children = new Drawable[] + { + scoreText = new OsuSpriteText + { + Spacing = new Vector2(-1f, 0f), + Font = OsuFont.Torus.With(size: 16, weight: FontWeight.SemiBold, fixedWidth: true), + Shadow = false, + }, + accuracyText = new OsuSpriteText + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Font = OsuFont.Torus.With(size: 12, weight: FontWeight.SemiBold, fixedWidth: true), + Spacing = new Vector2(-1f, 0f), + Shadow = false, + }, + comboText = new OsuSpriteText + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + Spacing = new Vector2(-1f, 0f), + Font = OsuFont.Torus.With(size: 12, weight: FontWeight.SemiBold, fixedWidth: true), + Shadow = false, + }, + }, + } + } + } + } + }; + + LoadComponentAsync(new DrawableAvatar(User), avatarContainer.Add); + + TotalScore.BindValueChanged(v => scoreText.Text = v.NewValue.ToString("N0"), true); + Accuracy.BindValueChanged(v => accuracyText.Text = v.NewValue.FormatAccuracy(), true); + Combo.BindValueChanged(v => comboText.Text = $"{v.NewValue}x", true); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + updateColour(); + FinishTransforms(true); + } + + private const double panel_transition_duration = 500; + + private void updateColour() + { + if (scorePosition == 1) + { + mainFillContainer.ResizeWidthTo(EXTENDED_WIDTH, panel_transition_duration, Easing.OutElastic); + panelColour = Color4Extensions.FromHex("7fcc33"); + textColour = Color4.White; + } + else if (trackedPlayer) + { + mainFillContainer.ResizeWidthTo(EXTENDED_WIDTH, panel_transition_duration, Easing.OutElastic); + panelColour = Color4Extensions.FromHex("ffd966"); + textColour = Color4Extensions.FromHex("2e576b"); + } + else + { + mainFillContainer.ResizeWidthTo(regular_width, panel_transition_duration, Easing.OutElastic); + panelColour = Color4Extensions.FromHex("3399cc"); + textColour = Color4.White; + } + } + + private Color4 panelColour + { + set + { + mainFillContainer.FadeColour(value, panel_transition_duration, Easing.OutQuint); + centralFill.FadeColour(value, panel_transition_duration, Easing.OutQuint); + } + } + + private const double text_transition_duration = 200; + + private Color4 textColour + { + set + { + scoreText.FadeColour(value, text_transition_duration, Easing.OutQuint); + accuracyText.FadeColour(value, text_transition_duration, Easing.OutQuint); + comboText.FadeColour(value, text_transition_duration, Easing.OutQuint); + usernameText.FadeColour(value, text_transition_duration, Easing.OutQuint); + positionText.FadeColour(value, text_transition_duration, Easing.OutQuint); + } } } } diff --git a/osu.Game/Screens/Play/HUD/ILeaderboardScore.cs b/osu.Game/Screens/Play/HUD/ILeaderboardScore.cs new file mode 100644 index 0000000000..bc1a03c5aa --- /dev/null +++ b/osu.Game/Screens/Play/HUD/ILeaderboardScore.cs @@ -0,0 +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 osu.Framework.Bindables; + +namespace osu.Game.Screens.Play.HUD +{ + public interface ILeaderboardScore + { + BindableDouble TotalScore { get; } + BindableDouble Accuracy { get; } + BindableInt Combo { get; } + } +} diff --git a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs new file mode 100644 index 0000000000..c10ec9e004 --- /dev/null +++ b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs @@ -0,0 +1,131 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using JetBrains.Annotations; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Game.Configuration; +using osu.Game.Database; +using osu.Game.Online.API; +using osu.Game.Online.Spectator; +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Screens.Play.HUD +{ + [LongRunningLoad] + public class MultiplayerGameplayLeaderboard : GameplayLeaderboard + { + private readonly ScoreProcessor scoreProcessor; + + private readonly int[] userIds; + + private readonly Dictionary userScores = new Dictionary(); + + /// + /// Construct a new leaderboard. + /// + /// A score processor instance to handle score calculation for scores of users in the match. + /// IDs of all users in this match. + public MultiplayerGameplayLeaderboard(ScoreProcessor scoreProcessor, int[] userIds) + { + // todo: this will eventually need to be created per user to support different mod combinations. + this.scoreProcessor = scoreProcessor; + + // todo: this will likely be passed in as User instances. + this.userIds = userIds; + } + + [Resolved] + private SpectatorStreamingClient streamingClient { get; set; } + + [Resolved] + private UserLookupCache userLookupCache { get; set; } + + private Bindable scoringMode; + + [BackgroundDependencyLoader] + private void load(OsuConfigManager config, IAPIProvider api) + { + streamingClient.OnNewFrames += handleIncomingFrames; + + foreach (var user in userIds) + { + streamingClient.WatchUser(user); + + // probably won't be required in the final implementation. + var resolvedUser = userLookupCache.GetUserAsync(user).Result; + + var trackedUser = new TrackedUserData(); + + userScores[user] = trackedUser; + var leaderboardScore = AddPlayer(resolvedUser, resolvedUser.Id == api.LocalUser.Value.Id); + + ((IBindable)leaderboardScore.Accuracy).BindTo(trackedUser.Accuracy); + ((IBindable)leaderboardScore.TotalScore).BindTo(trackedUser.Score); + ((IBindable)leaderboardScore.Combo).BindTo(trackedUser.CurrentCombo); + } + + scoringMode = config.GetBindable(OsuSetting.ScoreDisplayMode); + scoringMode.BindValueChanged(updateAllScores, true); + } + + private void updateAllScores(ValueChangedEvent mode) + { + foreach (var trackedData in userScores.Values) + trackedData.UpdateScore(scoreProcessor, mode.NewValue); + } + + private void handleIncomingFrames(int userId, FrameDataBundle bundle) + { + if (userScores.TryGetValue(userId, out var trackedData)) + { + trackedData.LastHeader = bundle.Header; + trackedData.UpdateScore(scoreProcessor, scoringMode.Value); + } + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (streamingClient != null) + { + foreach (var user in userIds) + { + streamingClient.StopWatchingUser(user); + } + + streamingClient.OnNewFrames -= handleIncomingFrames; + } + } + + private class TrackedUserData + { + public IBindableNumber Score => score; + + private readonly BindableDouble score = new BindableDouble(); + + public IBindableNumber Accuracy => accuracy; + + private readonly BindableDouble accuracy = new BindableDouble(1); + + public IBindableNumber CurrentCombo => currentCombo; + + private readonly BindableInt currentCombo = new BindableInt(); + + [CanBeNull] + public FrameHeader LastHeader; + + public void UpdateScore(ScoreProcessor processor, ScoringMode mode) + { + if (LastHeader == null) + return; + + score.Value = processor.GetImmediateScore(mode, LastHeader.MaxCombo, LastHeader.Statistics); + accuracy.Value = LastHeader.Accuracy; + currentCombo.Value = LastHeader.Combo; + } + } + } +} diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 50195d571c..3dffab8102 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -28,6 +28,11 @@ namespace osu.Game.Screens.Play public const Easing FADE_EASING = Easing.Out; + /// + /// The total height of all the top of screen scoring elements. + /// + public float TopScoringElementsHeight { get; private set; } + public readonly KeyCounterDisplay KeyCounter; public readonly SkinnableComboCounter ComboCounter; public readonly SkinnableScoreCounter ScoreCounter; @@ -209,7 +214,7 @@ namespace osu.Game.Screens.Play // HACK: for now align with the accuracy counter. // this is done for the sake of hacky legacy skins which extend the health bar to take up the full screen area. // it only works with the default skin due to padding offsetting it *just enough* to coexist. - topRightElements.Y = ToLocalSpace(AccuracyCounter.Drawable.ScreenSpaceDrawQuad.BottomRight).Y; + topRightElements.Y = TopScoringElementsHeight = ToLocalSpace(AccuracyCounter.Drawable.ScreenSpaceDrawQuad.BottomRight).Y; bottomRightElements.Y = -Progress.Height; } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index a54f9fc047..bf2e6f5379 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading.Tasks; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Audio; @@ -22,8 +23,10 @@ using osu.Game.Graphics.Containers; using osu.Game.IO.Archives; using osu.Game.Online.API; using osu.Game.Overlays; +using osu.Game.Replays; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; using osu.Game.Scoring; @@ -125,18 +128,14 @@ namespace osu.Game.Screens.Play ///
protected virtual bool CheckModsAllowFailure() => Mods.Value.OfType().All(m => m.PerformFail()); - private readonly bool allowPause; - private readonly bool showResults; + public readonly PlayerConfiguration Configuration; /// /// Create a new player instance. /// - /// Whether pausing should be allowed. If not allowed, attempting to pause will quit. - /// Whether results screen should be pushed on completion. - public Player(bool allowPause = true, bool showResults = true) + public Player(PlayerConfiguration configuration = null) { - this.allowPause = allowPause; - this.showResults = showResults; + Configuration = configuration ?? new PlayerConfiguration(); } private GameplayBeatmap gameplayBeatmap; @@ -314,59 +313,80 @@ namespace osu.Game.Screens.Play } }; - private Drawable createOverlayComponents(WorkingBeatmap working) => new Container + private Drawable createOverlayComponents(WorkingBeatmap working) { - RelativeSizeAxes = Axes.Both, - Children = new[] + var container = new Container { - DimmableStoryboard.OverlayLayerContainer.CreateProxy(), - BreakOverlay = new BreakOverlay(working.Beatmap.BeatmapInfo.LetterboxInBreaks, ScoreProcessor) + RelativeSizeAxes = Axes.Both, + Children = new[] { - Clock = DrawableRuleset.FrameStableClock, - ProcessCustomClock = false, - Breaks = working.Beatmap.Breaks - }, - // display the cursor above some HUD elements. - DrawableRuleset.Cursor?.CreateProxy() ?? new Container(), - DrawableRuleset.ResumeOverlay?.CreateProxy() ?? new Container(), - HUDOverlay = new HUDOverlay(ScoreProcessor, HealthProcessor, DrawableRuleset, Mods.Value) - { - HoldToQuit = + DimmableStoryboard.OverlayLayerContainer.CreateProxy(), + BreakOverlay = new BreakOverlay(working.Beatmap.BeatmapInfo.LetterboxInBreaks, ScoreProcessor) { - Action = performUserRequestedExit, - IsPaused = { BindTarget = GameplayClockContainer.IsPaused } + Clock = DrawableRuleset.FrameStableClock, + ProcessCustomClock = false, + Breaks = working.Beatmap.Breaks }, - PlayerSettingsOverlay = { PlaybackSettings = { UserPlaybackRate = { BindTarget = GameplayClockContainer.UserPlaybackRate } } }, - KeyCounter = + // display the cursor above some HUD elements. + DrawableRuleset.Cursor?.CreateProxy() ?? new Container(), + DrawableRuleset.ResumeOverlay?.CreateProxy() ?? new Container(), + HUDOverlay = new HUDOverlay(ScoreProcessor, HealthProcessor, DrawableRuleset, Mods.Value) { - AlwaysVisible = { BindTarget = DrawableRuleset.HasReplayLoaded }, - IsCounting = false + HoldToQuit = + { + Action = performUserRequestedExit, + IsPaused = { BindTarget = GameplayClockContainer.IsPaused } + }, + PlayerSettingsOverlay = { PlaybackSettings = { UserPlaybackRate = { BindTarget = GameplayClockContainer.UserPlaybackRate } } }, + KeyCounter = + { + AlwaysVisible = { BindTarget = DrawableRuleset.HasReplayLoaded }, + IsCounting = false + }, + RequestSeek = time => + { + GameplayClockContainer.Seek(time); + GameplayClockContainer.Start(); + }, + Anchor = Anchor.Centre, + Origin = Anchor.Centre }, - RequestSeek = time => + skipOverlay = new SkipOverlay(DrawableRuleset.GameplayStartTime) { - GameplayClockContainer.Seek(time); - GameplayClockContainer.Start(); + RequestSkip = GameplayClockContainer.Skip }, - Anchor = Anchor.Centre, - Origin = Anchor.Centre - }, - skipOverlay = new SkipOverlay(DrawableRuleset.GameplayStartTime) - { - RequestSkip = GameplayClockContainer.Skip - }, - FailOverlay = new FailOverlay - { - OnRetry = Restart, - OnQuit = performUserRequestedExit, - }, - PauseOverlay = new PauseOverlay - { - OnResume = Resume, - Retries = RestartCount, - OnRetry = Restart, - OnQuit = performUserRequestedExit, - }, - new HotkeyRetryOverlay + FailOverlay = new FailOverlay + { + OnRetry = Restart, + OnQuit = performUserRequestedExit, + }, + PauseOverlay = new PauseOverlay + { + OnResume = Resume, + Retries = RestartCount, + OnRetry = Restart, + OnQuit = performUserRequestedExit, + }, + new HotkeyExitOverlay + { + Action = () => + { + if (!this.IsCurrentScreen()) return; + + fadeOut(true); + PerformExit(true); + }, + }, + failAnimation = new FailAnimation(DrawableRuleset) { OnComplete = onFailComplete, }, + } + }; + + if (!Configuration.AllowSkippingIntro) + skipOverlay.Expire(); + + if (Configuration.AllowRestart) + { + container.Add(new HotkeyRetryOverlay { Action = () => { @@ -375,20 +395,11 @@ namespace osu.Game.Screens.Play fadeOut(true); Restart(); }, - }, - new HotkeyExitOverlay - { - Action = () => - { - if (!this.IsCurrentScreen()) return; - - fadeOut(true); - performImmediateExit(); - }, - }, - failAnimation = new FailAnimation(DrawableRuleset) { OnComplete = onFailComplete, }, + }); } - }; + + return container; + } private void onBreakTimeChanged(ValueChangedEvent isBreakTime) { @@ -455,20 +466,30 @@ namespace osu.Game.Screens.Play return playable; } - private void performImmediateExit() + /// + /// Exits the . + /// + /// + /// Whether the exit is requested by the user, or a higher-level game component. + /// Pausing is allowed only in the former case. + /// + protected void PerformExit(bool userRequested) { // if a restart has been requested, cancel any pending completion (user has shown intent to restart). completionProgressDelegate?.Cancel(); ValidForResume = false; - performUserRequestedExit(); + if (!this.IsCurrentScreen()) return; + + if (userRequested) + performUserRequestedExit(); + else + this.Exit(); } private void performUserRequestedExit() { - if (!this.IsCurrentScreen()) return; - if (ValidForResume && HasFailed && !FailOverlay.IsPresent) { failAnimation.FinishTransforms(true); @@ -487,6 +508,9 @@ namespace osu.Game.Screens.Play ///
public void Restart() { + if (!Configuration.AllowRestart) + return; + // at the point of restarting the track should either already be paused or the volume should be zero. // stopping here is to ensure music doesn't become audible after exiting back to PlayerLoader. musicController.Stop(); @@ -495,12 +519,13 @@ namespace osu.Game.Screens.Play RestartRequested?.Invoke(); if (this.IsCurrentScreen()) - performImmediateExit(); + PerformExit(true); else this.MakeCurrent(); } private ScheduledDelegate completionProgressDelegate; + private Task scoreSubmissionTask; private void updateCompletionState(ValueChangedEvent completionState) { @@ -525,35 +550,52 @@ namespace osu.Game.Screens.Play ValidForResume = false; - if (!showResults) return; + if (!Configuration.ShowResults) return; + + scoreSubmissionTask ??= Task.Run(async () => + { + var score = CreateScore(); + + try + { + await SubmitScore(score); + } + catch (Exception ex) + { + Logger.Error(ex, "Score submission failed!"); + } + + try + { + await ImportScore(score); + } + catch (Exception ex) + { + Logger.Error(ex, "Score import failed!"); + } + + return score.ScoreInfo; + }); using (BeginDelayedSequence(RESULTS_DISPLAY_DELAY)) - completionProgressDelegate = Schedule(GotoRanking); + scheduleCompletion(); } - protected virtual ScoreInfo CreateScore() + private void scheduleCompletion() => completionProgressDelegate = Schedule(() => { - var score = new ScoreInfo + if (!scoreSubmissionTask.IsCompleted) { - Beatmap = Beatmap.Value.BeatmapInfo, - Ruleset = rulesetInfo, - Mods = Mods.Value.ToArray(), - }; + scheduleCompletion(); + return; + } - if (DrawableRuleset.ReplayScore != null) - score.User = DrawableRuleset.ReplayScore.ScoreInfo?.User ?? new GuestUser(); - else - score.User = api.LocalUser.Value; - - ScoreProcessor.PopulateScore(score); - - return score; - } + // screen may be in the exiting transition phase. + if (this.IsCurrentScreen()) + this.Push(CreateResults(scoreSubmissionTask.Result)); + }); protected override bool OnScroll(ScrollEvent e) => mouseWheelDisabled.Value && !GameplayClockContainer.IsPaused.Value; - protected virtual ResultsScreen CreateResults(ScoreInfo score) => new SoloResultsScreen(score, true); - #region Fail Logic protected FailOverlay FailOverlay { get; private set; } @@ -607,7 +649,7 @@ namespace osu.Game.Screens.Play private bool canPause => // must pass basic screen conditions (beatmap loaded, instance allows pause) - LoadedBeatmapSuccessfully && allowPause && ValidForResume + LoadedBeatmapSuccessfully && Configuration.AllowPause && ValidForResume // replays cannot be paused and exit immediately && !DrawableRuleset.HasReplayLoaded.Value // cannot pause if we are already in a fail state @@ -692,9 +734,6 @@ namespace osu.Game.Screens.Play storyboardReplacesBackground.Value = Beatmap.Value.Storyboard.ReplacesBackground && Beatmap.Value.Storyboard.HasDrawable; - GameplayClockContainer.Restart(); - GameplayClockContainer.FadeInFromZero(750, Easing.OutQuint); - foreach (var mod in Mods.Value.OfType()) mod.ApplyToPlayer(this); @@ -709,6 +748,21 @@ namespace osu.Game.Screens.Play mod.ApplyToTrack(musicController.CurrentTrack); updateGameplayState(); + + GameplayClockContainer.FadeInFromZero(750, Easing.OutQuint); + StartGameplay(); + } + + /// + /// Called to trigger the starting of the gameplay clock and underlying gameplay. + /// This will be called on entering the player screen once. A derived class may block the first call to this to delay the start of gameplay. + /// + protected virtual void StartGameplay() + { + if (GameplayClockContainer.GameplayClock.IsRunning) + throw new InvalidOperationException($"{nameof(StartGameplay)} should not be called when the gameplay clock is already running"); + + GameplayClockContainer.Restart(); } public override void OnSuspending(IScreen next) @@ -748,39 +802,74 @@ namespace osu.Game.Screens.Play return base.OnExiting(next); } - protected virtual void GotoRanking() + /// + /// Creates the player's . + /// + /// The . + protected virtual Score CreateScore() { + var score = new Score + { + ScoreInfo = new ScoreInfo + { + Beatmap = Beatmap.Value.BeatmapInfo, + Ruleset = rulesetInfo, + Mods = Mods.Value.ToArray(), + } + }; + if (DrawableRuleset.ReplayScore != null) { - // if a replay is present, we likely don't want to import into the local database. - this.Push(CreateResults(CreateScore())); - return; + score.ScoreInfo.User = DrawableRuleset.ReplayScore.ScoreInfo?.User ?? new GuestUser(); + score.Replay = DrawableRuleset.ReplayScore.Replay; } - - LegacyByteArrayReader replayReader = null; - - var score = new Score { ScoreInfo = CreateScore() }; - - if (recordingScore?.Replay.Frames.Count > 0) + else { - score.Replay = recordingScore.Replay; - - using (var stream = new MemoryStream()) - { - new LegacyScoreEncoder(score, gameplayBeatmap.PlayableBeatmap).Encode(stream); - replayReader = new LegacyByteArrayReader(stream.ToArray(), "replay.osr"); - } + score.ScoreInfo.User = api.LocalUser.Value; + score.Replay = new Replay { Frames = recordingScore?.Replay.Frames.ToList() ?? new List() }; } - scoreManager.Import(score.ScoreInfo, replayReader) - .ContinueWith(imported => Schedule(() => - { - // screen may be in the exiting transition phase. - if (this.IsCurrentScreen()) - this.Push(CreateResults(imported.Result)); - })); + ScoreProcessor.PopulateScore(score.ScoreInfo); + + return score; } + /// + /// Imports the player's to the local database. + /// + /// The to import. + /// The imported score. + protected virtual Task ImportScore(Score score) + { + // Replays are already populated and present in the game's database, so should not be re-imported. + if (DrawableRuleset.ReplayScore != null) + return Task.CompletedTask; + + LegacyByteArrayReader replayReader; + + using (var stream = new MemoryStream()) + { + new LegacyScoreEncoder(score, gameplayBeatmap.PlayableBeatmap).Encode(stream); + replayReader = new LegacyByteArrayReader(stream.ToArray(), "replay.osr"); + } + + return scoreManager.Import(score.ScoreInfo, replayReader); + } + + /// + /// Submits the player's . + /// + /// The to submit. + /// The submitted score. + protected virtual Task SubmitScore(Score score) => Task.CompletedTask; + + /// + /// Creates the for a . + /// + /// The to be displayed in the results screen. + /// The . + protected virtual ResultsScreen CreateResults(ScoreInfo score) => new SoloResultsScreen(score, true); + private void fadeOut(bool instant = false) { float fadeOutDuration = instant ? 0 : 250; diff --git a/osu.Game/Screens/Play/PlayerConfiguration.cs b/osu.Game/Screens/Play/PlayerConfiguration.cs new file mode 100644 index 0000000000..cd30ead638 --- /dev/null +++ b/osu.Game/Screens/Play/PlayerConfiguration.cs @@ -0,0 +1,28 @@ +// 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.Play +{ + public class PlayerConfiguration + { + /// + /// Whether pausing should be allowed. If not allowed, attempting to pause will quit. + /// + public bool AllowPause { get; set; } = true; + + /// + /// Whether results screen should be pushed on completion. + /// + public bool ShowResults { get; set; } = true; + + /// + /// Whether the player should be allowed to trigger a restart. + /// + public bool AllowRestart { get; set; } = true; + + /// + /// Whether the player should be allowed to skip the intro, advancing to the start of gameplay. + /// + public bool AllowSkippingIntro { get; set; } = true; + } +} diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index 294d116f51..e23cc22929 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.cs @@ -1,6 +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 System.Threading.Tasks; using osu.Framework.Input.Bindings; using osu.Game.Input.Bindings; using osu.Game.Scoring; @@ -15,8 +16,8 @@ namespace osu.Game.Screens.Play // Disallow replays from failing. (see https://github.com/ppy/osu/issues/6108) protected override bool CheckModsAllowFailure() => false; - public ReplayPlayer(Score score, bool allowPause = true, bool showResults = true) - : base(allowPause, showResults) + public ReplayPlayer(Score score, PlayerConfiguration configuration = null) + : base(configuration) { Score = score; } @@ -26,18 +27,21 @@ namespace osu.Game.Screens.Play DrawableRuleset?.SetReplayScore(Score); } - protected override ResultsScreen CreateResults(ScoreInfo score) => new SoloResultsScreen(score, false); - - protected override ScoreInfo CreateScore() + protected override Score CreateScore() { var baseScore = base.CreateScore(); // Since the replay score doesn't contain statistics, we'll pass them through here. - Score.ScoreInfo.HitEvents = baseScore.HitEvents; + Score.ScoreInfo.HitEvents = baseScore.ScoreInfo.HitEvents; - return Score.ScoreInfo; + return Score; } + // Don't re-import replay scores as they're already present in the database. + protected override Task ImportScore(Score score) => Task.CompletedTask; + + protected override ResultsScreen CreateResults(ScoreInfo score) => new SoloResultsScreen(score, false); + public bool OnPressed(GlobalAction action) { switch (action) diff --git a/osu.Game/Screens/Play/Spectator.cs b/osu.Game/Screens/Play/Spectator.cs index 71ce157296..28311f5113 100644 --- a/osu.Game/Screens/Play/Spectator.cs +++ b/osu.Game/Screens/Play/Spectator.cs @@ -30,7 +30,7 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Replays.Types; using osu.Game.Scoring; -using osu.Game.Screens.Multi.Match.Components; +using osu.Game.Screens.OnlinePlay.Match.Components; using osu.Game.Users; using osuTK; diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 887e7ec8a9..528a1842af 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -57,11 +57,13 @@ namespace osu.Game.Screens.Ranking private APIRequest nextPageRequest; private readonly bool allowRetry; + private readonly bool allowWatchingReplay; - protected ResultsScreen(ScoreInfo score, bool allowRetry) + protected ResultsScreen(ScoreInfo score, bool allowRetry, bool allowWatchingReplay = true) { Score = score; this.allowRetry = allowRetry; + this.allowWatchingReplay = allowWatchingReplay; SelectedScore.Value = score; } @@ -128,15 +130,7 @@ namespace osu.Game.Screens.Ranking Origin = Anchor.Centre, AutoSizeAxes = Axes.Both, Spacing = new Vector2(5), - Direction = FillDirection.Horizontal, - Children = new Drawable[] - { - new ReplayDownloadButton(null) - { - Score = { BindTarget = SelectedScore }, - Width = 300 - }, - } + Direction = FillDirection.Horizontal } } } @@ -157,6 +151,15 @@ namespace osu.Game.Screens.Ranking ScorePanelList.AddScore(Score, shouldFlair); } + if (allowWatchingReplay) + { + buttons.Add(new ReplayDownloadButton(null) + { + Score = { BindTarget = SelectedScore }, + Width = 300 + }); + } + if (player != null && allowRetry) { buttons.Add(new RetryButton { Width = 300 }); diff --git a/osu.Game/Screens/Select/MatchSongSelect.cs b/osu.Game/Screens/Select/MatchSongSelect.cs index 8692833a21..0948a4d19a 100644 --- a/osu.Game/Screens/Select/MatchSongSelect.cs +++ b/osu.Game/Screens/Select/MatchSongSelect.cs @@ -9,13 +9,13 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Screens; using osu.Game.Beatmaps; -using osu.Game.Online.Multiplayer; -using osu.Game.Screens.Multi; -using osu.Game.Screens.Multi.Components; +using osu.Game.Online.Rooms; +using osu.Game.Screens.OnlinePlay; +using osu.Game.Screens.OnlinePlay.Components; namespace osu.Game.Screens.Select { - public class MatchSongSelect : SongSelect, IMultiplayerSubScreen + public class MatchSongSelect : SongSelect, IOnlinePlaySubScreen { public Action Selected; diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index f32011a27a..a5252fdc96 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -80,8 +80,6 @@ namespace osu.Game.Screens.Select protected BeatmapCarousel Carousel { get; private set; } - private readonly DifficultyRecommender recommender = new DifficultyRecommender(); - private BeatmapInfoWedge beatmapInfoWedge; private DialogOverlay dialogOverlay; @@ -105,7 +103,7 @@ namespace osu.Game.Screens.Select private MusicController music { get; set; } [BackgroundDependencyLoader(true)] - private void load(AudioManager audio, DialogOverlay dialog, OsuColour colours, SkinManager skins, ScoreManager scores, CollectionManager collections, ManageCollectionsDialog manageCollectionsDialog) + private void load(AudioManager audio, DialogOverlay dialog, OsuColour colours, SkinManager skins, ScoreManager scores, CollectionManager collections, ManageCollectionsDialog manageCollectionsDialog, DifficultyRecommender recommender) { // initial value transfer is required for FilterControl (it uses our re-cached bindables in its async load for the initial filter). transferRulesetValue(); @@ -120,12 +118,11 @@ namespace osu.Game.Screens.Select BleedBottom = Footer.HEIGHT, SelectionChanged = updateSelectedBeatmap, BeatmapSetsChanged = carouselBeatmapsLoaded, - GetRecommendedBeatmap = recommender.GetRecommendedBeatmap, + GetRecommendedBeatmap = s => recommender?.GetRecommendedBeatmap(s), }, c => carouselContainer.Child = c); AddRangeInternal(new Drawable[] { - recommender, new ResetScrollContainer(() => Carousel.ScrollToSelected()) { RelativeSizeAxes = Axes.Y, diff --git a/osu.Game/Skinning/DefaultLegacySkin.cs b/osu.Game/Skinning/DefaultLegacySkin.cs index 2758a4cbba..4027cc650d 100644 --- a/osu.Game/Skinning/DefaultLegacySkin.cs +++ b/osu.Game/Skinning/DefaultLegacySkin.cs @@ -1,16 +1,16 @@ // 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.Audio; using osu.Framework.IO.Stores; +using osu.Game.IO; using osuTK.Graphics; namespace osu.Game.Skinning { public class DefaultLegacySkin : LegacySkin { - public DefaultLegacySkin(IResourceStore storage, AudioManager audioManager) - : base(Info, storage, audioManager, string.Empty) + public DefaultLegacySkin(IResourceStore storage, IStorageResourceProvider resources) + : base(Info, storage, resources, string.Empty) { Configuration.CustomColours["SliderBall"] = new Color4(2, 170, 255, 255); Configuration.AddComboColours( diff --git a/osu.Game/Skinning/LegacyBeatmapSkin.cs b/osu.Game/Skinning/LegacyBeatmapSkin.cs index d647bc4a2d..fdcb81b574 100644 --- a/osu.Game/Skinning/LegacyBeatmapSkin.cs +++ b/osu.Game/Skinning/LegacyBeatmapSkin.cs @@ -1,12 +1,12 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.IO.Stores; using osu.Game.Audio; using osu.Game.Beatmaps; +using osu.Game.IO; using osu.Game.Rulesets.Objects.Legacy; namespace osu.Game.Skinning @@ -16,8 +16,8 @@ namespace osu.Game.Skinning protected override bool AllowManiaSkin => false; protected override bool UseCustomSampleBanks => true; - public LegacyBeatmapSkin(BeatmapInfo beatmap, IResourceStore storage, AudioManager audioManager) - : base(createSkinInfo(beatmap), new LegacySkinResourceStore(beatmap.BeatmapSet, storage), audioManager, beatmap.Path) + public LegacyBeatmapSkin(BeatmapInfo beatmap, IResourceStore storage, IStorageResourceProvider resources) + : base(createSkinInfo(beatmap), new LegacySkinResourceStore(beatmap.BeatmapSet, storage), resources, beatmap.Path) { // Disallow default colours fallback on beatmap skins to allow using parent skin combo colours. (via SkinProvidingContainer) Configuration.AllowDefaultComboColoursFallback = false; diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 80b2fef35c..e4e5bf2f75 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -7,7 +7,6 @@ using System.Diagnostics; using System.IO; using System.Linq; using JetBrains.Annotations; -using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -54,12 +53,12 @@ namespace osu.Game.Skinning private readonly Dictionary maniaConfigurations = new Dictionary(); - public LegacySkin(SkinInfo skin, IResourceStore storage, AudioManager audioManager) - : this(skin, new LegacySkinResourceStore(skin, storage), audioManager, "skin.ini") + public LegacySkin(SkinInfo skin, IStorageResourceProvider resources) + : this(skin, new LegacySkinResourceStore(skin, resources.Files), resources, "skin.ini") { } - protected LegacySkin(SkinInfo skin, IResourceStore storage, AudioManager audioManager, string filename) + protected LegacySkin(SkinInfo skin, [CanBeNull] IResourceStore storage, [CanBeNull] IStorageResourceProvider resources, string filename) : base(skin) { using (var stream = storage?.GetStream(filename)) @@ -85,12 +84,12 @@ namespace osu.Game.Skinning if (storage != null) { - var samples = audioManager?.GetSampleStore(storage); + var samples = resources?.AudioManager?.GetSampleStore(storage); if (samples != null) samples.PlaybackConcurrency = OsuGameBase.SAMPLE_CONCURRENCY; Samples = samples; - Textures = new TextureStore(new TextureLoaderStore(storage)); + Textures = new TextureStore(resources?.CreateTextureLoaderStore(storage)); (storage as ResourceStore)?.AddExtension("ogg"); } diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index 9b69a1eecd..99c64b13a4 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -22,15 +22,18 @@ using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Audio; using osu.Game.Database; +using osu.Game.IO; using osu.Game.IO.Archives; namespace osu.Game.Skinning { [ExcludeFromDynamicCompile] - public class SkinManager : ArchiveModelManager, ISkinSource + public class SkinManager : ArchiveModelManager, ISkinSource, IStorageResourceProvider { private readonly AudioManager audio; + private readonly GameHost host; + private readonly IResourceStore legacyDefaultResources; public readonly Bindable CurrentSkin = new Bindable(new DefaultSkin()); @@ -42,10 +45,12 @@ namespace osu.Game.Skinning protected override string ImportFromStablePath => "Skins"; - public SkinManager(Storage storage, DatabaseContextFactory contextFactory, IIpcHost importHost, AudioManager audio, IResourceStore legacyDefaultResources) - : base(storage, contextFactory, new SkinStore(contextFactory, storage), importHost) + public SkinManager(Storage storage, DatabaseContextFactory contextFactory, GameHost host, AudioManager audio, IResourceStore legacyDefaultResources) + : base(storage, contextFactory, new SkinStore(contextFactory, storage), host) { this.audio = audio; + this.host = host; + this.legacyDefaultResources = legacyDefaultResources; CurrentSkinInfo.ValueChanged += skin => CurrentSkin.Value = GetSkin(skin.NewValue); @@ -148,9 +153,9 @@ namespace osu.Game.Skinning return new DefaultSkin(); if (skinInfo == DefaultLegacySkin.Info) - return new DefaultLegacySkin(legacyDefaultResources, audio); + return new DefaultLegacySkin(legacyDefaultResources, this); - return new LegacySkin(skinInfo, Files.Store, audio); + return new LegacySkin(skinInfo, this); } /// @@ -169,5 +174,13 @@ namespace osu.Game.Skinning public SampleChannel GetSample(ISampleInfo sampleInfo) => CurrentSkin.Value.GetSample(sampleInfo); public IBindable GetConfig(TLookup lookup) => CurrentSkin.Value.GetConfig(lookup); + + #region IResourceStorageProvider + + AudioManager IStorageResourceProvider.AudioManager => audio; + IResourceStore IStorageResourceProvider.Files => Files.Store; + IResourceStore IStorageResourceProvider.CreateTextureLoaderStore(IResourceStore underlyingStore) => host.CreateTextureLoaderStore(underlyingStore); + + #endregion } } diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs index 4bc28e6cef..af4615c895 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs @@ -8,6 +8,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Textures; +using osu.Framework.Platform; using osu.Game.IO; using osu.Game.Screens.Play; @@ -59,12 +60,12 @@ namespace osu.Game.Storyboards.Drawables } [BackgroundDependencyLoader(true)] - private void load(FileStore fileStore, GameplayClock clock, CancellationToken? cancellationToken) + private void load(FileStore fileStore, GameplayClock clock, CancellationToken? cancellationToken, GameHost host) { if (clock != null) Clock = clock; - dependencies.Cache(new TextureStore(new TextureLoaderStore(fileStore.Store), false, scaleAdjust: 1)); + dependencies.Cache(new TextureStore(host.CreateTextureLoaderStore(fileStore.Store), false, scaleAdjust: 1)); foreach (var layer in Storyboard.Layers) { diff --git a/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs b/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs index e3557222d5..62814d4ed4 100644 --- a/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs +++ b/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Audio; +using osu.Framework.Graphics.Textures; using osu.Framework.IO.Stores; using osu.Framework.Testing; using osu.Framework.Timing; @@ -25,7 +26,7 @@ using osu.Game.Users; namespace osu.Game.Tests.Beatmaps { [HeadlessTest] - public abstract class HitObjectSampleTest : PlayerTestScene + public abstract class HitObjectSampleTest : PlayerTestScene, IStorageResourceProvider { protected abstract IResourceStore Resources { get; } protected LegacySkin Skin { get; private set; } @@ -58,7 +59,7 @@ namespace osu.Game.Tests.Beatmaps protected sealed override IBeatmap CreateBeatmap(RulesetInfo ruleset) => currentTestBeatmap; protected sealed override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) - => new TestWorkingBeatmap(beatmapInfo, beatmapSkinResourceStore, beatmap, storyboard, Clock, Audio); + => new TestWorkingBeatmap(beatmapInfo, beatmapSkinResourceStore, beatmap, storyboard, Clock, this); protected override TestPlayer CreatePlayer(Ruleset ruleset) => new TestPlayer(false); @@ -109,7 +110,7 @@ namespace osu.Game.Tests.Beatmaps }; // Need to refresh the cached skin source to refresh the skin resource store. - dependencies.SkinSource = new SkinProvidingContainer(Skin = new LegacySkin(userSkinInfo, userSkinResourceStore, Audio)); + dependencies.SkinSource = new SkinProvidingContainer(Skin = new LegacySkin(userSkinInfo, this)); }); } @@ -122,6 +123,14 @@ namespace osu.Game.Tests.Beatmaps protected void AssertNoLookup(string name) => AddAssert($"\"{name}\" not looked up", () => !beatmapSkinResourceStore.PerformedLookups.Contains(name) && !userSkinResourceStore.PerformedLookups.Contains(name)); + #region IResourceStorageProvider + + public AudioManager AudioManager => Audio; + public IResourceStore Files => userSkinResourceStore; + public IResourceStore CreateTextureLoaderStore(IResourceStore underlyingStore) => null; + + #endregion + private class SkinSourceDependencyContainer : IReadOnlyDependencyContainer { public ISkinSource SkinSource; @@ -191,14 +200,17 @@ namespace osu.Game.Tests.Beatmaps private readonly BeatmapInfo skinBeatmapInfo; private readonly IResourceStore resourceStore; - public TestWorkingBeatmap(BeatmapInfo skinBeatmapInfo, IResourceStore resourceStore, IBeatmap beatmap, Storyboard storyboard, IFrameBasedClock referenceClock, AudioManager audio) - : base(beatmap, storyboard, referenceClock, audio) + private readonly IStorageResourceProvider resources; + + public TestWorkingBeatmap(BeatmapInfo skinBeatmapInfo, IResourceStore resourceStore, IBeatmap beatmap, Storyboard storyboard, IFrameBasedClock referenceClock, IStorageResourceProvider resources) + : base(beatmap, storyboard, referenceClock, resources.AudioManager) { this.skinBeatmapInfo = skinBeatmapInfo; this.resourceStore = resourceStore; + this.resources = resources; } - protected override ISkin GetSkin() => new LegacyBeatmapSkin(skinBeatmapInfo, resourceStore, AudioManager); + protected override ISkin GetSkin() => new LegacyBeatmapSkin(skinBeatmapInfo, resourceStore, resources); } } } diff --git a/osu.Game/Tests/Visual/LegacySkinPlayerTestScene.cs b/osu.Game/Tests/Visual/LegacySkinPlayerTestScene.cs index 054f72400e..c186525757 100644 --- a/osu.Game/Tests/Visual/LegacySkinPlayerTestScene.cs +++ b/osu.Game/Tests/Visual/LegacySkinPlayerTestScene.cs @@ -3,7 +3,6 @@ using NUnit.Framework; using osu.Framework.Allocation; -using osu.Framework.Audio; using osu.Framework.IO.Stores; using osu.Game.Rulesets; using osu.Game.Skinning; @@ -18,9 +17,9 @@ namespace osu.Game.Tests.Visual protected override TestPlayer CreatePlayer(Ruleset ruleset) => new SkinProvidingPlayer(legacySkinSource); [BackgroundDependencyLoader] - private void load(AudioManager audio, OsuGameBase game) + private void load(OsuGameBase game, SkinManager skins) { - var legacySkin = new DefaultLegacySkin(new NamespacedResourceStore(game.Resources, "Skins/Legacy"), audio); + var legacySkin = new DefaultLegacySkin(new NamespacedResourceStore(game.Resources, "Skins/Legacy"), skins); legacySkinSource = new SkinProvidingContainer(legacySkin); } diff --git a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs new file mode 100644 index 0000000000..da0e39d965 --- /dev/null +++ b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs @@ -0,0 +1,50 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using 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.Screens.OnlinePlay; +using osu.Game.Screens.OnlinePlay.Lounge.Components; + +namespace osu.Game.Tests.Visual.Multiplayer +{ + public abstract class MultiplayerTestScene : RoomTestScene + { + [Cached(typeof(StatefulMultiplayerClient))] + public TestMultiplayerClient Client { get; } + + [Cached(typeof(IRoomManager))] + public TestMultiplayerRoomManager RoomManager { get; } + + [Cached] + public Bindable Filter { get; } + + protected override Container Content => content; + private readonly TestMultiplayerRoomContainer content; + + 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; + } + + [SetUp] + public new void Setup() => Schedule(() => + { + RoomManager.Schedule(() => RoomManager.PartRoom()); + + if (joinRoom) + RoomManager.Schedule(() => RoomManager.CreateRoom(Room)); + }); + } +} diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs new file mode 100644 index 0000000000..9a839c8d22 --- /dev/null +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -0,0 +1,119 @@ +// 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.Diagnostics; +using System.Linq; +using System.Threading.Tasks; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Game.Online.API; +using osu.Game.Online.Multiplayer; +using osu.Game.Users; + +namespace osu.Game.Tests.Visual.Multiplayer +{ + public class TestMultiplayerClient : StatefulMultiplayerClient + { + public override IBindable IsConnected => isConnected; + private readonly Bindable isConnected = new Bindable(true); + + [Resolved] + private IAPIProvider api { get; set; } = null!; + + public void Connect() => isConnected.Value = true; + + public void Disconnect() => isConnected.Value = false; + + public void AddUser(User user) => ((IMultiplayerClient)this).UserJoined(new MultiplayerRoomUser(user.Id) { User = user }); + + public void RemoveUser(User user) + { + Debug.Assert(Room != null); + ((IMultiplayerClient)this).UserLeft(new MultiplayerRoomUser(user.Id)); + + Schedule(() => + { + if (Room.Users.Any()) + TransferHost(Room.Users.First().UserID); + }); + } + + public void ChangeUserState(int userId, MultiplayerUserState newState) + { + Debug.Assert(Room != null); + + ((IMultiplayerClient)this).UserStateChanged(userId, newState); + + Schedule(() => + { + switch (newState) + { + case MultiplayerUserState.Loaded: + if (Room.Users.All(u => u.State != MultiplayerUserState.WaitingForLoad)) + { + foreach (var u in Room.Users.Where(u => u.State == MultiplayerUserState.Loaded)) + ChangeUserState(u.UserID, MultiplayerUserState.Playing); + + ((IMultiplayerClient)this).MatchStarted(); + } + + break; + + case MultiplayerUserState.FinishedPlay: + if (Room.Users.All(u => u.State != MultiplayerUserState.Playing)) + { + foreach (var u in Room.Users.Where(u => u.State == MultiplayerUserState.FinishedPlay)) + ChangeUserState(u.UserID, MultiplayerUserState.Results); + + ((IMultiplayerClient)this).ResultsReady(); + } + + break; + } + }); + } + + protected override Task JoinRoom(long roomId) + { + var user = new MultiplayerRoomUser(api.LocalUser.Value.Id) { User = api.LocalUser.Value }; + + var room = new MultiplayerRoom(roomId); + room.Users.Add(user); + + if (room.Users.Count == 1) + room.Host = user; + + return Task.FromResult(room); + } + + public override Task TransferHost(int userId) => ((IMultiplayerClient)this).HostChanged(userId); + + public override async Task ChangeSettings(MultiplayerRoomSettings settings) + { + Debug.Assert(Room != null); + + await ((IMultiplayerClient)this).SettingsChanged(settings); + + foreach (var user in Room.Users.Where(u => u.State == MultiplayerUserState.Ready)) + ChangeUserState(user.UserID, MultiplayerUserState.Idle); + } + + public override Task ChangeState(MultiplayerUserState newState) + { + ChangeUserState(api.LocalUser.Value.Id, newState); + return Task.CompletedTask; + } + + public override Task StartMatch() + { + Debug.Assert(Room != null); + + foreach (var user in Room.Users.Where(u => u.State == MultiplayerUserState.Ready)) + ChangeUserState(user.UserID, MultiplayerUserState.WaitingForLoad); + + return ((IMultiplayerClient)this).LoadRequested(); + } + } +} diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomContainer.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomContainer.cs new file mode 100644 index 0000000000..ad3e2f7105 --- /dev/null +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomContainer.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 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(StatefulMultiplayerClient))] + public readonly TestMultiplayerClient Client; + + [Cached(typeof(IRoomManager))] + public readonly TestMultiplayerRoomManager RoomManager; + + [Cached] + public readonly Bindable Filter = new Bindable(new FilterCriteria()); + + public TestMultiplayerRoomContainer() + { + RelativeSizeAxes = Axes.Both; + + AddRangeInternal(new Drawable[] + { + Client = new TestMultiplayerClient(), + RoomManager = new TestMultiplayerRoomManager(), + content = new Container { RelativeSizeAxes = Axes.Both } + }); + } + } +} diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs new file mode 100644 index 0000000000..5e12156f3c --- /dev/null +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs @@ -0,0 +1,116 @@ +// 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.API.Requests; +using osu.Game.Online.Rooms; +using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; +using osu.Game.Screens.OnlinePlay.Lounge.Components; +using osu.Game.Screens.OnlinePlay.Multiplayer; + +namespace osu.Game.Tests.Visual.Multiplayer +{ + public class TestMultiplayerRoomManager : MultiplayerRoomManager + { + [Resolved] + private IAPIProvider api { get; set; } + + [Resolved] + private OsuGameBase game { get; set; } + + [Cached] + public readonly Bindable Filter = new Bindable(new FilterCriteria()); + + private readonly List rooms = new List(); + + protected override void LoadComplete() + { + base.LoadComplete(); + + int currentScoreId = 0; + int currentRoomId = 0; + + ((DummyAPIAccess)api).HandleRequest = req => + { + switch (req) + { + case CreateRoomRequest createRoomRequest: + var createdRoom = new APICreatedRoom(); + + createdRoom.CopyFrom(createRoomRequest.Room); + createdRoom.RoomID.Value ??= currentRoomId++; + + rooms.Add(createdRoom); + createRoomRequest.TriggerSuccess(createdRoom); + break; + + case JoinRoomRequest joinRoomRequest: + joinRoomRequest.TriggerSuccess(); + break; + + case PartRoomRequest partRoomRequest: + partRoomRequest.TriggerSuccess(); + break; + + case GetRoomsRequest getRoomsRequest: + var roomsWithoutParticipants = new List(); + + foreach (var r in rooms) + { + var newRoom = new Room(); + + newRoom.CopyFrom(r); + newRoom.RecentParticipants.Clear(); + + roomsWithoutParticipants.Add(newRoom); + } + + getRoomsRequest.TriggerSuccess(roomsWithoutParticipants); + break; + + case GetRoomRequest getRoomRequest: + getRoomRequest.TriggerSuccess(rooms.Single(r => r.RoomID.Value == getRoomRequest.RoomId)); + break; + + case GetBeatmapSetRequest getBeatmapSetRequest: + var onlineReq = new GetBeatmapSetRequest(getBeatmapSetRequest.ID, getBeatmapSetRequest.Type); + onlineReq.Success += res => getBeatmapSetRequest.TriggerSuccess(res); + onlineReq.Failure += e => getBeatmapSetRequest.TriggerFailure(e); + + // Get the online API from the game's dependencies. + game.Dependencies.Get().Queue(onlineReq); + break; + + case CreateRoomScoreRequest createRoomScoreRequest: + createRoomScoreRequest.TriggerSuccess(new APIScoreToken { ID = 1 }); + break; + + case SubmitRoomScoreRequest submitRoomScoreRequest: + submitRoomScoreRequest.TriggerSuccess(new MultiplayerScore + { + ID = currentScoreId++, + Accuracy = 1, + EndedAt = DateTimeOffset.Now, + Passed = true, + Rank = ScoreRank.S, + MaxCombo = 1000, + TotalScore = 1000000, + User = api.LocalUser.Value, + Statistics = new Dictionary() + }); + break; + } + }; + } + + public new void ClearRooms() => base.ClearRooms(); + + public new void Schedule(Action action) => base.Schedule(action); + } +} diff --git a/osu.Game/Tests/Visual/MultiplayerTestScene.cs b/osu.Game/Tests/Visual/RoomTestScene.cs similarity index 90% rename from osu.Game/Tests/Visual/MultiplayerTestScene.cs rename to osu.Game/Tests/Visual/RoomTestScene.cs index 6f24e00a92..aaf5c7624f 100644 --- a/osu.Game/Tests/Visual/MultiplayerTestScene.cs +++ b/osu.Game/Tests/Visual/RoomTestScene.cs @@ -4,11 +4,11 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; namespace osu.Game.Tests.Visual { - public abstract class MultiplayerTestScene : ScreenTestScene + public abstract class RoomTestScene : ScreenTestScene { [Cached] private readonly Bindable currentRoom = new Bindable(); diff --git a/osu.Game/Tests/Visual/SkinnableTestScene.cs b/osu.Game/Tests/Visual/SkinnableTestScene.cs index 68098f9d3b..3d2c68c2ad 100644 --- a/osu.Game/Tests/Visual/SkinnableTestScene.cs +++ b/osu.Game/Tests/Visual/SkinnableTestScene.cs @@ -13,8 +13,10 @@ using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Textures; using osu.Framework.IO.Stores; +using osu.Framework.Platform; using osu.Game.Beatmaps; using osu.Game.Graphics.Sprites; +using osu.Game.IO; using osu.Game.Rulesets; using osu.Game.Skinning; using osuTK; @@ -22,13 +24,16 @@ using osuTK.Graphics; namespace osu.Game.Tests.Visual { - public abstract class SkinnableTestScene : OsuGridTestScene + public abstract class SkinnableTestScene : OsuGridTestScene, IStorageResourceProvider { private Skin metricsSkin; private Skin defaultSkin; private Skin specialSkin; private Skin oldSkin; + [Resolved] + private GameHost host { get; set; } + protected SkinnableTestScene() : base(2, 3) { @@ -39,10 +44,10 @@ namespace osu.Game.Tests.Visual { var dllStore = new DllResourceStore(DynamicCompilationOriginal.GetType().Assembly); - metricsSkin = new TestLegacySkin(new SkinInfo { Name = "metrics-skin" }, new NamespacedResourceStore(dllStore, "Resources/metrics_skin"), audio, true); - defaultSkin = new DefaultLegacySkin(new NamespacedResourceStore(game.Resources, "Skins/Legacy"), audio); - specialSkin = new TestLegacySkin(new SkinInfo { Name = "special-skin" }, new NamespacedResourceStore(dllStore, "Resources/special_skin"), audio, true); - oldSkin = new TestLegacySkin(new SkinInfo { Name = "old-skin" }, new NamespacedResourceStore(dllStore, "Resources/old_skin"), audio, true); + metricsSkin = new TestLegacySkin(new SkinInfo { Name = "metrics-skin" }, new NamespacedResourceStore(dllStore, "Resources/metrics_skin"), this, true); + defaultSkin = new DefaultLegacySkin(new NamespacedResourceStore(game.Resources, "Skins/Legacy"), this); + specialSkin = new TestLegacySkin(new SkinInfo { Name = "special-skin" }, new NamespacedResourceStore(dllStore, "Resources/special_skin"), this, true); + oldSkin = new TestLegacySkin(new SkinInfo { Name = "old-skin" }, new NamespacedResourceStore(dllStore, "Resources/old_skin"), this, true); } private readonly List createdDrawables = new List(); @@ -147,6 +152,14 @@ namespace osu.Game.Tests.Visual protected virtual IBeatmap CreateBeatmapForSkinProvider() => CreateWorkingBeatmap(Ruleset.Value).GetPlayableBeatmap(Ruleset.Value); + #region IResourceStorageProvider + + public AudioManager AudioManager => Audio; + public IResourceStore Files => null; + public IResourceStore CreateTextureLoaderStore(IResourceStore underlyingStore) => host.CreateTextureLoaderStore(underlyingStore); + + #endregion + private class OutlineBox : CompositeDrawable { public OutlineBox() @@ -170,8 +183,8 @@ namespace osu.Game.Tests.Visual { private readonly bool extrapolateAnimations; - public TestLegacySkin(SkinInfo skin, IResourceStore storage, AudioManager audioManager, bool extrapolateAnimations) - : base(skin, storage, audioManager, "skin.ini") + public TestLegacySkin(SkinInfo skin, IResourceStore storage, IStorageResourceProvider resources, bool extrapolateAnimations) + : base(skin, storage, resources, "skin.ini") { this.extrapolateAnimations = extrapolateAnimations; } diff --git a/osu.Game/Tests/Visual/TestPlayer.cs b/osu.Game/Tests/Visual/TestPlayer.cs index f016d29f38..f47391ce6a 100644 --- a/osu.Game/Tests/Visual/TestPlayer.cs +++ b/osu.Game/Tests/Visual/TestPlayer.cs @@ -37,7 +37,11 @@ namespace osu.Game.Tests.Visual public readonly List Results = new List(); public TestPlayer(bool allowPause = true, bool showResults = true, bool pauseOnFocusLost = false) - : base(allowPause, showResults) + : base(new PlayerConfiguration + { + AllowPause = allowPause, + ShowResults = showResults + }) { PauseOnFocusLost = pauseOnFocusLost; } diff --git a/osu.Game/Users/Drawables/ClickableAvatar.cs b/osu.Game/Users/Drawables/ClickableAvatar.cs new file mode 100644 index 0000000000..0fca9c7c9b --- /dev/null +++ b/osu.Game/Users/Drawables/ClickableAvatar.cs @@ -0,0 +1,73 @@ +// 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.Graphics.Textures; +using osu.Framework.Input.Events; +using osu.Game.Graphics.Containers; + +namespace osu.Game.Users.Drawables +{ + public class ClickableAvatar : Container + { + /// + /// Whether to open the user's profile when clicked. + /// + public readonly BindableBool OpenOnClick = new BindableBool(true); + + private readonly User user; + + [Resolved(CanBeNull = true)] + private OsuGame game { get; set; } + + /// + /// A clickable avatar for the specified user, with UI sounds included. + /// If is true, clicking will open the user's profile. + /// + /// The user. A null value will get a placeholder avatar. + public ClickableAvatar(User user = null) + { + this.user = user; + } + + [BackgroundDependencyLoader] + private void load(LargeTextureStore textures) + { + ClickableArea clickableArea; + Add(clickableArea = new ClickableArea + { + RelativeSizeAxes = Axes.Both, + Action = openProfile + }); + + LoadComponentAsync(new DrawableAvatar(user), clickableArea.Add); + + clickableArea.Enabled.BindTo(OpenOnClick); + } + + private void openProfile() + { + if (!OpenOnClick.Value) + return; + + if (user?.Id > 1) + game?.ShowUser(user.Id); + } + + private class ClickableArea : OsuClickableContainer + { + public override string TooltipText => Enabled.Value ? @"view profile" : null; + + protected override bool OnClick(ClickEvent e) + { + if (!Enabled.Value) + return false; + + return base.OnClick(e); + } + } + } +} diff --git a/osu.Game/Users/Drawables/DrawableAvatar.cs b/osu.Game/Users/Drawables/DrawableAvatar.cs index 42d2dbb1c6..3dae3afe3f 100644 --- a/osu.Game/Users/Drawables/DrawableAvatar.cs +++ b/osu.Game/Users/Drawables/DrawableAvatar.cs @@ -1,88 +1,45 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; -using osu.Framework.Input.Events; -using osu.Game.Graphics.Containers; namespace osu.Game.Users.Drawables { [LongRunningLoad] - public class DrawableAvatar : Container + public class DrawableAvatar : Sprite { - /// - /// Whether to open the user's profile when clicked. - /// - public readonly BindableBool OpenOnClick = new BindableBool(true); - private readonly User user; - [Resolved(CanBeNull = true)] - private OsuGame game { get; set; } - /// - /// An avatar for specified user. + /// A simple, non-interactable avatar sprite for the specified user. /// /// The user. A null value will get a placeholder avatar. public DrawableAvatar(User user = null) { this.user = user; + + RelativeSizeAxes = Axes.Both; + FillMode = FillMode.Fit; + Anchor = Anchor.Centre; + Origin = Anchor.Centre; } [BackgroundDependencyLoader] private void load(LargeTextureStore textures) { - if (textures == null) - throw new ArgumentNullException(nameof(textures)); + if (user != null && user.Id > 1) + Texture = textures.Get($@"https://a.ppy.sh/{user.Id}"); - Texture texture = null; - if (user != null && user.Id > 1) texture = textures.Get($@"https://a.ppy.sh/{user.Id}"); - texture ??= textures.Get(@"Online/avatar-guest"); - - ClickableArea clickableArea; - Add(clickableArea = new ClickableArea - { - RelativeSizeAxes = Axes.Both, - Child = new Sprite - { - RelativeSizeAxes = Axes.Both, - Texture = texture, - FillMode = FillMode.Fit, - Anchor = Anchor.Centre, - Origin = Anchor.Centre - }, - Action = openProfile - }); - - clickableArea.Enabled.BindTo(OpenOnClick); + Texture ??= textures.Get(@"Online/avatar-guest"); } - private void openProfile() + protected override void LoadComplete() { - if (!OpenOnClick.Value) - return; - - if (user?.Id > 1) - game?.ShowUser(user.Id); - } - - private class ClickableArea : OsuClickableContainer - { - public override string TooltipText => Enabled.Value ? @"view profile" : null; - - protected override bool OnClick(ClickEvent e) - { - if (!Enabled.Value) - return false; - - return base.OnClick(e); - } + base.LoadComplete(); + this.FadeInFromZero(300, Easing.OutQuint); } } } diff --git a/osu.Game/Users/Drawables/UpdateableAvatar.cs b/osu.Game/Users/Drawables/UpdateableAvatar.cs index 171462f3fc..927e48cb56 100644 --- a/osu.Game/Users/Drawables/UpdateableAvatar.cs +++ b/osu.Game/Users/Drawables/UpdateableAvatar.cs @@ -65,12 +65,11 @@ namespace osu.Game.Users.Drawables if (user == null && !ShowGuestOnNull) return null; - var avatar = new DrawableAvatar(user) + var avatar = new ClickableAvatar(user) { RelativeSizeAxes = Axes.Both, }; - avatar.OnLoadComplete += d => d.FadeInFromZero(300, Easing.OutQuint); avatar.OpenOnClick.BindTo(OpenOnClick); return avatar; diff --git a/osu.Game/Users/UserActivity.cs b/osu.Game/Users/UserActivity.cs index 0b4fa94942..f633773d11 100644 --- a/osu.Game/Users/UserActivity.cs +++ b/osu.Game/Users/UserActivity.cs @@ -3,7 +3,7 @@ using osu.Game.Beatmaps; using osu.Game.Graphics; -using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; using osu.Game.Rulesets; using osuTK.Graphics; diff --git a/osu.Game/Utils/FormatUtils.cs b/osu.Game/Utils/FormatUtils.cs index f2ab99f4b7..2578d8d835 100644 --- a/osu.Game/Utils/FormatUtils.cs +++ b/osu.Game/Utils/FormatUtils.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 Humanizer; + namespace osu.Game.Utils { public static class FormatUtils @@ -18,5 +20,11 @@ namespace osu.Game.Utils /// The accuracy to be formatted /// formatted accuracy in percentage public static string FormatAccuracy(this decimal accuracy) => $"{accuracy:0.00}%"; + + /// + /// Formats the supplied rank/leaderboard position in a consistent, simplified way. + /// + /// The rank/position to be formatted. + public static string FormatRank(this int rank) => rank.ToMetric(decimals: rank < 100_000 ? 1 : 0); } } diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 960959f367..cbf9f6f1bd 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -26,7 +26,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index a5bcb91c74..adbcc0ef1c 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -88,7 +88,7 @@ - +