1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-14 01:42:55 +08:00

Fix merge conflicts.

This commit is contained in:
Lucas A 2020-12-27 13:52:45 +01:00
commit 7ae4979882
270 changed files with 7096 additions and 1779 deletions

View File

@ -52,6 +52,6 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.1202.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.1214.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.1222.0" />
</ItemGroup>
</Project>

View File

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

View File

@ -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,
})
{
}
}

View File

@ -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,
})
{
}
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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,
})
{
}
}

View File

@ -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,
})
{
}
}

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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));

View File

@ -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);

View File

@ -0,0 +1,39 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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);
}
}
}

View File

@ -0,0 +1,37 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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);
}
}
}

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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<DrawableHit>().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<string>();
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");

View File

@ -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
/// </summary>
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;

View File

@ -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
/// </summary>
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;

View File

@ -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<HitType> type;
private readonly Bindable<HitType> type = new Bindable<HitType>();
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;
/// <summary>
/// The lenience for the second key press.
/// This does not adjust by map difficulty in ScoreV2 yet.
/// </summary>
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);

View File

@ -1,7 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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
/// </summary>
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;
}
}
}

View File

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

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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)
{
}

View File

@ -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();
}

View File

@ -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<bool> isStrong;
private readonly Bindable<bool> isStrong = new BindableBool();
private readonly Container<DrawableStrongNestedHit> strongHitContainer;
protected DrawableTaikoStrongableHitObject(TObject hitObject)
protected DrawableTaikoStrongableHitObject([CanBeNull] TObject hitObject)
: base(hitObject)
{
isStrong = HitObject.IsStrongBindable.GetBoundCopy();
AddInternal(strongHitContainer = new Container<DrawableStrongNestedHit>());
}
[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)

View File

@ -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<TaikoHitObject> 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<TaikoHitObject> CreateDrawableRepresentation(TaikoHitObject h) => null;
protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new TaikoFramedReplayInputHandler(replay);

View File

@ -147,6 +147,32 @@ namespace osu.Game.Rulesets.Taiko.UI
},
drumRollHitContainer.CreateProxy(),
};
RegisterPool<Hit, DrawableHit>(50);
RegisterPool<Hit.StrongNestedHit, DrawableHit.StrongNestedHit>(50);
RegisterPool<DrumRoll, DrawableDrumRoll>(5);
RegisterPool<DrumRoll.StrongNestedHit, DrawableDrumRoll.StrongNestedHit>(5);
RegisterPool<DrumRollTick, DrawableDrumRollTick>(100);
RegisterPool<DrumRollTick.StrongNestedHit, DrawableDrumRollTick.StrongNestedHit>(100);
RegisterPool<Swell, DrawableSwell>(5);
RegisterPool<SwellTick, DrawableSwellTick>(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 _:

View File

@ -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<byte[]> Files => null;
public IResourceStore<TextureUpload> CreateTextureLoaderStore(IResourceStore<byte[]> underlyingStore) => null;
#endregion
}
}

View File

@ -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());
}
}
}

View File

@ -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);
}
/// <summary>
@ -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;
}
}
}

View File

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

View File

@ -0,0 +1,157 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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<Drawable> 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<int> PlayingUsers => (BindableList<int>)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<int, FrameHeader> lastHeaders = new Dictionary<int, FrameHeader>();
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, int>
{
[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<LegacyReplayFrame>()));
}
}
protected override Task Connect() => Task.CompletedTask;
}
}
}

View File

@ -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<int> PlayingUsers => (BindableList<int>)base.PlayingUsers;
private int beatmapId;
public TestSpectatorStreamingClient()
: base(new DevelopmentEndpointConfiguration())
{
}
protected override Task Connect()
{
return Task.CompletedTask;

View File

@ -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();

View File

@ -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<Room> Rooms = new BindableList<Room>();
public Bindable<bool> InitialRoomsReceived { get; } = new Bindable<bool>(true);
public IBindable<bool> InitialRoomsReceived { get; } = new Bindable<bool>(true);
IBindableList<Room> IRoomManager.Rooms => Rooms;

View File

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

View File

@ -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(() =>

View File

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

View File

@ -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; }

View File

@ -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()
{

View File

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

View File

@ -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; }

View File

@ -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;
}

View File

@ -0,0 +1,47 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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();
}
}
}

View File

