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