@ -0,0 +1,77 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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<MultiplayerMatchSettingsOverlay.CreateOrUpdateButton>().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<MultiplayerMatchSettingsOverlay.CreateOrUpdateButton>().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<MultiplayerMatchSettingsOverlay.CreateOrUpdateButton>().Single());
InputManager.Click(MouseButton.Left);
});
AddWaitStep("wait", 10);
}
}
}

View File

@ -0,0 +1,117 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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<ParticipantPanel>().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<ParticipantPanel>().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<ParticipantPanel>().Single().User.User == secondUser);
}
[Test]
public void TestToggleReadyState()
{
AddAssert("ready mark invisible", () => !this.ChildrenOfType<StateDisplay>().Single().IsPresent);
AddStep("make user ready", () => Client.ChangeState(MultiplayerUserState.Ready));
AddUntilStep("ready mark visible", () => this.ChildrenOfType<StateDisplay>().Single().IsPresent);
AddStep("make user idle", () => Client.ChangeState(MultiplayerUserState.Idle));
AddUntilStep("ready mark invisible", () => !this.ChildrenOfType<StateDisplay>().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<ParticipantPanel>().ElementAt(0).ChildrenOfType<SpriteIcon>().First().Alpha == 1);
AddUntilStep("second user crown hidden", () => this.ChildrenOfType<ParticipantPanel>().ElementAt(1).ChildrenOfType<SpriteIcon>().First().Alpha == 0);
AddStep("make second user host", () => Client.TransferHost(3));
AddUntilStep("first user crown hidden", () => this.ChildrenOfType<ParticipantPanel>().ElementAt(0).ChildrenOfType<SpriteIcon>().First().Alpha == 0);
AddUntilStep("second user crown visible", () => this.ChildrenOfType<ParticipantPanel>().ElementAt(1).ChildrenOfType<SpriteIcon>().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));
}
});
}
}
}

View File

@ -0,0 +1,164 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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);
});
}
}

View File

@ -0,0 +1,153 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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;
}
}
}

View File

@ -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 },
}
};
}

View File

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

View File

@ -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();
}

View File

@ -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<APIChangelogEntry>
{
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]

View File

@ -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<User> 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],
});
}
}
}

View File

@ -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://"));
}

View File

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

View File

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

View File

@ -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<TimeSpan> DurationField => Settings.DurationField;
public OsuTextBox NameField => ((MatchSettings)Settings).NameField;
public OsuDropdown<TimeSpan> 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<bool> InitialRoomsReceived { get; } = new Bindable<bool>(true);
public IBindable<bool> InitialRoomsReceived { get; } = new Bindable<bool>(true);
public IBindableList<Room> Rooms => null;

View File

@ -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
});
}

View File

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

View File

@ -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<MatchSettingsOverlay.CreateRoomButton>().Single());
InputManager.MoveMouseTo(match.ChildrenOfType<PlaylistsMatchSettingsOverlay.CreateRoomButton>().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<PlaylistItem> SelectedItem => base.SelectedItem;
public new Bindable<WorkingBeatmap> 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<bool> InitialRoomsReceived { get; } = new Bindable<bool>(true);
public IBindable<bool> InitialRoomsReceived { get; } = new Bindable<bool>(true);
public IBindableList<Room> Rooms { get; } = new BindableList<Room>();

View File

@ -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);

View File

@ -0,0 +1,211 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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<BeatmapSetInfo> beatmapSets = null;
const int import_count = 5;
AddStep("import 5 maps", () =>
{
beatmapSets = new List<BeatmapSetInfo>();
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<RulesetInfo> 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<BeatmapSetInfo> beatmapSets) => beatmapSets.All(set => set != null);
private void presentAndConfirm(Func<BeatmapSetInfo> 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;
});
}
}
}

View File

@ -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.
/// </summary>
[ExcludeFromDynamicCompile]
public partial class BeatmapManager : DownloadableArchiveModelManager<BeatmapSetInfo, BeatmapSetFileInfo>, IDisposable
public partial class BeatmapManager : DownloadableArchiveModelManager<BeatmapSetInfo, BeatmapSetFileInfo>, IDisposable, IBeatmapResourceProvider
{
/// <summary>
/// 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<byte[]> IStorageResourceProvider.Files => Files.Store;
IResourceStore<TextureUpload> IStorageResourceProvider.CreateTextureLoaderStore(IResourceStore<byte[]> underlyingStore) => host?.CreateTextureLoaderStore(underlyingStore);
#endregion
/// <summary>
/// A dummy WorkingBeatmap for the purpose of retrieving a beatmap for star difficulty calculation.
/// </summary>

View File

@ -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<byte[]> store;
private readonly TextureStore textureStore;
private readonly ITrackStore trackStore;
[NotNull]
private readonly IBeatmapResourceProvider resources;
public BeatmapManagerWorkingBeatmap(IResourceStore<byte[]> 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<Beatmap>(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<Storyboard>(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)
{

View File

@ -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
{
/// <summary>
/// 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.
/// </summary>
public class DifficultyRecommender : Component
{
[Resolved]
@ -26,7 +30,12 @@ namespace osu.Game.Screens.Select
[Resolved]
private Bindable<RulesetInfo> ruleset { get; set; }
private readonly Dictionary<RulesetInfo, double> recommendedStarDifficulty = new Dictionary<RulesetInfo, double>();
/// <summary>
/// The user for which the last requests were run.
/// </summary>
private int? requestedUserId;
private readonly Dictionary<RulesetInfo, double> recommendedDifficultyMapping = new Dictionary<RulesetInfo, double>();
private readonly IBindable<APIState> apiState = new Bindable<APIState>();
@ -45,42 +54,64 @@ namespace osu.Game.Screens.Select
/// </remarks>
/// <param name="beatmaps">A collection of beatmaps to select a difficulty from.</param>
/// <returns>The recommended difficulty, or null if a recommendation could not be provided.</returns>
[CanBeNull]
public BeatmapInfo GetRecommendedBeatmap(IEnumerable<BeatmapInfo> 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);
});
}
/// <returns>
/// Rulesets ordered descending by their respective recommended difficulties.
/// The currently selected ruleset will always be first.
/// </returns>
private IEnumerable<RulesetInfo> orderedRulesets =>
recommendedDifficultyMapping
.OrderByDescending(pair => pair.Value).Select(pair => pair.Key).Where(r => !r.Equals(ruleset.Value))
.Prepend(ruleset.Value);
private void onlineStateChanged(ValueChangedEvent<APIState> state) => Schedule(() =>
{
switch (state.NewValue)
{
case APIState.Online:
calculateRecommendedDifficulties();
fetchRecommendedValues();
break;
}
});

View File

@ -0,0 +1,22 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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
{
/// <summary>
/// Retrieve a global large texture store, used for loading beatmap backgrounds.
/// </summary>
TextureStore LargeTextureStore { get; }
/// <summary>
/// Access a global track store for retrieving beatmap tracks from.
/// </summary>
ITrackStore Tracks { get; }
}
}

View File

@ -0,0 +1,19 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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)
{
}
}
}

View File

@ -0,0 +1,26 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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
{
/// <summary>
/// Denote a task which is to be run without local error handling logic, where failure is not catastrophic.
/// Avoids unobserved exceptions from being fired.
/// </summary>
/// <param name="task">The task.</param>
/// <param name="logOnError">Whether errors should be logged as important, or silently ignored.</param>
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);
}
}
}

View File

@ -0,0 +1,29 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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
{
/// <summary>
/// Retrieve the game-wide audio manager.
/// </summary>
AudioManager AudioManager { get; }
/// <summary>
/// Access game-wide user files.
/// </summary>
IResourceStore<byte[]> Files { get; }
/// <summary>
/// Create a texture loader store based on an underlying data store.
/// </summary>
/// <param name="underlyingStore">The underlying provider of texture data (in arbitrary image formats).</param>
/// <returns>A texture loader store.</returns>
IResourceStore<TextureUpload> CreateTextureLoaderStore(IResourceStore<byte[]> underlyingStore);
}
}

View File

@ -0,0 +1,16 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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();
}
}
}

View File

@ -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<APIRequest> queue = new Queue<APIRequest>();
public string APIEndpointUrl { get; }
public string WebsiteRootUrl { get; }
/// <summary>
/// The username/email provided by the user when initiating a login.
/// </summary>
@ -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<string>(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,

View File

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

View File

@ -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";
/// <summary>
/// Provide handling logic for an arbitrary API request.

View File

@ -46,7 +46,12 @@ namespace osu.Game.Online.API
/// <summary>
/// The URL endpoint for this API. Does not include a trailing slash.
/// </summary>
string Endpoint { get; }
string APIEndpointUrl { get; }
/// <summary>
/// The root URL of of the website, excluding the trailing slash.
/// </summary>
string WebsiteRootUrl { get; }
/// <summary>
/// The current connection state of the API.

View File

@ -7,16 +7,16 @@ namespace osu.Game.Online.API.Requests
{
public class GetBeatmapSetRequest : APIRequest<APIBeatmapSet>
{
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

View File

@ -9,14 +9,14 @@ namespace osu.Game.Online.API.Requests
public class GetUserRequest : APIRequest<User>
{
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}";
}
}

View File

@ -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;
}
}
}

View File

@ -48,6 +48,7 @@ namespace osu.Game.Online.API.Requests.Responses
public enum ChangelogEntryType
{
Add,
Fix
Fix,
Misc
}
}

View File

@ -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);
}
/// <summary>

View File

@ -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();

View File

@ -0,0 +1,17 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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";
}
}
}

View File

@ -0,0 +1,41 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
namespace osu.Game.Online
{
/// <summary>
/// Holds configuration for API endpoints.
/// </summary>
public class EndpointConfiguration
{
/// <summary>
/// The base URL for the website.
/// </summary>
public string WebsiteRootUrl { get; set; }
/// <summary>
/// The endpoint for the main (osu-web) API.
/// </summary>
public string APIEndpointUrl { get; set; }
/// <summary>
/// The OAuth client secret.
/// </summary>
public string APIClientSecret { get; set; }
/// <summary>
/// The OAuth client ID.
/// </summary>
public string APIClientID { get; set; }
/// <summary>
/// The endpoint for the SignalR spectator server.
/// </summary>
public string SpectatorEndpointUrl { get; set; }
/// <summary>
/// The endpoint for the SignalR multiplayer server.
/// </summary>
public string MultiplayerEndpointUrl { get; set; }
}
}

View File

@ -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()
};
}

View File

@ -3,7 +3,7 @@
using System.Threading.Tasks;
namespace osu.Game.Online.RealtimeMultiplayer
namespace osu.Game.Online.Multiplayer
{
/// <summary>
/// An interface defining a multiplayer client instance.

View File

@ -3,7 +3,7 @@
using System.Threading.Tasks;
namespace osu.Game.Online.RealtimeMultiplayer
namespace osu.Game.Online.Multiplayer
{
/// <summary>
/// Interface for an out-of-room multiplayer server.

View File

@ -3,7 +3,7 @@
using System.Threading.Tasks;
namespace osu.Game.Online.RealtimeMultiplayer
namespace osu.Game.Online.Multiplayer
{
/// <summary>
/// Interface for an in-room multiplayer server.

View File

@ -1,7 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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
{
/// <summary>
/// An interface defining the multiplayer server instance.

View File

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

View File

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

View File

@ -0,0 +1,183 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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<bool> IsConnected => isConnected;
private readonly Bindable<bool> isConnected = new Bindable<bool>();
private readonly IBindable<APIState> apiState = new Bindable<APIState>();
[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<APIState> 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<MultiplayerRoomState>(nameof(IMultiplayerClient.RoomStateChanged), ((IMultiplayerClient)this).RoomStateChanged);
connection.On<MultiplayerRoomUser>(nameof(IMultiplayerClient.UserJoined), ((IMultiplayerClient)this).UserJoined);
connection.On<MultiplayerRoomUser>(nameof(IMultiplayerClient.UserLeft), ((IMultiplayerClient)this).UserLeft);
connection.On<int>(nameof(IMultiplayerClient.HostChanged), ((IMultiplayerClient)this).HostChanged);
connection.On<MultiplayerRoomSettings>(nameof(IMultiplayerClient.SettingsChanged), ((IMultiplayerClient)this).SettingsChanged);
connection.On<int, MultiplayerUserState>(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<MultiplayerRoom> JoinRoom(long roomId)
{
if (!isConnected.Value)
return Task.FromCanceled<MultiplayerRoom>(new CancellationToken(true));
return connection.InvokeAsync<MultiplayerRoom>(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));
}
}
}

View File

@ -9,7 +9,7 @@ using System.Threading;
using Newtonsoft.Json;
using osu.Framework.Allocation;
namespace osu.Game.Online.RealtimeMultiplayer
namespace osu.Game.Online.Multiplayer
{
/// <summary>
/// A multiplayer room.

View File

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

View File

@ -3,10 +3,10 @@
#nullable enable
namespace osu.Game.Online.RealtimeMultiplayer
namespace osu.Game.Online.Multiplayer
{
/// <summary>
/// The current overall state of a realtime multiplayer room.
/// The current overall state of a multiplayer room.
/// </summary>
public enum MultiplayerRoomState
{

View File

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

View File

@ -1,7 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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
{

View File

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

View File

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

View File

@ -0,0 +1,458 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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
{
/// <summary>
/// Invoked when any change occurs to the multiplayer room.
/// </summary>
public event Action? RoomUpdated;
/// <summary>
/// Invoked when the multiplayer server requests the current beatmap to be loaded into play.
/// </summary>
public event Action? LoadRequested;
/// <summary>
/// Invoked when the multiplayer server requests gameplay to be started.
/// </summary>
public event Action? MatchStarted;
/// <summary>
/// Invoked when the multiplayer server has finished collating results.
/// </summary>
public event Action? ResultsReady;
/// <summary>
/// Whether the <see cref="StatefulMultiplayerClient"/> is currently connected.
/// </summary>
public abstract IBindable<bool> IsConnected { get; }
/// <summary>
/// The joined <see cref="MultiplayerRoom"/>.
/// </summary>
public MultiplayerRoom? Room { get; private set; }
/// <summary>
/// The users currently in gameplay.
/// </summary>
public readonly BindableList<int> PlayingUsers = new BindableList<int>();
[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();
}
});
}
/// <summary>
/// Joins the <see cref="MultiplayerRoom"/> for a given API <see cref="Room"/>.
/// </summary>
/// <param name="room">The API <see cref="Room"/>.</param>
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);
}
/// <summary>
/// Joins the <see cref="MultiplayerRoom"/> with a given ID.
/// </summary>
/// <param name="roomId">The room ID.</param>
/// <returns>The joined <see cref="MultiplayerRoom"/>.</returns>
protected abstract Task<MultiplayerRoom> JoinRoom(long roomId);
public virtual Task LeaveRoom()
{
Scheduler.Add(() =>
{
if (Room == null)
return;
apiRoom = null;
Room = null;
RoomUpdated?.Invoke();
}, false);
return Task.CompletedTask;
}
/// <summary>
/// Change the current <see cref="MultiplayerRoom"/> settings.
/// </summary>
/// <remarks>
/// A room must be joined for this to have any effect.
/// </remarks>
/// <param name="name">The new room name, if any.</param>
/// <param name="item">The new room playlist item, if any.</param>
public Task ChangeSettings(Optional<string> name = default, Optional<PlaylistItem> 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;
}
/// <summary>
/// Populates the <see cref="User"/> for a given <see cref="MultiplayerRoomUser"/>.
/// </summary>
/// <param name="multiplayerUser">The <see cref="MultiplayerRoomUser"/> to populate.</param>
protected async Task PopulateUser(MultiplayerRoomUser multiplayerUser) => multiplayerUser.User ??= await userLookupCache.GetUserAsync(multiplayerUser.UserID);
/// <summary>
/// Retrieve a copy of users currently in the joined <see cref="Room"/> in a thread-safe manner.
/// This should be used whenever accessing users from outside of an Update thread context (ie. when not calling <see cref="Drawable.Schedule"/>).
/// </summary>
/// <returns>A copy of users in the current room, or null if unavailable.</returns>
private List<MultiplayerRoomUser>? getRoomUsers()
{
List<MultiplayerRoomUser>? 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;
}
/// <summary>
/// Updates the local room settings with the given <see cref="MultiplayerRoomSettings"/>.
/// </summary>
/// <remarks>
/// This updates both the joined <see cref="MultiplayerRoom"/> and the respective API <see cref="Room"/>.
/// </remarks>
/// <param name="settings">The new <see cref="MultiplayerRoomSettings"/> to update from.</param>
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);
}
}
}

View File

@ -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;
/// <summary>
/// The time in milliseconds to wait between polls.
/// Setting to zero stops all polling.
/// </summary>
public double TimeBetweenPolls
{
get => timeBetweenPolls;
set
{
timeBetweenPolls = value;
scheduledPoll?.Cancel();
pollIfNecessary();
}
}
public readonly Bindable<double> TimeBetweenPolls = new Bindable<double>();
/// <summary>
///
@ -42,7 +32,13 @@ namespace osu.Game.Online
/// <param name="timeBetweenPolls">The initial time in milliseconds to wait between polls. Setting to zero stops all polling.</param>
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
/// </summary>
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));
}
}
}

View File

@ -0,0 +1,17 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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";
}
}
}

View File

@ -3,7 +3,7 @@
using Newtonsoft.Json;
namespace osu.Game.Online.Multiplayer
namespace osu.Game.Online.Rooms
{
public class APICreatedRoom : Room
{

View File

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

Some files were not shown because too many files have changed in this diff Show More