mirror of
https://github.com/ppy/osu.git
synced 2025-03-15 23:57:25 +08:00
Merge branch 'master' into fix-legacy-skin-texture-loader-store
This commit is contained in:
commit
a7f78d706a
@ -25,16 +25,22 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
|||||||
|
|
||||||
private ScrollingHitObjectContainer hitObjectContainer;
|
private ScrollingHitObjectContainer hitObjectContainer;
|
||||||
|
|
||||||
[SetUpSteps]
|
[BackgroundDependencyLoader]
|
||||||
public void SetUp()
|
private void load()
|
||||||
=> AddStep("create SHOC", () => Child = hitObjectContainer = new ScrollingHitObjectContainer
|
{
|
||||||
|
Child = hitObjectContainer = new ScrollingHitObjectContainer
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.X,
|
RelativeSizeAxes = Axes.X,
|
||||||
Height = 200,
|
Height = 200,
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
Clock = new FramedClock(new StopwatchClock())
|
Clock = new FramedClock(new StopwatchClock())
|
||||||
});
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
[SetUpSteps]
|
||||||
|
public void SetUp()
|
||||||
|
=> AddStep("clear SHOC", () => hitObjectContainer.Clear(false));
|
||||||
|
|
||||||
protected void AddHitObject(DrawableHitObject hitObject)
|
protected void AddHitObject(DrawableHitObject hitObject)
|
||||||
=> AddStep("add to SHOC", () => hitObjectContainer.Add(hitObject));
|
=> AddStep("add to SHOC", () => hitObjectContainer.Add(hitObject));
|
||||||
|
@ -12,12 +12,13 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestApplyNewBarLine()
|
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,
|
StartTime = 400,
|
||||||
Major = true
|
Major = true
|
||||||
}));
|
}), null));
|
||||||
|
|
||||||
AddHitObject(barLine);
|
AddHitObject(barLine);
|
||||||
RemoveHitObject(barLine);
|
RemoveHitObject(barLine);
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
37
osu.Game.Rulesets.Taiko.Tests/TestSceneHitApplication.cs
Normal file
37
osu.Game.Rulesets.Taiko.Tests/TestSceneHitApplication.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Game.Audio;
|
using osu.Game.Audio;
|
||||||
@ -18,24 +19,33 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
|||||||
public override void SetUpSteps()
|
public override void SetUpSteps()
|
||||||
{
|
{
|
||||||
base.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[]
|
// due to pooling we can't access all samples right away due to object re-use,
|
||||||
{
|
// so we need to collect as we go.
|
||||||
string.Empty,
|
AddStep("collect sample names", () => Player.DrawableRuleset.Playfield.NewResult += (dho, _) =>
|
||||||
string.Empty,
|
{
|
||||||
string.Empty,
|
if (!(dho is DrawableHit h))
|
||||||
string.Empty,
|
return;
|
||||||
HitSampleInfo.HIT_FINISH,
|
|
||||||
HitSampleInfo.HIT_WHISTLE,
|
|
||||||
HitSampleInfo.HIT_WHISTLE,
|
|
||||||
HitSampleInfo.HIT_WHISTLE,
|
|
||||||
};
|
|
||||||
|
|
||||||
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");
|
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TaikoBeatmapConversionTest().GetBeatmap("sample-to-type-conversions");
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using JetBrains.Annotations;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Utils;
|
using osu.Framework.Utils;
|
||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
@ -31,15 +32,26 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private int rollingHits;
|
private int rollingHits;
|
||||||
|
|
||||||
private Container tickContainer;
|
private readonly Container tickContainer;
|
||||||
|
|
||||||
private Color4 colourIdle;
|
private Color4 colourIdle;
|
||||||
private Color4 colourEngaged;
|
private Color4 colourEngaged;
|
||||||
|
|
||||||
public DrawableDrumRoll(DrumRoll drumRoll)
|
public DrawableDrumRoll()
|
||||||
|
: this(null)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public DrawableDrumRoll([CanBeNull] DrumRoll drumRoll)
|
||||||
: base(drumRoll)
|
: base(drumRoll)
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Y;
|
RelativeSizeAxes = Axes.Y;
|
||||||
|
|
||||||
|
Content.Add(tickContainer = new Container
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Depth = float.MinValue
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
@ -47,12 +59,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
|||||||
{
|
{
|
||||||
colourIdle = colours.YellowDark;
|
colourIdle = colours.YellowDark;
|
||||||
colourEngaged = colours.YellowDarker;
|
colourEngaged = colours.YellowDarker;
|
||||||
|
|
||||||
Content.Add(tickContainer = new Container
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.Both,
|
|
||||||
Depth = float.MinValue
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
@ -68,6 +74,12 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
|||||||
updateColour();
|
updateColour();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void OnFree()
|
||||||
|
{
|
||||||
|
base.OnFree();
|
||||||
|
rollingHits = 0;
|
||||||
|
}
|
||||||
|
|
||||||
protected override void AddNestedHitObject(DrawableHitObject hitObject)
|
protected override void AddNestedHitObject(DrawableHitObject hitObject)
|
||||||
{
|
{
|
||||||
base.AddNestedHitObject(hitObject);
|
base.AddNestedHitObject(hitObject);
|
||||||
@ -83,7 +95,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
|||||||
protected override void ClearNestedHitObjects()
|
protected override void ClearNestedHitObjects()
|
||||||
{
|
{
|
||||||
base.ClearNestedHitObjects();
|
base.ClearNestedHitObjects();
|
||||||
tickContainer.Clear();
|
tickContainer.Clear(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject)
|
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);
|
rollingHits = Math.Clamp(rollingHits, 0, rolling_hits_for_engaged_colour);
|
||||||
|
|
||||||
updateColour();
|
updateColour(100);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void CheckForResult(bool userTriggered, double timeOffset)
|
protected override void CheckForResult(bool userTriggered, double timeOffset)
|
||||||
@ -154,27 +166,34 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
|||||||
Content.X = DrawHeight / 2;
|
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);
|
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)
|
public new DrawableDrumRoll ParentHitObject => (DrawableDrumRoll)base.ParentHitObject;
|
||||||
: base(nestedHit, drumRoll)
|
|
||||||
|
public StrongNestedHit()
|
||||||
|
: this(null)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public StrongNestedHit([CanBeNull] DrumRoll.StrongNestedHit nestedHit)
|
||||||
|
: base(nestedHit)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void CheckForResult(bool userTriggered, double timeOffset)
|
protected override void CheckForResult(bool userTriggered, double timeOffset)
|
||||||
{
|
{
|
||||||
if (!MainObject.Judged)
|
if (!ParentHitObject.Judged)
|
||||||
return;
|
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;
|
public override bool OnPressed(TaikoAction action) => false;
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using JetBrains.Annotations;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Game.Rulesets.Objects.Drawables;
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
using osu.Game.Rulesets.Taiko.Skinning.Default;
|
using osu.Game.Rulesets.Taiko.Skinning.Default;
|
||||||
@ -16,7 +17,12 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public HitType JudgementType;
|
public HitType JudgementType;
|
||||||
|
|
||||||
public DrawableDrumRollTick(DrumRollTick tick)
|
public DrawableDrumRollTick()
|
||||||
|
: this(null)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public DrawableDrumRollTick([CanBeNull] DrumRollTick tick)
|
||||||
: base(tick)
|
: base(tick)
|
||||||
{
|
{
|
||||||
FillMode = FillMode.Fit;
|
FillMode = FillMode.Fit;
|
||||||
@ -61,21 +67,28 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
|||||||
return UpdateResult(true);
|
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)
|
public new DrawableDrumRollTick ParentHitObject => (DrawableDrumRollTick)base.ParentHitObject;
|
||||||
: base(nestedHit, tick)
|
|
||||||
|
public StrongNestedHit()
|
||||||
|
: this(null)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public StrongNestedHit([CanBeNull] DrumRollTick.StrongNestedHit nestedHit)
|
||||||
|
: base(nestedHit)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void CheckForResult(bool userTriggered, double timeOffset)
|
protected override void CheckForResult(bool userTriggered, double timeOffset)
|
||||||
{
|
{
|
||||||
if (!MainObject.Judged)
|
if (!ParentHitObject.Judged)
|
||||||
return;
|
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;
|
public override bool OnPressed(TaikoAction action) => false;
|
||||||
|
@ -5,7 +5,7 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Allocation;
|
using JetBrains.Annotations;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Game.Audio;
|
using osu.Game.Audio;
|
||||||
@ -36,29 +36,51 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
|||||||
|
|
||||||
private bool pressHandledThisFrame;
|
private bool pressHandledThisFrame;
|
||||||
|
|
||||||
private readonly Bindable<HitType> type;
|
private readonly Bindable<HitType> type = new Bindable<HitType>();
|
||||||
|
|
||||||
public DrawableHit(Hit hit)
|
public DrawableHit()
|
||||||
: base(hit)
|
: this(null)
|
||||||
{
|
{
|
||||||
type = HitObject.TypeBindable.GetBoundCopy();
|
|
||||||
FillMode = FillMode.Fit;
|
|
||||||
|
|
||||||
updateActionsFromType();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
public DrawableHit([CanBeNull] Hit hit)
|
||||||
private void load()
|
: base(hit)
|
||||||
{
|
{
|
||||||
|
FillMode = FillMode.Fit;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnApply()
|
||||||
|
{
|
||||||
|
type.BindTo(HitObject.TypeBindable);
|
||||||
type.BindValueChanged(_ =>
|
type.BindValueChanged(_ =>
|
||||||
{
|
{
|
||||||
updateActionsFromType();
|
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();
|
updateSamplesFromTypeChange();
|
||||||
|
|
||||||
RecreatePieces();
|
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();
|
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>
|
/// <summary>
|
||||||
/// The lenience for the second key press.
|
/// The lenience for the second key press.
|
||||||
/// This does not adjust by map difficulty in ScoreV2 yet.
|
/// This does not adjust by map difficulty in ScoreV2 yet.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private const double second_hit_window = 30;
|
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)
|
public StrongNestedHit([CanBeNull] Hit.StrongNestedHit nestedHit)
|
||||||
: base(nestedHit, hit)
|
: base(nestedHit)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void CheckForResult(bool userTriggered, double timeOffset)
|
protected override void CheckForResult(bool userTriggered, double timeOffset)
|
||||||
{
|
{
|
||||||
if (!MainObject.Result.HasResult)
|
if (!ParentHitObject.Result.HasResult)
|
||||||
{
|
{
|
||||||
base.CheckForResult(userTriggered, timeOffset);
|
base.CheckForResult(userTriggered, timeOffset);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!MainObject.Result.IsHit)
|
if (!ParentHitObject.Result.IsHit)
|
||||||
{
|
{
|
||||||
ApplyResult(r => r.Type = r.Judgement.MinResult);
|
ApplyResult(r => r.Type = r.Judgement.MinResult);
|
||||||
return;
|
return;
|
||||||
@ -261,27 +288,27 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
|||||||
|
|
||||||
if (!userTriggered)
|
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);
|
ApplyResult(r => r.Type = r.Judgement.MinResult);
|
||||||
return;
|
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);
|
ApplyResult(r => r.Type = r.Judgement.MaxResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool OnPressed(TaikoAction action)
|
public override bool OnPressed(TaikoAction action)
|
||||||
{
|
{
|
||||||
// Don't process actions until the main hitobject is hit
|
// Don't process actions until the main hitobject is hit
|
||||||
if (!MainObject.IsHit)
|
if (!ParentHitObject.IsHit)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
// Don't process actions if the pressed button was released
|
// Don't process actions if the pressed button was released
|
||||||
if (MainObject.HitAction == null)
|
if (ParentHitObject.HitAction == null)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
// Don't handle invalid hit action presses
|
// Don't handle invalid hit action presses
|
||||||
if (!MainObject.HitActions.Contains(action))
|
if (!ParentHitObject.HitActions.Contains(action))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
return UpdateResult(true);
|
return UpdateResult(true);
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// 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;
|
using osu.Game.Rulesets.Taiko.Judgements;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||||
@ -11,12 +11,11 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class DrawableStrongNestedHit : DrawableTaikoHitObject
|
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)
|
: base(nestedHit)
|
||||||
{
|
{
|
||||||
MainObject = mainObject;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using JetBrains.Annotations;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Extensions.Color4Extensions;
|
using osu.Framework.Extensions.Color4Extensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
@ -35,7 +36,12 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
|||||||
private readonly CircularContainer targetRing;
|
private readonly CircularContainer targetRing;
|
||||||
private readonly CircularContainer expandingRing;
|
private readonly CircularContainer expandingRing;
|
||||||
|
|
||||||
public DrawableSwell(Swell swell)
|
public DrawableSwell()
|
||||||
|
: this(null)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public DrawableSwell([CanBeNull] Swell swell)
|
||||||
: base(swell)
|
: base(swell)
|
||||||
{
|
{
|
||||||
FillMode = FillMode.Fit;
|
FillMode = FillMode.Fit;
|
||||||
@ -123,12 +129,13 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
|||||||
Origin = Anchor.Centre,
|
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
|
UnproxyContent();
|
||||||
Width *= Parent.RelativeChildSize.X;
|
|
||||||
|
lastWasCentre = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void AddNestedHitObject(DrawableHitObject hitObject)
|
protected override void AddNestedHitObject(DrawableHitObject hitObject)
|
||||||
@ -146,7 +153,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
|||||||
protected override void ClearNestedHitObjects()
|
protected override void ClearNestedHitObjects()
|
||||||
{
|
{
|
||||||
base.ClearNestedHitObjects();
|
base.ClearNestedHitObjects();
|
||||||
ticks.Clear();
|
ticks.Clear(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject)
|
protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject)
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using JetBrains.Annotations;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Game.Rulesets.Taiko.Skinning.Default;
|
using osu.Game.Rulesets.Taiko.Skinning.Default;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
@ -11,7 +12,12 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
|||||||
{
|
{
|
||||||
public override bool DisplayResult => false;
|
public override bool DisplayResult => false;
|
||||||
|
|
||||||
public DrawableSwellTick(SwellTick hitObject)
|
public DrawableSwellTick()
|
||||||
|
: this(null)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public DrawableSwellTick([CanBeNull] SwellTick hitObject)
|
||||||
: base(hitObject)
|
: base(hitObject)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Allocation;
|
using JetBrains.Annotations;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Primitives;
|
using osu.Framework.Graphics.Primitives;
|
||||||
@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
|||||||
|
|
||||||
private readonly Container nonProxiedContent;
|
private readonly Container nonProxiedContent;
|
||||||
|
|
||||||
protected DrawableTaikoHitObject(TaikoHitObject hitObject)
|
protected DrawableTaikoHitObject([CanBeNull] TaikoHitObject hitObject)
|
||||||
: base(hitObject)
|
: base(hitObject)
|
||||||
{
|
{
|
||||||
AddRangeInternal(new[]
|
AddRangeInternal(new[]
|
||||||
@ -113,25 +113,23 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
|||||||
{
|
{
|
||||||
public override Vector2 OriginPosition => new Vector2(DrawHeight / 2);
|
public override Vector2 OriginPosition => new Vector2(DrawHeight / 2);
|
||||||
|
|
||||||
public new TObject HitObject;
|
public new TObject HitObject => (TObject)base.HitObject;
|
||||||
|
|
||||||
protected Vector2 BaseSize;
|
protected Vector2 BaseSize;
|
||||||
protected SkinnableDrawable MainPiece;
|
protected SkinnableDrawable MainPiece;
|
||||||
|
|
||||||
protected DrawableTaikoHitObject(TObject hitObject)
|
protected DrawableTaikoHitObject([CanBeNull] TObject hitObject)
|
||||||
: base(hitObject)
|
: base(hitObject)
|
||||||
{
|
{
|
||||||
HitObject = hitObject;
|
|
||||||
|
|
||||||
Anchor = Anchor.CentreLeft;
|
Anchor = Anchor.CentreLeft;
|
||||||
Origin = Anchor.Custom;
|
Origin = Anchor.Custom;
|
||||||
|
|
||||||
RelativeSizeAxes = Axes.Both;
|
RelativeSizeAxes = Axes.Both;
|
||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
protected override void OnApply()
|
||||||
private void load()
|
|
||||||
{
|
{
|
||||||
|
base.OnApply();
|
||||||
RecreatePieces();
|
RecreatePieces();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Allocation;
|
using JetBrains.Annotations;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Game.Audio;
|
using osu.Game.Audio;
|
||||||
@ -16,28 +16,38 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
|||||||
where TObject : TaikoStrongableHitObject
|
where TObject : TaikoStrongableHitObject
|
||||||
where TStrongNestedObject : StrongNestedHitObject
|
where TStrongNestedObject : StrongNestedHitObject
|
||||||
{
|
{
|
||||||
private readonly Bindable<bool> isStrong;
|
private readonly Bindable<bool> isStrong = new BindableBool();
|
||||||
|
|
||||||
private readonly Container<DrawableStrongNestedHit> strongHitContainer;
|
private readonly Container<DrawableStrongNestedHit> strongHitContainer;
|
||||||
|
|
||||||
protected DrawableTaikoStrongableHitObject(TObject hitObject)
|
protected DrawableTaikoStrongableHitObject([CanBeNull] TObject hitObject)
|
||||||
: base(hitObject)
|
: base(hitObject)
|
||||||
{
|
{
|
||||||
isStrong = HitObject.IsStrongBindable.GetBoundCopy();
|
|
||||||
|
|
||||||
AddInternal(strongHitContainer = new Container<DrawableStrongNestedHit>());
|
AddInternal(strongHitContainer = new Container<DrawableStrongNestedHit>());
|
||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
protected override void OnApply()
|
||||||
private void load()
|
|
||||||
{
|
{
|
||||||
|
isStrong.BindTo(HitObject.IsStrongBindable);
|
||||||
isStrong.BindValueChanged(_ =>
|
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();
|
updateSamplesFromStrong();
|
||||||
|
|
||||||
RecreatePieces();
|
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();
|
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()
|
protected override void ClearNestedHitObjects()
|
||||||
{
|
{
|
||||||
base.ClearNestedHitObjects();
|
base.ClearNestedHitObjects();
|
||||||
strongHitContainer.Clear();
|
strongHitContainer.Clear(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject)
|
protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject)
|
||||||
|
@ -7,7 +7,6 @@ using osu.Framework.Graphics;
|
|||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Rulesets.Objects.Drawables;
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
using osu.Game.Rulesets.Taiko.Objects;
|
using osu.Game.Rulesets.Taiko.Objects;
|
||||||
using osu.Game.Rulesets.Taiko.Objects.Drawables;
|
|
||||||
using osu.Game.Rulesets.UI;
|
using osu.Game.Rulesets.UI;
|
||||||
using osu.Game.Rulesets.Taiko.Replays;
|
using osu.Game.Rulesets.Taiko.Replays;
|
||||||
using osu.Framework.Input;
|
using osu.Framework.Input;
|
||||||
@ -64,22 +63,7 @@ namespace osu.Game.Rulesets.Taiko.UI
|
|||||||
|
|
||||||
protected override Playfield CreatePlayfield() => new TaikoPlayfield(Beatmap.ControlPointInfo);
|
protected override Playfield CreatePlayfield() => new TaikoPlayfield(Beatmap.ControlPointInfo);
|
||||||
|
|
||||||
public override DrawableHitObject<TaikoHitObject> CreateDrawableRepresentation(TaikoHitObject h)
|
public override DrawableHitObject<TaikoHitObject> CreateDrawableRepresentation(TaikoHitObject h) => null;
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new TaikoFramedReplayInputHandler(replay);
|
protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new TaikoFramedReplayInputHandler(replay);
|
||||||
|
|
||||||
|
@ -147,6 +147,32 @@ namespace osu.Game.Rulesets.Taiko.UI
|
|||||||
},
|
},
|
||||||
drumRollHitContainer.CreateProxy(),
|
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()
|
protected override void Update()
|
||||||
@ -207,9 +233,7 @@ namespace osu.Game.Rulesets.Taiko.UI
|
|||||||
barLinePlayfield.Add(barLine);
|
barLinePlayfield.Add(barLine);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case DrawableTaikoHitObject taikoObject:
|
case DrawableTaikoHitObject _:
|
||||||
h.OnNewResult += OnNewResult;
|
|
||||||
topLevelHitContainer.Add(taikoObject.CreateProxiedContent());
|
|
||||||
base.Add(h);
|
base.Add(h);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@ -226,8 +250,6 @@ namespace osu.Game.Rulesets.Taiko.UI
|
|||||||
return barLinePlayfield.Remove(barLine);
|
return barLinePlayfield.Remove(barLine);
|
||||||
|
|
||||||
case DrawableTaikoHitObject _:
|
case DrawableTaikoHitObject _:
|
||||||
h.OnNewResult -= OnNewResult;
|
|
||||||
// todo: consider tidying of proxied content if required.
|
|
||||||
return base.Remove(h);
|
return base.Remove(h);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@ -248,7 +270,7 @@ namespace osu.Game.Rulesets.Taiko.UI
|
|||||||
{
|
{
|
||||||
case TaikoStrongJudgement _:
|
case TaikoStrongJudgement _:
|
||||||
if (result.IsHit)
|
if (result.IsHit)
|
||||||
hitExplosionContainer.Children.FirstOrDefault(e => e.JudgedObject == ((DrawableStrongNestedHit)judgedObject).MainObject)?.VisualiseSecondHit();
|
hitExplosionContainer.Children.FirstOrDefault(e => e.JudgedObject == ((DrawableStrongNestedHit)judgedObject).ParentHitObject)?.VisualiseSecondHit();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case TaikoDrumRollTickJudgement _:
|
case TaikoDrumRollTickJudgement _:
|
||||||
|
@ -61,12 +61,12 @@ namespace osu.Game.Tests.Visual.Components
|
|||||||
{
|
{
|
||||||
createPoller(true);
|
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(1);
|
||||||
checkCount(2);
|
checkCount(2);
|
||||||
checkCount(3);
|
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);
|
checkCount(4);
|
||||||
checkCount(4);
|
checkCount(4);
|
||||||
@ -76,7 +76,7 @@ namespace osu.Game.Tests.Visual.Components
|
|||||||
checkCount(5);
|
checkCount(5);
|
||||||
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(6);
|
||||||
checkCount(7);
|
checkCount(7);
|
||||||
}
|
}
|
||||||
@ -87,7 +87,7 @@ namespace osu.Game.Tests.Visual.Components
|
|||||||
{
|
{
|
||||||
createPoller(false);
|
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);
|
checkCount(0);
|
||||||
skip();
|
skip();
|
||||||
checkCount(0);
|
checkCount(0);
|
||||||
@ -141,7 +141,7 @@ namespace osu.Game.Tests.Visual.Components
|
|||||||
|
|
||||||
public class TestSlowPoller : TestPoller
|
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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,8 @@ using osu.Framework.Timing;
|
|||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
|
using osu.Game.Scoring;
|
||||||
|
using osu.Game.Screens.Ranking;
|
||||||
using osu.Game.Storyboards;
|
using osu.Game.Storyboards;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
|
||||||
@ -50,7 +52,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
cancel();
|
cancel();
|
||||||
complete();
|
complete();
|
||||||
|
|
||||||
AddUntilStep("attempted to push ranking", () => ((FakeRankingPushPlayer)Player).GotoRankingInvoked);
|
AddUntilStep("attempted to push ranking", () => ((FakeRankingPushPlayer)Player).ResultsCreated);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -84,7 +86,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
{
|
{
|
||||||
// wait to ensure there was no attempt of pushing the results screen.
|
// wait to ensure there was no attempt of pushing the results screen.
|
||||||
AddWaitStep("wait", resultsDisplayWaitCount);
|
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)
|
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null)
|
||||||
@ -110,16 +112,18 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
|
|
||||||
public class FakeRankingPushPlayer : TestPlayer
|
public class FakeRankingPushPlayer : TestPlayer
|
||||||
{
|
{
|
||||||
public bool GotoRankingInvoked;
|
public bool ResultsCreated { get; private set; }
|
||||||
|
|
||||||
public FakeRankingPushPlayer()
|
public FakeRankingPushPlayer()
|
||||||
: base(true, true)
|
: base(true, true)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void GotoRanking()
|
protected override ResultsScreen CreateResults(ScoreInfo score)
|
||||||
{
|
{
|
||||||
GotoRankingInvoked = true;
|
var results = base.CreateResults(score);
|
||||||
|
ResultsCreated = true;
|
||||||
|
return results;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ using NUnit.Framework;
|
|||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
|
using osu.Framework.Utils;
|
||||||
using osu.Game.Screens.Play.HUD;
|
using osu.Game.Screens.Play.HUD;
|
||||||
using osu.Game.Users;
|
using osu.Game.Users;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
@ -26,7 +27,6 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
Scale = new Vector2(2),
|
Scale = new Vector2(2),
|
||||||
RelativeSizeAxes = Axes.X,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -39,7 +39,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
playerScore.Value = 1222333;
|
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);
|
AddSliderStep("set player score", 50, 5000000, 1222333, v => playerScore.Value = v);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,8 +49,8 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
var player2Score = new BindableDouble(1234567);
|
var player2Score = new BindableDouble(1234567);
|
||||||
var player3Score = new BindableDouble(1111111);
|
var player3Score = new BindableDouble(1111111);
|
||||||
|
|
||||||
AddStep("add player 2", () => leaderboard.AddPlayer(player2Score, new User { Username = "Player 2" }));
|
AddStep("add player 2", () => createLeaderboardScore(player2Score, new User { Username = "Player 2" }));
|
||||||
AddStep("add player 3", () => leaderboard.AddPlayer(player3Score, new User { Username = "Player 3" }));
|
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 2 position #1", () => leaderboard.CheckPositionByUsername("Player 2", 1));
|
||||||
AddAssert("is player position #2", () => leaderboard.CheckPositionByUsername("You", 2));
|
AddAssert("is player position #2", () => leaderboard.CheckPositionByUsername("You", 2));
|
||||||
@ -67,6 +67,30 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
AddAssert("is player 2 position #3", () => leaderboard.CheckPositionByUsername("Player 2", 3));
|
AddAssert("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
|
private class TestGameplayLeaderboard : GameplayLeaderboard
|
||||||
{
|
{
|
||||||
public bool CheckPositionByUsername(string username, int? expectedPosition)
|
public bool CheckPositionByUsername(string username, int? expectedPosition)
|
||||||
|
@ -0,0 +1,155 @@
|
|||||||
|
// 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.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)
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -232,7 +232,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
|
|
||||||
public class TestSpectatorStreamingClient : SpectatorStreamingClient
|
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;
|
public new BindableList<int> PlayingUsers => (BindableList<int>)base.PlayingUsers;
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
|
|
||||||
public readonly BindableList<Room> Rooms = new BindableList<Room>();
|
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;
|
IBindableList<Room> IRoomManager.Rooms => Rooms;
|
||||||
|
|
||||||
|
@ -4,7 +4,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Game.Online.Multiplayer;
|
|
||||||
using osu.Game.Online.Multiplayer.RoomStatuses;
|
using osu.Game.Online.Multiplayer.RoomStatuses;
|
||||||
using osu.Game.Screens.Multi.Lounge.Components;
|
using osu.Game.Screens.Multi.Lounge.Components;
|
||||||
using osu.Game.Users;
|
using osu.Game.Users;
|
||||||
@ -14,10 +13,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
public class TestSceneLoungeRoomInfo : MultiplayerTestScene
|
public class TestSceneLoungeRoomInfo : MultiplayerTestScene
|
||||||
{
|
{
|
||||||
[SetUp]
|
[SetUp]
|
||||||
public void Setup() => Schedule(() =>
|
public new void Setup() => Schedule(() =>
|
||||||
{
|
{
|
||||||
Room = new Room();
|
|
||||||
|
|
||||||
Child = new RoomInfo
|
Child = new RoomInfo
|
||||||
{
|
{
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
|
@ -24,10 +24,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
private RulesetStore rulesetStore { get; set; }
|
private RulesetStore rulesetStore { get; set; }
|
||||||
|
|
||||||
[SetUp]
|
[SetUp]
|
||||||
public void Setup() => Schedule(() =>
|
public new void Setup() => Schedule(() =>
|
||||||
{
|
{
|
||||||
Room = new Room();
|
|
||||||
|
|
||||||
Child = new MatchBeatmapDetailArea
|
Child = new MatchBeatmapDetailArea
|
||||||
{
|
{
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using NUnit.Framework;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Online.Multiplayer;
|
using osu.Game.Online.Multiplayer;
|
||||||
using osu.Game.Rulesets.Osu;
|
using osu.Game.Rulesets.Osu;
|
||||||
@ -14,7 +15,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
{
|
{
|
||||||
public TestSceneMatchHeader()
|
public TestSceneMatchHeader()
|
||||||
{
|
{
|
||||||
Room = new Room();
|
Child = new Header();
|
||||||
|
}
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public new void Setup() => Schedule(() =>
|
||||||
|
{
|
||||||
Room.Playlist.Add(new PlaylistItem
|
Room.Playlist.Add(new PlaylistItem
|
||||||
{
|
{
|
||||||
Beatmap =
|
Beatmap =
|
||||||
@ -41,8 +47,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
|
|
||||||
Room.Name.Value = "A very awesome room";
|
Room.Name.Value = "A very awesome room";
|
||||||
Room.Host.Value = new User { Id = 2, Username = "peppy" };
|
Room.Host.Value = new User { Id = 2, Username = "peppy" };
|
||||||
|
});
|
||||||
Child = new Header();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,10 +3,10 @@
|
|||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Game.Online.API;
|
using osu.Game.Online.API;
|
||||||
using osu.Game.Online.Multiplayer;
|
|
||||||
using osu.Game.Screens.Multi.Match.Components;
|
using osu.Game.Screens.Multi.Match.Components;
|
||||||
using osu.Game.Users;
|
using osu.Game.Users;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
@ -19,8 +19,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
|
|
||||||
public TestSceneMatchLeaderboard()
|
public TestSceneMatchLeaderboard()
|
||||||
{
|
{
|
||||||
Room = new Room { RoomID = { Value = 3 } };
|
|
||||||
|
|
||||||
Add(new MatchLeaderboard
|
Add(new MatchLeaderboard
|
||||||
{
|
{
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
@ -40,6 +38,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
api.Queue(req);
|
api.Queue(req);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public new void Setup() => Schedule(() =>
|
||||||
|
{
|
||||||
|
Room.RoomID.Value = 3;
|
||||||
|
});
|
||||||
|
|
||||||
private class GetRoomScoresRequest : APIRequest<List<RoomScore>>
|
private class GetRoomScoresRequest : APIRequest<List<RoomScore>>
|
||||||
{
|
{
|
||||||
protected override string Target => "rooms/3/leaderboard";
|
protected override string Target => "rooms/3/leaderboard";
|
||||||
|
@ -23,10 +23,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
private TestRoomSettings settings;
|
private TestRoomSettings settings;
|
||||||
|
|
||||||
[SetUp]
|
[SetUp]
|
||||||
public void Setup() => Schedule(() =>
|
public new void Setup() => Schedule(() =>
|
||||||
{
|
{
|
||||||
Room = new Room();
|
|
||||||
|
|
||||||
settings = new TestRoomSettings
|
settings = new TestRoomSettings
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
@ -133,7 +131,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
remove { }
|
remove { }
|
||||||
}
|
}
|
||||||
|
|
||||||
public Bindable<bool> InitialRoomsReceived { get; } = new Bindable<bool>(true);
|
public IBindable<bool> InitialRoomsReceived { get; } = new Bindable<bool>(true);
|
||||||
|
|
||||||
public IBindableList<Room> Rooms => null;
|
public IBindableList<Room> Rooms => null;
|
||||||
|
|
||||||
|
@ -14,7 +14,6 @@ using osu.Framework.Platform;
|
|||||||
using osu.Framework.Screens;
|
using osu.Framework.Screens;
|
||||||
using osu.Framework.Utils;
|
using osu.Framework.Utils;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Online.Multiplayer;
|
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Rulesets.Osu;
|
using osu.Game.Rulesets.Osu;
|
||||||
@ -94,12 +93,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
AddUntilStep("wait for present", () => songSelect.IsCurrentScreen());
|
AddUntilStep("wait for present", () => songSelect.IsCurrentScreen());
|
||||||
}
|
}
|
||||||
|
|
||||||
[SetUp]
|
|
||||||
public void Setup() => Schedule(() =>
|
|
||||||
{
|
|
||||||
Room = new Room();
|
|
||||||
});
|
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestItemAddedIfEmptyOnStart()
|
public void TestItemAddedIfEmptyOnStart()
|
||||||
{
|
{
|
||||||
|
@ -45,12 +45,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
manager.Import(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo.BeatmapSet).Wait();
|
manager.Import(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo.BeatmapSet).Wait();
|
||||||
}
|
}
|
||||||
|
|
||||||
[SetUp]
|
|
||||||
public void Setup() => Schedule(() =>
|
|
||||||
{
|
|
||||||
Room = new Room();
|
|
||||||
});
|
|
||||||
|
|
||||||
[SetUpSteps]
|
[SetUpSteps]
|
||||||
public void SetupSteps()
|
public void SetupSteps()
|
||||||
{
|
{
|
||||||
@ -157,7 +151,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
remove => throw new NotImplementedException();
|
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>();
|
public IBindableList<Room> Rooms { get; } = new BindableList<Room>();
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Game.Overlays;
|
using osu.Game.Overlays;
|
||||||
|
using osu.Game.Screens.Multi.Timeshift;
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.Multiplayer
|
namespace osu.Game.Tests.Visual.Multiplayer
|
||||||
{
|
{
|
||||||
@ -17,7 +18,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
|
|
||||||
public TestSceneMultiScreen()
|
public TestSceneMultiScreen()
|
||||||
{
|
{
|
||||||
Screens.Multi.Multiplayer multi = new Screens.Multi.Multiplayer();
|
var multi = new TimeshiftMultiplayer();
|
||||||
|
|
||||||
AddStep("show", () => LoadScreen(multi));
|
AddStep("show", () => LoadScreen(multi));
|
||||||
AddUntilStep("wait for loaded", () => multi.IsLoaded);
|
AddUntilStep("wait for loaded", () => multi.IsLoaded);
|
||||||
|
@ -1,50 +0,0 @@
|
|||||||
// 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.Game.Online.Multiplayer;
|
|
||||||
using osu.Game.Screens.Multi.Components;
|
|
||||||
using osuTK;
|
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.Multiplayer
|
|
||||||
{
|
|
||||||
public class TestSceneOverlinedParticipants : MultiplayerTestScene
|
|
||||||
{
|
|
||||||
protected override bool UseOnlineAPI => true;
|
|
||||||
|
|
||||||
[SetUp]
|
|
||||||
public void Setup() => Schedule(() =>
|
|
||||||
{
|
|
||||||
Room = new Room { RoomID = { Value = 7 } };
|
|
||||||
});
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void TestHorizontalLayout()
|
|
||||||
{
|
|
||||||
AddStep("create component", () =>
|
|
||||||
{
|
|
||||||
Child = new ParticipantsDisplay(Direction.Horizontal)
|
|
||||||
{
|
|
||||||
Anchor = Anchor.Centre,
|
|
||||||
Origin = Anchor.Centre,
|
|
||||||
Width = 500,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void TestVerticalLayout()
|
|
||||||
{
|
|
||||||
AddStep("create component", () =>
|
|
||||||
{
|
|
||||||
Child = new ParticipantsDisplay(Direction.Vertical)
|
|
||||||
{
|
|
||||||
Anchor = Anchor.Centre,
|
|
||||||
Origin = Anchor.Centre,
|
|
||||||
Size = new Vector2(500)
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,39 +0,0 @@
|
|||||||
// 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.Graphics;
|
|
||||||
using osu.Game.Online.Multiplayer;
|
|
||||||
using osu.Game.Rulesets.Osu;
|
|
||||||
using osu.Game.Screens.Multi;
|
|
||||||
using osu.Game.Tests.Beatmaps;
|
|
||||||
using osuTK;
|
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.Multiplayer
|
|
||||||
{
|
|
||||||
public class TestSceneOverlinedPlaylist : MultiplayerTestScene
|
|
||||||
{
|
|
||||||
protected override bool UseOnlineAPI => true;
|
|
||||||
|
|
||||||
public TestSceneOverlinedPlaylist()
|
|
||||||
{
|
|
||||||
Room = new Room { RoomID = { Value = 7 } };
|
|
||||||
|
|
||||||
for (int i = 0; i < 10; i++)
|
|
||||||
{
|
|
||||||
Room.Playlist.Add(new PlaylistItem
|
|
||||||
{
|
|
||||||
ID = i,
|
|
||||||
Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo },
|
|
||||||
Ruleset = { Value = new OsuRuleset().RulesetInfo }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Add(new DrawableRoomPlaylist(false, false)
|
|
||||||
{
|
|
||||||
Anchor = Anchor.Centre,
|
|
||||||
Origin = Anchor.Centre,
|
|
||||||
Size = new Vector2(500),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -3,24 +3,55 @@
|
|||||||
|
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Game.Online.Multiplayer;
|
|
||||||
using osu.Game.Screens.Multi.Components;
|
using osu.Game.Screens.Multi.Components;
|
||||||
|
using osu.Game.Users;
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.Multiplayer
|
namespace osu.Game.Tests.Visual.Multiplayer
|
||||||
{
|
{
|
||||||
public class TestSceneParticipantsList : MultiplayerTestScene
|
public class TestSceneParticipantsList : MultiplayerTestScene
|
||||||
{
|
{
|
||||||
protected override bool UseOnlineAPI => true;
|
|
||||||
|
|
||||||
[SetUp]
|
[SetUp]
|
||||||
public void Setup() => Schedule(() =>
|
public new void Setup() => Schedule(() =>
|
||||||
{
|
{
|
||||||
Room = new Room { RoomID = { Value = 7 } };
|
Room.RoomID.Value = 7;
|
||||||
|
|
||||||
|
for (int i = 0; i < 50; i++)
|
||||||
|
{
|
||||||
|
Room.RecentParticipants.Add(new User
|
||||||
|
{
|
||||||
|
Username = "peppy",
|
||||||
|
Id = 2
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
public TestSceneParticipantsList()
|
[Test]
|
||||||
|
public void TestHorizontalLayout()
|
||||||
{
|
{
|
||||||
Add(new ParticipantsList { RelativeSizeAxes = Axes.Both });
|
AddStep("create component", () =>
|
||||||
|
{
|
||||||
|
Child = new ParticipantsDisplay(Direction.Horizontal)
|
||||||
|
{
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
Width = 0.2f,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestVerticalLayout()
|
||||||
|
{
|
||||||
|
AddStep("create component", () =>
|
||||||
|
{
|
||||||
|
Child = new ParticipantsDisplay(Direction.Vertical)
|
||||||
|
{
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
Width = 0.2f,
|
||||||
|
Height = 0.2f,
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Game.Online.Multiplayer;
|
using osu.Game.Online.Multiplayer;
|
||||||
@ -21,19 +22,28 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
{
|
{
|
||||||
new DrawableRoom(new Room
|
new DrawableRoom(new Room
|
||||||
{
|
{
|
||||||
Name = { Value = "Room 1" },
|
Name = { Value = "Open - ending in 1 day" },
|
||||||
Status = { Value = new RoomStatusOpen() }
|
Status = { Value = new RoomStatusOpen() },
|
||||||
}),
|
EndDate = { Value = DateTimeOffset.Now.AddDays(1) }
|
||||||
|
}) { MatchingFilter = true },
|
||||||
new DrawableRoom(new Room
|
new DrawableRoom(new Room
|
||||||
{
|
{
|
||||||
Name = { Value = "Room 2" },
|
Name = { Value = "Playing - ending in 1 day" },
|
||||||
Status = { Value = new RoomStatusPlaying() }
|
Status = { Value = new RoomStatusPlaying() },
|
||||||
}),
|
EndDate = { Value = DateTimeOffset.Now.AddDays(1) }
|
||||||
|
}) { MatchingFilter = true },
|
||||||
new DrawableRoom(new Room
|
new DrawableRoom(new Room
|
||||||
{
|
{
|
||||||
Name = { Value = "Room 3" },
|
Name = { Value = "Ended" },
|
||||||
Status = { Value = new RoomStatusEnded() }
|
Status = { Value = new RoomStatusEnded() },
|
||||||
}),
|
EndDate = { Value = DateTimeOffset.Now }
|
||||||
|
}) { MatchingFilter = true },
|
||||||
|
new DrawableRoom(new Room
|
||||||
|
{
|
||||||
|
Name = { Value = "Open (realtime)" },
|
||||||
|
Status = { Value = new RoomStatusOpen() },
|
||||||
|
Category = { Value = RoomCategory.Realtime }
|
||||||
|
}) { MatchingFilter = true },
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ using osu.Game.Beatmaps;
|
|||||||
using osu.Game.Overlays;
|
using osu.Game.Overlays;
|
||||||
using osu.Game.Overlays.Mods;
|
using osu.Game.Overlays.Mods;
|
||||||
using osu.Game.Overlays.Toolbar;
|
using osu.Game.Overlays.Toolbar;
|
||||||
|
using osu.Game.Screens.Multi.Timeshift;
|
||||||
using osu.Game.Screens.Play;
|
using osu.Game.Screens.Play;
|
||||||
using osu.Game.Screens.Select;
|
using osu.Game.Screens.Select;
|
||||||
using osu.Game.Screens.Select.Options;
|
using osu.Game.Screens.Select.Options;
|
||||||
@ -107,14 +108,14 @@ namespace osu.Game.Tests.Visual.Navigation
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestExitMultiWithEscape()
|
public void TestExitMultiWithEscape()
|
||||||
{
|
{
|
||||||
PushAndConfirm(() => new Screens.Multi.Multiplayer());
|
PushAndConfirm(() => new TimeshiftMultiplayer());
|
||||||
exitViaEscapeAndConfirm();
|
exitViaEscapeAndConfirm();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestExitMultiWithBackButton()
|
public void TestExitMultiWithBackButton()
|
||||||
{
|
{
|
||||||
PushAndConfirm(() => new Screens.Multi.Multiplayer());
|
PushAndConfirm(() => new TimeshiftMultiplayer());
|
||||||
exitViaBackButtonAndConfirm();
|
exitViaBackButtonAndConfirm();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,8 +69,32 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
|
|
||||||
internal class TestUserLookupCache : UserLookupCache
|
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)
|
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],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,116 @@
|
|||||||
|
// 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.Game.Online.RealtimeMultiplayer;
|
||||||
|
using osu.Game.Screens.Multi.RealtimeMultiplayer.Participants;
|
||||||
|
using osu.Game.Users;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Visual.RealtimeMultiplayer
|
||||||
|
{
|
||||||
|
public class TestSceneParticipantsList : RealtimeMultiplayerTestScene
|
||||||
|
{
|
||||||
|
[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<ReadyMark>().Single().IsPresent);
|
||||||
|
|
||||||
|
AddStep("make user ready", () => Client.ChangeState(MultiplayerUserState.Ready));
|
||||||
|
AddUntilStep("ready mark visible", () => this.ChildrenOfType<ReadyMark>().Single().IsPresent);
|
||||||
|
|
||||||
|
AddStep("make user idle", () => Client.ChangeState(MultiplayerUserState.Idle));
|
||||||
|
AddUntilStep("ready mark invisible", () => !this.ChildrenOfType<ReadyMark>().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}",
|
||||||
|
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (i % 2 == 0)
|
||||||
|
Client.ChangeUserState(i, MultiplayerUserState.Ready);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,135 @@
|
|||||||
|
// 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.Game.Beatmaps;
|
||||||
|
using osu.Game.Online.Multiplayer;
|
||||||
|
using osu.Game.Online.RealtimeMultiplayer;
|
||||||
|
using osu.Game.Rulesets;
|
||||||
|
using osu.Game.Screens.Multi.RealtimeMultiplayer;
|
||||||
|
using osu.Game.Tests.Resources;
|
||||||
|
using osu.Game.Users;
|
||||||
|
using osuTK;
|
||||||
|
using osuTK.Input;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Visual.RealtimeMultiplayer
|
||||||
|
{
|
||||||
|
public class TestSceneRealtimeReadyButton : RealtimeMultiplayerTestScene
|
||||||
|
{
|
||||||
|
private RealtimeReadyButton 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 RealtimeReadyButton
|
||||||
|
{
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
Size = new Vector2(200, 50),
|
||||||
|
SelectedItem =
|
||||||
|
{
|
||||||
|
Value = new PlaylistItem
|
||||||
|
{
|
||||||
|
Beatmap = { Value = beatmap },
|
||||||
|
Ruleset = { Value = beatmap.Ruleset }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Client.AddUser(API.LocalUser.Value);
|
||||||
|
});
|
||||||
|
|
||||||
|
[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);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addClickButtonStep() => AddStep("click button", () =>
|
||||||
|
{
|
||||||
|
InputManager.MoveMouseTo(button);
|
||||||
|
InputManager.Click(MouseButton.Left);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -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.Multiplayer;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Visual.RealtimeMultiplayer
|
||||||
|
{
|
||||||
|
[HeadlessTest]
|
||||||
|
public class TestSceneRealtimeRoomManager : MultiplayerTestScene
|
||||||
|
{
|
||||||
|
private TestRealtimeRoomContainer roomContainer;
|
||||||
|
private TestRealtimeRoomManager 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 TestRealtimeRoomManager createRoomManager()
|
||||||
|
{
|
||||||
|
Child = roomContainer = new TestRealtimeRoomContainer
|
||||||
|
{
|
||||||
|
RoomManager =
|
||||||
|
{
|
||||||
|
TimeBetweenListingPolls = { Value = 1 },
|
||||||
|
TimeBetweenSelectionPolls = { Value = 1 }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return roomManager;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -7,16 +7,16 @@ namespace osu.Game.Online.API.Requests
|
|||||||
{
|
{
|
||||||
public class GetBeatmapSetRequest : APIRequest<APIBeatmapSet>
|
public class GetBeatmapSetRequest : APIRequest<APIBeatmapSet>
|
||||||
{
|
{
|
||||||
private readonly int id;
|
public readonly int ID;
|
||||||
private readonly BeatmapSetLookupType type;
|
public readonly BeatmapSetLookupType Type;
|
||||||
|
|
||||||
public GetBeatmapSetRequest(int id, BeatmapSetLookupType type = BeatmapSetLookupType.SetId)
|
public GetBeatmapSetRequest(int id, BeatmapSetLookupType type = BeatmapSetLookupType.SetId)
|
||||||
{
|
{
|
||||||
this.id = id;
|
ID = id;
|
||||||
this.type = type;
|
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
|
public enum BeatmapSetLookupType
|
||||||
|
@ -80,7 +80,7 @@ namespace osu.Game.Online.API.Requests.Responses
|
|||||||
|
|
||||||
public BeatmapSetInfo ToBeatmapSet(RulesetStore rulesets)
|
public BeatmapSetInfo ToBeatmapSet(RulesetStore rulesets)
|
||||||
{
|
{
|
||||||
return new BeatmapSetInfo
|
var beatmapSet = new BeatmapSetInfo
|
||||||
{
|
{
|
||||||
OnlineBeatmapSetID = OnlineBeatmapSetID,
|
OnlineBeatmapSetID = OnlineBeatmapSetID,
|
||||||
Metadata = this,
|
Metadata = this,
|
||||||
@ -104,8 +104,17 @@ namespace osu.Game.Online.API.Requests.Responses
|
|||||||
Genre = genre,
|
Genre = genre,
|
||||||
Language = language
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -57,7 +57,7 @@ namespace osu.Game.Online.Chat
|
|||||||
{
|
{
|
||||||
CurrentChannel.ValueChanged += currentChannelChanged;
|
CurrentChannel.ValueChanged += currentChannelChanged;
|
||||||
|
|
||||||
HighPollRate.BindValueChanged(enabled => TimeBetweenPolls = enabled.NewValue ? 1000 : 6000, true);
|
HighPollRate.BindValueChanged(enabled => TimeBetweenPolls.Value = enabled.NewValue ? 1000 : 6000, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -24,8 +24,8 @@ using osu.Game.Scoring;
|
|||||||
using osu.Game.Users.Drawables;
|
using osu.Game.Users.Drawables;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
using Humanizer;
|
|
||||||
using osu.Game.Online.API;
|
using osu.Game.Online.API;
|
||||||
|
using osu.Game.Utils;
|
||||||
|
|
||||||
namespace osu.Game.Online.Leaderboards
|
namespace osu.Game.Online.Leaderboards
|
||||||
{
|
{
|
||||||
@ -358,7 +358,7 @@ namespace osu.Game.Online.Leaderboards
|
|||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
Font = OsuFont.GetFont(size: 20, italics: true),
|
Font = OsuFont.GetFont(size: 20, italics: true),
|
||||||
Text = rank == null ? "-" : rank.Value.ToMetric(decimals: rank < 100000 ? 1 : 0),
|
Text = rank == null ? "-" : rank.Value.FormatRank()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,11 +10,11 @@ namespace osu.Game.Online.Multiplayer
|
|||||||
{
|
{
|
||||||
public class CreateRoomRequest : APIRequest<APICreatedRoom>
|
public class CreateRoomRequest : APIRequest<APICreatedRoom>
|
||||||
{
|
{
|
||||||
private readonly Room room;
|
public readonly Room Room;
|
||||||
|
|
||||||
public CreateRoomRequest(Room room)
|
public CreateRoomRequest(Room room)
|
||||||
{
|
{
|
||||||
this.room = room;
|
Room = room;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override WebRequest CreateWebRequest()
|
protected override WebRequest CreateWebRequest()
|
||||||
@ -24,7 +24,7 @@ namespace osu.Game.Online.Multiplayer
|
|||||||
req.ContentType = "application/json";
|
req.ContentType = "application/json";
|
||||||
req.Method = HttpMethod.Post;
|
req.Method = HttpMethod.Post;
|
||||||
|
|
||||||
req.AddRaw(JsonConvert.SerializeObject(room));
|
req.AddRaw(JsonConvert.SerializeObject(Room));
|
||||||
|
|
||||||
return req;
|
return req;
|
||||||
}
|
}
|
||||||
|
@ -7,13 +7,13 @@ namespace osu.Game.Online.Multiplayer
|
|||||||
{
|
{
|
||||||
public class GetRoomRequest : APIRequest<Room>
|
public class GetRoomRequest : APIRequest<Room>
|
||||||
{
|
{
|
||||||
private readonly int roomId;
|
public readonly int RoomId;
|
||||||
|
|
||||||
public GetRoomRequest(int roomId)
|
public GetRoomRequest(int roomId)
|
||||||
{
|
{
|
||||||
this.roomId = roomId;
|
RoomId = roomId;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override string Target => $"rooms/{roomId}";
|
protected override string Target => $"rooms/{RoomId}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ using System.Linq;
|
|||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Game.IO.Serialization.Converters;
|
||||||
using osu.Game.Online.Multiplayer.GameTypes;
|
using osu.Game.Online.Multiplayer.GameTypes;
|
||||||
using osu.Game.Online.Multiplayer.RoomStatuses;
|
using osu.Game.Online.Multiplayer.RoomStatuses;
|
||||||
using osu.Game.Users;
|
using osu.Game.Users;
|
||||||
@ -35,12 +36,21 @@ namespace osu.Game.Online.Multiplayer
|
|||||||
public readonly Bindable<int> ChannelId = new Bindable<int>();
|
public readonly Bindable<int> ChannelId = new Bindable<int>();
|
||||||
|
|
||||||
[Cached]
|
[Cached]
|
||||||
[JsonProperty("category")]
|
[JsonIgnore]
|
||||||
public readonly Bindable<RoomCategory> Category = new Bindable<RoomCategory>();
|
public readonly Bindable<RoomCategory> Category = new Bindable<RoomCategory>();
|
||||||
|
|
||||||
|
// Todo: osu-framework bug (https://github.com/ppy/osu-framework/issues/4106)
|
||||||
|
[JsonProperty("category")]
|
||||||
|
[JsonConverter(typeof(SnakeCaseStringEnumConverter))]
|
||||||
|
private RoomCategory category
|
||||||
|
{
|
||||||
|
get => Category.Value;
|
||||||
|
set => Category.Value = value;
|
||||||
|
}
|
||||||
|
|
||||||
[Cached]
|
[Cached]
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public readonly Bindable<TimeSpan> Duration = new Bindable<TimeSpan>(TimeSpan.FromMinutes(30));
|
public readonly Bindable<TimeSpan?> Duration = new Bindable<TimeSpan?>();
|
||||||
|
|
||||||
[Cached]
|
[Cached]
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
@ -67,27 +77,26 @@ namespace osu.Game.Online.Multiplayer
|
|||||||
public readonly BindableList<User> RecentParticipants = new BindableList<User>();
|
public readonly BindableList<User> RecentParticipants = new BindableList<User>();
|
||||||
|
|
||||||
[Cached]
|
[Cached]
|
||||||
|
[JsonProperty("participant_count")]
|
||||||
public readonly Bindable<int> ParticipantCount = new Bindable<int>();
|
public readonly Bindable<int> ParticipantCount = new Bindable<int>();
|
||||||
|
|
||||||
// todo: TEMPORARY
|
|
||||||
[JsonProperty("participant_count")]
|
|
||||||
private int? participantCount
|
|
||||||
{
|
|
||||||
get => ParticipantCount.Value;
|
|
||||||
set => ParticipantCount.Value = value ?? 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
[JsonProperty("duration")]
|
[JsonProperty("duration")]
|
||||||
private int duration
|
private int? duration
|
||||||
{
|
{
|
||||||
get => (int)Duration.Value.TotalMinutes;
|
get => (int?)Duration.Value?.TotalMinutes;
|
||||||
set => Duration.Value = TimeSpan.FromMinutes(value);
|
set
|
||||||
|
{
|
||||||
|
if (value == null)
|
||||||
|
Duration.Value = null;
|
||||||
|
else
|
||||||
|
Duration.Value = TimeSpan.FromMinutes(value.Value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only supports retrieval for now
|
// Only supports retrieval for now
|
||||||
[Cached]
|
[Cached]
|
||||||
[JsonProperty("ends_at")]
|
[JsonProperty("ends_at")]
|
||||||
public readonly Bindable<DateTimeOffset> EndDate = new Bindable<DateTimeOffset>();
|
public readonly Bindable<DateTimeOffset?> EndDate = new Bindable<DateTimeOffset?>();
|
||||||
|
|
||||||
// Todo: Find a better way to do this (https://github.com/ppy/osu-framework/issues/1930)
|
// Todo: Find a better way to do this (https://github.com/ppy/osu-framework/issues/1930)
|
||||||
[JsonProperty("max_attempts", DefaultValueHandling = DefaultValueHandling.Ignore)]
|
[JsonProperty("max_attempts", DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||||
@ -133,7 +142,7 @@ namespace osu.Game.Online.Multiplayer
|
|||||||
ParticipantCount.Value = other.ParticipantCount.Value;
|
ParticipantCount.Value = other.ParticipantCount.Value;
|
||||||
EndDate.Value = other.EndDate.Value;
|
EndDate.Value = other.EndDate.Value;
|
||||||
|
|
||||||
if (DateTimeOffset.Now >= EndDate.Value)
|
if (EndDate.Value != null && DateTimeOffset.Now >= EndDate.Value)
|
||||||
Status.Value = new RoomStatusEnded();
|
Status.Value = new RoomStatusEnded();
|
||||||
|
|
||||||
if (!Playlist.SequenceEqual(other.Playlist))
|
if (!Playlist.SequenceEqual(other.Playlist))
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Threading;
|
using osu.Framework.Threading;
|
||||||
|
|
||||||
@ -19,22 +20,11 @@ namespace osu.Game.Online
|
|||||||
|
|
||||||
private bool pollingActive;
|
private bool pollingActive;
|
||||||
|
|
||||||
private double timeBetweenPolls;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The time in milliseconds to wait between polls.
|
/// The time in milliseconds to wait between polls.
|
||||||
/// Setting to zero stops all polling.
|
/// Setting to zero stops all polling.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public double TimeBetweenPolls
|
public readonly Bindable<double> TimeBetweenPolls = new Bindable<double>();
|
||||||
{
|
|
||||||
get => timeBetweenPolls;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
timeBetweenPolls = value;
|
|
||||||
scheduledPoll?.Cancel();
|
|
||||||
pollIfNecessary();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// <param name="timeBetweenPolls">The initial time in milliseconds to wait between polls. Setting to zero stops all polling.</param>
|
||||||
protected PollingComponent(double timeBetweenPolls = 0)
|
protected PollingComponent(double timeBetweenPolls = 0)
|
||||||
{
|
{
|
||||||
TimeBetweenPolls = timeBetweenPolls;
|
TimeBetweenPolls.BindValueChanged(_ =>
|
||||||
|
{
|
||||||
|
scheduledPoll?.Cancel();
|
||||||
|
pollIfNecessary();
|
||||||
|
});
|
||||||
|
|
||||||
|
TimeBetweenPolls.Value = timeBetweenPolls;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
@ -60,7 +56,7 @@ namespace osu.Game.Online
|
|||||||
if (pollingActive) return false;
|
if (pollingActive) return false;
|
||||||
|
|
||||||
// don't try polling if the time between polls hasn't been set.
|
// 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)
|
if (!lastTimePolled.HasValue)
|
||||||
{
|
{
|
||||||
@ -68,7 +64,7 @@ namespace osu.Game.Online
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Time.Current - lastTimePolled.Value > timeBetweenPolls)
|
if (Time.Current - lastTimePolled.Value > TimeBetweenPolls.Value)
|
||||||
{
|
{
|
||||||
doPoll();
|
doPoll();
|
||||||
return true;
|
return true;
|
||||||
@ -99,7 +95,7 @@ namespace osu.Game.Online
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public void PollImmediately()
|
public void PollImmediately()
|
||||||
{
|
{
|
||||||
lastTimePolled = Time.Current - timeBetweenPolls;
|
lastTimePolled = Time.Current - TimeBetweenPolls.Value;
|
||||||
scheduleNextPoll();
|
scheduleNextPoll();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -121,7 +117,7 @@ namespace osu.Game.Online
|
|||||||
|
|
||||||
double lastPollDuration = lastTimePolled.HasValue ? Time.Current - lastTimePolled.Value : 0;
|
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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
408
osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs
Normal file
408
osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs
Normal file
@ -0,0 +1,408 @@
|
|||||||
|
// 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.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Extensions.ObjectExtensions;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Database;
|
||||||
|
using osu.Game.Online.API;
|
||||||
|
using osu.Game.Online.API.Requests;
|
||||||
|
using osu.Game.Online.API.Requests.Responses;
|
||||||
|
using osu.Game.Online.Multiplayer;
|
||||||
|
using osu.Game.Online.Multiplayer.RoomStatuses;
|
||||||
|
using osu.Game.Rulesets;
|
||||||
|
using osu.Game.Users;
|
||||||
|
using osu.Game.Utils;
|
||||||
|
|
||||||
|
namespace osu.Game.Online.RealtimeMultiplayer
|
||||||
|
{
|
||||||
|
public abstract class StatefulMultiplayerClient : Component, IMultiplayerClient, IMultiplayerRoomServer
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Invoked when any change occurs to the multiplayer room.
|
||||||
|
/// </summary>
|
||||||
|
public event Action? RoomChanged;
|
||||||
|
|
||||||
|
/// <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;
|
||||||
|
|
||||||
|
/// <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);
|
||||||
|
|
||||||
|
foreach (var user in Room.Users)
|
||||||
|
await PopulateUser(user);
|
||||||
|
|
||||||
|
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()
|
||||||
|
{
|
||||||
|
if (Room == null)
|
||||||
|
return Task.CompletedTask;
|
||||||
|
|
||||||
|
apiRoom = null;
|
||||||
|
Room = null;
|
||||||
|
|
||||||
|
Schedule(() => RoomChanged?.Invoke());
|
||||||
|
|
||||||
|
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 void ChangeSettings(Optional<string> name = default, Optional<PlaylistItem> item = default)
|
||||||
|
{
|
||||||
|
if (Room == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// 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
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
Schedule(() =>
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
RoomChanged?.Invoke();
|
||||||
|
});
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task IMultiplayerClient.UserJoined(MultiplayerRoomUser user)
|
||||||
|
{
|
||||||
|
if (Room == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
await PopulateUser(user);
|
||||||
|
|
||||||
|
Schedule(() =>
|
||||||
|
{
|
||||||
|
if (Room == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Room.Users.Add(user);
|
||||||
|
|
||||||
|
RoomChanged?.Invoke();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Task IMultiplayerClient.UserLeft(MultiplayerRoomUser user)
|
||||||
|
{
|
||||||
|
if (Room == null)
|
||||||
|
return Task.CompletedTask;
|
||||||
|
|
||||||
|
Schedule(() =>
|
||||||
|
{
|
||||||
|
if (Room == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Room.Users.Remove(user);
|
||||||
|
PlayingUsers.Remove(user.UserID);
|
||||||
|
|
||||||
|
RoomChanged?.Invoke();
|
||||||
|
});
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
Task IMultiplayerClient.HostChanged(int userId)
|
||||||
|
{
|
||||||
|
if (Room == null)
|
||||||
|
return Task.CompletedTask;
|
||||||
|
|
||||||
|
Schedule(() =>
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
|
||||||
|
RoomChanged?.Invoke();
|
||||||
|
});
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
Schedule(() =>
|
||||||
|
{
|
||||||
|
if (Room == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Room.Users.Single(u => u.UserID == userId).State = state;
|
||||||
|
|
||||||
|
if (state != MultiplayerUserState.Playing)
|
||||||
|
PlayingUsers.Remove(userId);
|
||||||
|
|
||||||
|
RoomChanged?.Invoke();
|
||||||
|
});
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
Task IMultiplayerClient.LoadRequested()
|
||||||
|
{
|
||||||
|
if (Room == null)
|
||||||
|
return Task.CompletedTask;
|
||||||
|
|
||||||
|
Schedule(() =>
|
||||||
|
{
|
||||||
|
if (Room == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
LoadRequested?.Invoke();
|
||||||
|
});
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
Task IMultiplayerClient.MatchStarted()
|
||||||
|
{
|
||||||
|
if (Room == null)
|
||||||
|
return Task.CompletedTask;
|
||||||
|
|
||||||
|
Schedule(() =>
|
||||||
|
{
|
||||||
|
if (Room == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
PlayingUsers.AddRange(Room.Users.Where(u => u.State == MultiplayerUserState.Playing).Select(u => u.UserID));
|
||||||
|
|
||||||
|
MatchStarted?.Invoke();
|
||||||
|
});
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
Task IMultiplayerClient.ResultsReady()
|
||||||
|
{
|
||||||
|
if (Room == null)
|
||||||
|
return Task.CompletedTask;
|
||||||
|
|
||||||
|
Schedule(() =>
|
||||||
|
{
|
||||||
|
if (Room == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
ResultsReady?.Invoke();
|
||||||
|
});
|
||||||
|
|
||||||
|
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>
|
||||||
|
/// 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;
|
||||||
|
|
||||||
|
// Update a few properties of the room instantaneously.
|
||||||
|
Schedule(() =>
|
||||||
|
{
|
||||||
|
if (Room == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Debug.Assert(apiRoom != null);
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
RoomChanged?.Invoke();
|
||||||
|
});
|
||||||
|
|
||||||
|
var req = new GetBeatmapSetRequest(settings.BeatmapID, BeatmapSetLookupType.BeatmapId);
|
||||||
|
req.Success += res => updatePlaylist(settings, res);
|
||||||
|
|
||||||
|
api.Queue(req);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -303,7 +303,8 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
|||||||
samplesBindable.CollectionChanged -= onSamplesChanged;
|
samplesBindable.CollectionChanged -= onSamplesChanged;
|
||||||
|
|
||||||
// Release the samples for other hitobjects to use.
|
// Release the samples for other hitobjects to use.
|
||||||
Samples.Samples = null;
|
if (Samples != null)
|
||||||
|
Samples.Samples = null;
|
||||||
|
|
||||||
if (nestedHitObjects.IsValueCreated)
|
if (nestedHitObjects.IsValueCreated)
|
||||||
{
|
{
|
||||||
|
@ -68,7 +68,12 @@ namespace osu.Game.Rulesets.Scoring
|
|||||||
private readonly double comboPortion;
|
private readonly double comboPortion;
|
||||||
|
|
||||||
private int maxAchievableCombo;
|
private int maxAchievableCombo;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The maximum achievable base score.
|
||||||
|
/// </summary>
|
||||||
private double maxBaseScore;
|
private double maxBaseScore;
|
||||||
|
|
||||||
private double rollingMaxBaseScore;
|
private double rollingMaxBaseScore;
|
||||||
private double baseScore;
|
private double baseScore;
|
||||||
|
|
||||||
@ -196,8 +201,8 @@ namespace osu.Game.Rulesets.Scoring
|
|||||||
private double getScore(ScoringMode mode)
|
private double getScore(ScoringMode mode)
|
||||||
{
|
{
|
||||||
return GetScore(mode, maxAchievableCombo,
|
return GetScore(mode, maxAchievableCombo,
|
||||||
maxBaseScore > 0 ? baseScore / maxBaseScore : 0,
|
calculateAccuracyRatio(baseScore),
|
||||||
maxAchievableCombo > 0 ? (double)HighestCombo.Value / maxAchievableCombo : 1,
|
calculateComboRatio(HighestCombo.Value),
|
||||||
scoreResultCounts);
|
scoreResultCounts);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -227,6 +232,37 @@ namespace osu.Game.Rulesets.Scoring
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Given a minimal set of inputs, return the computed score and accuracy for the tracked beatmap / mods combination.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="mode">The <see cref="ScoringMode"/> to compute the total score in.</param>
|
||||||
|
/// <param name="maxCombo">The maximum combo achievable in the beatmap.</param>
|
||||||
|
/// <param name="statistics">Statistics to be used for calculating accuracy, bonus score, etc.</param>
|
||||||
|
/// <returns>The computed score and accuracy for provided inputs.</returns>
|
||||||
|
public (double score, double accuracy) GetScoreAndAccuracy(ScoringMode mode, int maxCombo, Dictionary<HitResult, int> statistics)
|
||||||
|
{
|
||||||
|
// calculate base score from statistics pairs
|
||||||
|
int computedBaseScore = 0;
|
||||||
|
|
||||||
|
foreach (var pair in statistics)
|
||||||
|
{
|
||||||
|
if (!pair.Key.AffectsAccuracy())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
computedBaseScore += Judgement.ToNumericResult(pair.Key) * pair.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
double accuracy = calculateAccuracyRatio(computedBaseScore);
|
||||||
|
double comboRatio = calculateComboRatio(maxCombo);
|
||||||
|
|
||||||
|
double score = GetScore(mode, maxAchievableCombo, accuracy, comboRatio, scoreResultCounts);
|
||||||
|
|
||||||
|
return (score, accuracy);
|
||||||
|
}
|
||||||
|
|
||||||
|
private double calculateAccuracyRatio(double baseScore) => maxBaseScore > 0 ? baseScore / maxBaseScore : 0;
|
||||||
|
private double calculateComboRatio(int maxCombo) => maxAchievableCombo > 0 ? (double)maxCombo / maxAchievableCombo : 1;
|
||||||
|
|
||||||
private double getBonusScore(Dictionary<HitResult, int> statistics)
|
private double getBonusScore(Dictionary<HitResult, int> statistics)
|
||||||
=> statistics.GetOrDefault(HitResult.SmallBonus) * Judgement.SMALL_BONUS_SCORE
|
=> statistics.GetOrDefault(HitResult.SmallBonus) * Judgement.SMALL_BONUS_SCORE
|
||||||
+ statistics.GetOrDefault(HitResult.LargeBonus) * Judgement.LARGE_BONUS_SCORE;
|
+ statistics.GetOrDefault(HitResult.LargeBonus) * Judgement.LARGE_BONUS_SCORE;
|
||||||
|
@ -13,6 +13,7 @@ using osu.Framework.Bindables;
|
|||||||
using osu.Framework.Graphics.OpenGL.Textures;
|
using osu.Framework.Graphics.OpenGL.Textures;
|
||||||
using osu.Framework.Graphics.Textures;
|
using osu.Framework.Graphics.Textures;
|
||||||
using osu.Framework.IO.Stores;
|
using osu.Framework.IO.Stores;
|
||||||
|
using osu.Framework.Platform;
|
||||||
using osu.Game.Rulesets.Configuration;
|
using osu.Game.Rulesets.Configuration;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.UI
|
namespace osu.Game.Rulesets.UI
|
||||||
@ -46,7 +47,7 @@ namespace osu.Game.Rulesets.UI
|
|||||||
|
|
||||||
if (resources != null)
|
if (resources != null)
|
||||||
{
|
{
|
||||||
TextureStore = new TextureStore(new TextureLoaderStore(new NamespacedResourceStore<byte[]>(resources, @"Textures")));
|
TextureStore = new TextureStore(parent.Get<GameHost>().CreateTextureLoaderStore(new NamespacedResourceStore<byte[]>(resources, @"Textures")));
|
||||||
CacheAs(TextureStore = new FallbackTextureStore(TextureStore, parent.Get<TextureStore>()));
|
CacheAs(TextureStore = new FallbackTextureStore(TextureStore, parent.Get<TextureStore>()));
|
||||||
|
|
||||||
SampleStore = parent.Get<AudioManager>().GetSampleStore(new NamespacedResourceStore<byte[]>(resources, @"Samples"));
|
SampleStore = parent.Get<AudioManager>().GetSampleStore(new NamespacedResourceStore<byte[]>(resources, @"Samples"));
|
||||||
|
@ -92,6 +92,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Container dragHandles;
|
||||||
private FillFlowContainer buttons;
|
private FillFlowContainer buttons;
|
||||||
|
|
||||||
public const float BORDER_RADIUS = 3;
|
public const float BORDER_RADIUS = 3;
|
||||||
@ -151,6 +152,12 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
dragHandles = new Container
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
// ensures that the centres of all drag handles line up with the middle of the selection box border.
|
||||||
|
Padding = new MarginPadding(BORDER_RADIUS / 2)
|
||||||
|
},
|
||||||
buttons = new FillFlowContainer
|
buttons = new FillFlowContainer
|
||||||
{
|
{
|
||||||
Y = 20,
|
Y = 20,
|
||||||
@ -232,7 +239,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addDragHandle(Anchor anchor) => AddInternal(new SelectionBoxDragHandle
|
private void addDragHandle(Anchor anchor) => dragHandles.Add(new SelectionBoxDragHandle
|
||||||
{
|
{
|
||||||
Anchor = anchor,
|
Anchor = anchor,
|
||||||
HandleDrag = e => OnScale?.Invoke(e.Delta, anchor),
|
HandleDrag = e => OnScale?.Invoke(e.Delta, anchor),
|
||||||
|
@ -17,7 +17,7 @@ using osu.Game.Online.API;
|
|||||||
using osu.Game.Overlays;
|
using osu.Game.Overlays;
|
||||||
using osu.Game.Screens.Backgrounds;
|
using osu.Game.Screens.Backgrounds;
|
||||||
using osu.Game.Screens.Edit;
|
using osu.Game.Screens.Edit;
|
||||||
using osu.Game.Screens.Multi;
|
using osu.Game.Screens.Multi.Timeshift;
|
||||||
using osu.Game.Screens.Select;
|
using osu.Game.Screens.Select;
|
||||||
|
|
||||||
namespace osu.Game.Screens.Menu
|
namespace osu.Game.Screens.Menu
|
||||||
@ -104,7 +104,7 @@ namespace osu.Game.Screens.Menu
|
|||||||
this.Push(new Editor());
|
this.Push(new Editor());
|
||||||
},
|
},
|
||||||
OnSolo = onSolo,
|
OnSolo = onSolo,
|
||||||
OnMulti = delegate { this.Push(new Multiplayer()); },
|
OnMulti = delegate { this.Push(new TimeshiftMultiplayer()); },
|
||||||
OnExit = confirmAndExit,
|
OnExit = confirmAndExit,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
69
osu.Game/Screens/Multi/Components/ListingPollingComponent.cs
Normal file
69
osu.Game/Screens/Multi/Components/ListingPollingComponent.cs
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
// 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.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Game.Online.Multiplayer;
|
||||||
|
using osu.Game.Screens.Multi.Lounge.Components;
|
||||||
|
|
||||||
|
namespace osu.Game.Screens.Multi.Components
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A <see cref="RoomPollingComponent"/> that polls for the lounge listing.
|
||||||
|
/// </summary>
|
||||||
|
public class ListingPollingComponent : RoomPollingComponent
|
||||||
|
{
|
||||||
|
[Resolved]
|
||||||
|
private Bindable<FilterCriteria> currentFilter { get; set; }
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private Bindable<Room> selectedRoom { get; set; }
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load()
|
||||||
|
{
|
||||||
|
currentFilter.BindValueChanged(_ =>
|
||||||
|
{
|
||||||
|
NotifyRoomsReceived(null);
|
||||||
|
if (IsLoaded)
|
||||||
|
PollImmediately();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private GetRoomsRequest pollReq;
|
||||||
|
|
||||||
|
protected override Task Poll()
|
||||||
|
{
|
||||||
|
if (!API.IsLoggedIn)
|
||||||
|
return base.Poll();
|
||||||
|
|
||||||
|
var tcs = new TaskCompletionSource<bool>();
|
||||||
|
|
||||||
|
pollReq?.Cancel();
|
||||||
|
pollReq = new GetRoomsRequest(currentFilter.Value.Status, currentFilter.Value.Category);
|
||||||
|
|
||||||
|
pollReq.Success += result =>
|
||||||
|
{
|
||||||
|
for (int i = 0; i < result.Count; i++)
|
||||||
|
{
|
||||||
|
if (result[i].RoomID.Value == selectedRoom.Value?.RoomID.Value)
|
||||||
|
{
|
||||||
|
// The listing request always has less information than the opened room, so don't include it.
|
||||||
|
result[i] = selectedRoom.Value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NotifyRoomsReceived(result);
|
||||||
|
tcs.SetResult(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
pollReq.Failure += _ => tcs.SetResult(false);
|
||||||
|
|
||||||
|
API.Queue(pollReq);
|
||||||
|
|
||||||
|
return tcs.Task;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -11,28 +11,22 @@ using osu.Game.Graphics;
|
|||||||
using osu.Game.Graphics.UserInterface;
|
using osu.Game.Graphics.UserInterface;
|
||||||
using osu.Game.Online.Multiplayer;
|
using osu.Game.Online.Multiplayer;
|
||||||
|
|
||||||
namespace osu.Game.Screens.Multi.Match.Components
|
namespace osu.Game.Screens.Multi.Components
|
||||||
{
|
{
|
||||||
public class ReadyButton : TriangleButton
|
public abstract class ReadyButton : TriangleButton
|
||||||
{
|
{
|
||||||
public readonly Bindable<PlaylistItem> SelectedItem = new Bindable<PlaylistItem>();
|
public readonly Bindable<PlaylistItem> SelectedItem = new Bindable<PlaylistItem>();
|
||||||
|
|
||||||
[Resolved(typeof(Room), nameof(Room.EndDate))]
|
public new readonly BindableBool Enabled = new BindableBool();
|
||||||
private Bindable<DateTimeOffset> endDate { get; set; }
|
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private IBindable<WorkingBeatmap> gameBeatmap { get; set; }
|
protected IBindable<WorkingBeatmap> GameBeatmap { get; private set; }
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private BeatmapManager beatmaps { get; set; }
|
private BeatmapManager beatmaps { get; set; }
|
||||||
|
|
||||||
private bool hasBeatmap;
|
private bool hasBeatmap;
|
||||||
|
|
||||||
public ReadyButton()
|
|
||||||
{
|
|
||||||
Text = "Start";
|
|
||||||
}
|
|
||||||
|
|
||||||
private IBindable<WeakReference<BeatmapSetInfo>> managerUpdated;
|
private IBindable<WeakReference<BeatmapSetInfo>> managerUpdated;
|
||||||
private IBindable<WeakReference<BeatmapSetInfo>> managerRemoved;
|
private IBindable<WeakReference<BeatmapSetInfo>> managerRemoved;
|
||||||
|
|
||||||
@ -45,10 +39,6 @@ namespace osu.Game.Screens.Multi.Match.Components
|
|||||||
managerRemoved.BindValueChanged(beatmapRemoved);
|
managerRemoved.BindValueChanged(beatmapRemoved);
|
||||||
|
|
||||||
SelectedItem.BindValueChanged(item => updateSelectedItem(item.NewValue), true);
|
SelectedItem.BindValueChanged(item => updateSelectedItem(item.NewValue), true);
|
||||||
|
|
||||||
BackgroundColour = colours.Green;
|
|
||||||
Triangles.ColourDark = colours.Green;
|
|
||||||
Triangles.ColourLight = colours.GreenLight;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateSelectedItem(PlaylistItem item)
|
private void updateSelectedItem(PlaylistItem item)
|
||||||
@ -94,15 +84,13 @@ namespace osu.Game.Screens.Multi.Match.Components
|
|||||||
|
|
||||||
private void updateEnabledState()
|
private void updateEnabledState()
|
||||||
{
|
{
|
||||||
if (gameBeatmap.Value == null || SelectedItem.Value == null)
|
if (GameBeatmap.Value == null || SelectedItem.Value == null)
|
||||||
{
|
{
|
||||||
Enabled.Value = false;
|
base.Enabled.Value = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool hasEnoughTime = DateTimeOffset.UtcNow.AddSeconds(30).AddMilliseconds(gameBeatmap.Value.Track.Length) < endDate.Value;
|
base.Enabled.Value = hasBeatmap && Enabled.Value;
|
||||||
|
|
||||||
Enabled.Value = hasBeatmap && hasEnoughTime;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
204
osu.Game/Screens/Multi/Components/RoomManager.cs
Normal file
204
osu.Game/Screens/Multi/Components/RoomManager.cs
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
// 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.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Logging;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Online.API;
|
||||||
|
using osu.Game.Online.Multiplayer;
|
||||||
|
using osu.Game.Rulesets;
|
||||||
|
|
||||||
|
namespace osu.Game.Screens.Multi.Components
|
||||||
|
{
|
||||||
|
public abstract class RoomManager : CompositeDrawable, IRoomManager
|
||||||
|
{
|
||||||
|
public event Action RoomsUpdated;
|
||||||
|
|
||||||
|
private readonly BindableList<Room> rooms = new BindableList<Room>();
|
||||||
|
|
||||||
|
public IBindable<bool> InitialRoomsReceived => initialRoomsReceived;
|
||||||
|
private readonly Bindable<bool> initialRoomsReceived = new Bindable<bool>();
|
||||||
|
|
||||||
|
public IBindableList<Room> Rooms => rooms;
|
||||||
|
|
||||||
|
protected IBindable<Room> JoinedRoom => joinedRoom;
|
||||||
|
private readonly Bindable<Room> joinedRoom = new Bindable<Room>();
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private RulesetStore rulesets { get; set; }
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private BeatmapManager beatmaps { get; set; }
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private IAPIProvider api { get; set; }
|
||||||
|
|
||||||
|
protected RoomManager()
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both;
|
||||||
|
|
||||||
|
InternalChildren = CreatePollingComponents().Select(p =>
|
||||||
|
{
|
||||||
|
p.RoomsReceived = onRoomsReceived;
|
||||||
|
return p;
|
||||||
|
}).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool isDisposing)
|
||||||
|
{
|
||||||
|
base.Dispose(isDisposing);
|
||||||
|
PartRoom();
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual void CreateRoom(Room room, Action<Room> onSuccess = null, Action<string> onError = null)
|
||||||
|
{
|
||||||
|
room.Host.Value = api.LocalUser.Value;
|
||||||
|
|
||||||
|
var req = new CreateRoomRequest(room);
|
||||||
|
|
||||||
|
req.Success += result =>
|
||||||
|
{
|
||||||
|
joinedRoom.Value = room;
|
||||||
|
|
||||||
|
update(room, result);
|
||||||
|
addRoom(room);
|
||||||
|
|
||||||
|
RoomsUpdated?.Invoke();
|
||||||
|
onSuccess?.Invoke(room);
|
||||||
|
};
|
||||||
|
|
||||||
|
req.Failure += exception =>
|
||||||
|
{
|
||||||
|
if (req.Result != null)
|
||||||
|
onError?.Invoke(req.Result.Error);
|
||||||
|
else
|
||||||
|
Logger.Log($"Failed to create the room: {exception}", level: LogLevel.Important);
|
||||||
|
};
|
||||||
|
|
||||||
|
api.Queue(req);
|
||||||
|
}
|
||||||
|
|
||||||
|
private JoinRoomRequest currentJoinRoomRequest;
|
||||||
|
|
||||||
|
public virtual void JoinRoom(Room room, Action<Room> onSuccess = null, Action<string> onError = null)
|
||||||
|
{
|
||||||
|
currentJoinRoomRequest?.Cancel();
|
||||||
|
currentJoinRoomRequest = new JoinRoomRequest(room);
|
||||||
|
|
||||||
|
currentJoinRoomRequest.Success += () =>
|
||||||
|
{
|
||||||
|
joinedRoom.Value = room;
|
||||||
|
onSuccess?.Invoke(room);
|
||||||
|
};
|
||||||
|
|
||||||
|
currentJoinRoomRequest.Failure += exception =>
|
||||||
|
{
|
||||||
|
if (!(exception is OperationCanceledException))
|
||||||
|
Logger.Log($"Failed to join room: {exception}", level: LogLevel.Important);
|
||||||
|
onError?.Invoke(exception.ToString());
|
||||||
|
};
|
||||||
|
|
||||||
|
api.Queue(currentJoinRoomRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual void PartRoom()
|
||||||
|
{
|
||||||
|
currentJoinRoomRequest?.Cancel();
|
||||||
|
|
||||||
|
if (JoinedRoom.Value == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
api.Queue(new PartRoomRequest(joinedRoom.Value));
|
||||||
|
joinedRoom.Value = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly HashSet<int> ignoredRooms = new HashSet<int>();
|
||||||
|
|
||||||
|
private void onRoomsReceived(List<Room> received)
|
||||||
|
{
|
||||||
|
if (received == null)
|
||||||
|
{
|
||||||
|
ClearRooms();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove past matches
|
||||||
|
foreach (var r in rooms.ToList())
|
||||||
|
{
|
||||||
|
if (received.All(e => e.RoomID.Value != r.RoomID.Value))
|
||||||
|
rooms.Remove(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < received.Count; i++)
|
||||||
|
{
|
||||||
|
var room = received[i];
|
||||||
|
|
||||||
|
Debug.Assert(room.RoomID.Value != null);
|
||||||
|
|
||||||
|
if (ignoredRooms.Contains(room.RoomID.Value.Value))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
room.Position.Value = i;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
update(room, room);
|
||||||
|
addRoom(room);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.Error(ex, $"Failed to update room: {room.Name.Value}.");
|
||||||
|
|
||||||
|
ignoredRooms.Add(room.RoomID.Value.Value);
|
||||||
|
rooms.Remove(room);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RoomsUpdated?.Invoke();
|
||||||
|
initialRoomsReceived.Value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void RemoveRoom(Room room) => rooms.Remove(room);
|
||||||
|
|
||||||
|
protected void ClearRooms()
|
||||||
|
{
|
||||||
|
rooms.Clear();
|
||||||
|
initialRoomsReceived.Value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates a local <see cref="Room"/> with a remote copy.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="local">The local <see cref="Room"/> to update.</param>
|
||||||
|
/// <param name="remote">The remote <see cref="Room"/> to update with.</param>
|
||||||
|
private void update(Room local, Room remote)
|
||||||
|
{
|
||||||
|
foreach (var pi in remote.Playlist)
|
||||||
|
pi.MapObjects(beatmaps, rulesets);
|
||||||
|
|
||||||
|
local.CopyFrom(remote);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a <see cref="Room"/> to the list of available rooms.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="room">The <see cref="Room"/> to add.</param>
|
||||||
|
private void addRoom(Room room)
|
||||||
|
{
|
||||||
|
var existing = rooms.FirstOrDefault(e => e.RoomID.Value == room.RoomID.Value);
|
||||||
|
if (existing == null)
|
||||||
|
rooms.Add(room);
|
||||||
|
else
|
||||||
|
existing.CopyFrom(room);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract IEnumerable<RoomPollingComponent> CreatePollingComponents();
|
||||||
|
}
|
||||||
|
}
|
29
osu.Game/Screens/Multi/Components/RoomPollingComponent.cs
Normal file
29
osu.Game/Screens/Multi/Components/RoomPollingComponent.cs
Normal 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 System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Game.Online;
|
||||||
|
using osu.Game.Online.API;
|
||||||
|
using osu.Game.Online.Multiplayer;
|
||||||
|
|
||||||
|
namespace osu.Game.Screens.Multi.Components
|
||||||
|
{
|
||||||
|
public abstract class RoomPollingComponent : PollingComponent
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Invoked when any <see cref="Room"/>s have been received from the API.
|
||||||
|
/// <para>
|
||||||
|
/// Any <see cref="Room"/>s present locally but not returned by this event are to be removed from display.
|
||||||
|
/// If null, the display of local rooms is reset to an initial state.
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
public Action<List<Room>> RoomsReceived;
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
protected IAPIProvider API { get; private set; }
|
||||||
|
|
||||||
|
protected void NotifyRoomsReceived(List<Room> rooms) => RoomsReceived?.Invoke(rooms);
|
||||||
|
}
|
||||||
|
}
|
@ -48,16 +48,23 @@ namespace osu.Game.Screens.Multi.Components
|
|||||||
|
|
||||||
private class EndDatePart : DrawableDate
|
private class EndDatePart : DrawableDate
|
||||||
{
|
{
|
||||||
public readonly IBindable<DateTimeOffset> EndDate = new Bindable<DateTimeOffset>();
|
public readonly IBindable<DateTimeOffset?> EndDate = new Bindable<DateTimeOffset?>();
|
||||||
|
|
||||||
public EndDatePart()
|
public EndDatePart()
|
||||||
: base(DateTimeOffset.UtcNow)
|
: base(DateTimeOffset.UtcNow)
|
||||||
{
|
{
|
||||||
EndDate.BindValueChanged(date => Date = date.NewValue);
|
EndDate.BindValueChanged(date =>
|
||||||
|
{
|
||||||
|
// If null, set a very large future date to prevent unnecessary schedules.
|
||||||
|
Date = date.NewValue ?? DateTimeOffset.Now.AddYears(1);
|
||||||
|
}, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override string Format()
|
protected override string Format()
|
||||||
{
|
{
|
||||||
|
if (EndDate.Value == null)
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
var diffToNow = Date.Subtract(DateTimeOffset.Now);
|
var diffToNow = Date.Subtract(DateTimeOffset.Now);
|
||||||
|
|
||||||
if (diffToNow.TotalSeconds < -5)
|
if (diffToNow.TotalSeconds < -5)
|
||||||
|
@ -0,0 +1,69 @@
|
|||||||
|
// 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.Threading.Tasks;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Game.Online.Multiplayer;
|
||||||
|
|
||||||
|
namespace osu.Game.Screens.Multi.Components
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A <see cref="RoomPollingComponent"/> that polls for the currently-selected room.
|
||||||
|
/// </summary>
|
||||||
|
public class SelectionPollingComponent : RoomPollingComponent
|
||||||
|
{
|
||||||
|
[Resolved]
|
||||||
|
private Bindable<Room> selectedRoom { get; set; }
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private IRoomManager roomManager { get; set; }
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load()
|
||||||
|
{
|
||||||
|
selectedRoom.BindValueChanged(_ =>
|
||||||
|
{
|
||||||
|
if (IsLoaded)
|
||||||
|
PollImmediately();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private GetRoomRequest pollReq;
|
||||||
|
|
||||||
|
protected override Task Poll()
|
||||||
|
{
|
||||||
|
if (!API.IsLoggedIn)
|
||||||
|
return base.Poll();
|
||||||
|
|
||||||
|
if (selectedRoom.Value?.RoomID.Value == null)
|
||||||
|
return base.Poll();
|
||||||
|
|
||||||
|
var tcs = new TaskCompletionSource<bool>();
|
||||||
|
|
||||||
|
pollReq?.Cancel();
|
||||||
|
pollReq = new GetRoomRequest(selectedRoom.Value.RoomID.Value.Value);
|
||||||
|
|
||||||
|
pollReq.Success += result =>
|
||||||
|
{
|
||||||
|
var rooms = new List<Room>(roomManager.Rooms);
|
||||||
|
|
||||||
|
int index = rooms.FindIndex(r => r.RoomID.Value == result.RoomID.Value);
|
||||||
|
if (index < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
rooms[index] = result;
|
||||||
|
|
||||||
|
NotifyRoomsReceived(rooms);
|
||||||
|
tcs.SetResult(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
pollReq.Failure += _ => tcs.SetResult(false);
|
||||||
|
|
||||||
|
API.Queue(pollReq);
|
||||||
|
|
||||||
|
return tcs.Task;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -2,11 +2,13 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Game.Online.Multiplayer;
|
using osu.Game.Online.Multiplayer;
|
||||||
|
|
||||||
namespace osu.Game.Screens.Multi
|
namespace osu.Game.Screens.Multi
|
||||||
{
|
{
|
||||||
|
[Cached(typeof(IRoomManager))]
|
||||||
public interface IRoomManager
|
public interface IRoomManager
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -17,7 +19,7 @@ namespace osu.Game.Screens.Multi
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether an initial listing of rooms has been received.
|
/// Whether an initial listing of rooms has been received.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Bindable<bool> InitialRoomsReceived { get; }
|
IBindable<bool> InitialRoomsReceived { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// All the active <see cref="Room"/>s.
|
/// All the active <see cref="Room"/>s.
|
||||||
|
@ -27,7 +27,7 @@ namespace osu.Game.Screens.Multi.Lounge
|
|||||||
|
|
||||||
protected override UserActivity InitialActivity => new UserActivity.SearchingForLobby();
|
protected override UserActivity InitialActivity => new UserActivity.SearchingForLobby();
|
||||||
|
|
||||||
private readonly Bindable<bool> initialRoomsReceived = new Bindable<bool>();
|
private readonly IBindable<bool> initialRoomsReceived = new Bindable<bool>();
|
||||||
|
|
||||||
private Container content;
|
private Container content;
|
||||||
private LoadingLayer loadingLayer;
|
private LoadingLayer loadingLayer;
|
||||||
|
@ -10,6 +10,7 @@ using osu.Framework.Graphics.Containers;
|
|||||||
using osu.Framework.Graphics.Shapes;
|
using osu.Framework.Graphics.Shapes;
|
||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Online.Multiplayer;
|
using osu.Game.Online.Multiplayer;
|
||||||
|
using osu.Game.Screens.Multi.Timeshift;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Screens.Multi.Match.Components
|
namespace osu.Game.Screens.Multi.Match.Components
|
||||||
@ -31,7 +32,7 @@ namespace osu.Game.Screens.Multi.Match.Components
|
|||||||
InternalChildren = new[]
|
InternalChildren = new[]
|
||||||
{
|
{
|
||||||
background = new Box { RelativeSizeAxes = Axes.Both },
|
background = new Box { RelativeSizeAxes = Axes.Both },
|
||||||
new ReadyButton
|
new TimeshiftReadyButton
|
||||||
{
|
{
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
|
@ -325,7 +325,7 @@ namespace osu.Game.Screens.Multi.Match.Components
|
|||||||
Availability.BindValueChanged(availability => AvailabilityPicker.Current.Value = availability.NewValue, true);
|
Availability.BindValueChanged(availability => AvailabilityPicker.Current.Value = availability.NewValue, true);
|
||||||
Type.BindValueChanged(type => TypePicker.Current.Value = type.NewValue, true);
|
Type.BindValueChanged(type => TypePicker.Current.Value = type.NewValue, true);
|
||||||
MaxParticipants.BindValueChanged(count => MaxParticipantsField.Text = count.NewValue?.ToString(), true);
|
MaxParticipants.BindValueChanged(count => MaxParticipantsField.Text = count.NewValue?.ToString(), true);
|
||||||
Duration.BindValueChanged(duration => DurationField.Current.Value = duration.NewValue, true);
|
Duration.BindValueChanged(duration => DurationField.Current.Value = duration.NewValue ?? TimeSpan.FromMinutes(30), true);
|
||||||
|
|
||||||
playlist.Items.BindTo(Playlist);
|
playlist.Items.BindTo(Playlist);
|
||||||
Playlist.BindCollectionChanged(onPlaylistChanged, true);
|
Playlist.BindCollectionChanged(onPlaylistChanged, true);
|
||||||
|
@ -8,7 +8,6 @@ using osu.Framework.Graphics;
|
|||||||
using osu.Framework.Graphics.Colour;
|
using osu.Framework.Graphics.Colour;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Shapes;
|
using osu.Framework.Graphics.Shapes;
|
||||||
using osu.Framework.Logging;
|
|
||||||
using osu.Framework.Screens;
|
using osu.Framework.Screens;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Beatmaps.Drawables;
|
using osu.Game.Beatmaps.Drawables;
|
||||||
@ -30,7 +29,7 @@ using osuTK;
|
|||||||
namespace osu.Game.Screens.Multi
|
namespace osu.Game.Screens.Multi
|
||||||
{
|
{
|
||||||
[Cached]
|
[Cached]
|
||||||
public class Multiplayer : OsuScreen
|
public abstract class Multiplayer : OsuScreen
|
||||||
{
|
{
|
||||||
public override bool CursorVisible => (screenStack.CurrentScreen as IMultiplayerSubScreen)?.CursorVisible ?? true;
|
public override bool CursorVisible => (screenStack.CurrentScreen as IMultiplayerSubScreen)?.CursorVisible ?? true;
|
||||||
|
|
||||||
@ -46,6 +45,9 @@ namespace osu.Game.Screens.Multi
|
|||||||
|
|
||||||
private readonly IBindable<bool> isIdle = new BindableBool();
|
private readonly IBindable<bool> isIdle = new BindableBool();
|
||||||
|
|
||||||
|
[Cached(Type = typeof(IRoomManager))]
|
||||||
|
protected RoomManager RoomManager { get; private set; }
|
||||||
|
|
||||||
[Cached]
|
[Cached]
|
||||||
private readonly Bindable<Room> selectedRoom = new Bindable<Room>();
|
private readonly Bindable<Room> selectedRoom = new Bindable<Room>();
|
||||||
|
|
||||||
@ -55,9 +57,6 @@ namespace osu.Game.Screens.Multi
|
|||||||
[Resolved(CanBeNull = true)]
|
[Resolved(CanBeNull = true)]
|
||||||
private MusicController music { get; set; }
|
private MusicController music { get; set; }
|
||||||
|
|
||||||
[Cached(Type = typeof(IRoomManager))]
|
|
||||||
private RoomManager roomManager;
|
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private OsuGameBase game { get; set; }
|
private OsuGameBase game { get; set; }
|
||||||
|
|
||||||
@ -70,7 +69,7 @@ namespace osu.Game.Screens.Multi
|
|||||||
private readonly Drawable header;
|
private readonly Drawable header;
|
||||||
private readonly Drawable headerBackground;
|
private readonly Drawable headerBackground;
|
||||||
|
|
||||||
public Multiplayer()
|
protected Multiplayer()
|
||||||
{
|
{
|
||||||
Anchor = Anchor.Centre;
|
Anchor = Anchor.Centre;
|
||||||
Origin = Anchor.Centre;
|
Origin = Anchor.Centre;
|
||||||
@ -137,7 +136,7 @@ namespace osu.Game.Screens.Multi
|
|||||||
Origin = Anchor.TopRight,
|
Origin = Anchor.TopRight,
|
||||||
Action = () => CreateRoom()
|
Action = () => CreateRoom()
|
||||||
},
|
},
|
||||||
roomManager = new RoomManager()
|
RoomManager = CreateRoomManager()
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -168,7 +167,7 @@ namespace osu.Game.Screens.Multi
|
|||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
{
|
{
|
||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
isIdle.BindValueChanged(idle => updatePollingRate(idle.NewValue), true);
|
isIdle.BindValueChanged(idle => UpdatePollingRate(idle.NewValue), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
|
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
|
||||||
@ -178,36 +177,7 @@ namespace osu.Game.Screens.Multi
|
|||||||
return dependencies;
|
return dependencies;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updatePollingRate(bool idle)
|
protected abstract void UpdatePollingRate(bool isIdle);
|
||||||
{
|
|
||||||
if (!this.IsCurrentScreen())
|
|
||||||
{
|
|
||||||
roomManager.TimeBetweenListingPolls = 0;
|
|
||||||
roomManager.TimeBetweenSelectionPolls = 0;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
switch (screenStack.CurrentScreen)
|
|
||||||
{
|
|
||||||
case LoungeSubScreen _:
|
|
||||||
roomManager.TimeBetweenListingPolls = idle ? 120000 : 15000;
|
|
||||||
roomManager.TimeBetweenSelectionPolls = idle ? 120000 : 15000;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case MatchSubScreen _:
|
|
||||||
roomManager.TimeBetweenListingPolls = 0;
|
|
||||||
roomManager.TimeBetweenSelectionPolls = idle ? 30000 : 5000;
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
roomManager.TimeBetweenListingPolls = 0;
|
|
||||||
roomManager.TimeBetweenSelectionPolls = 0;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.Log($"Polling adjusted (listing: {roomManager.TimeBetweenListingPolls}, selection: {roomManager.TimeBetweenSelectionPolls})");
|
|
||||||
}
|
|
||||||
|
|
||||||
private void forcefullyExit()
|
private void forcefullyExit()
|
||||||
{
|
{
|
||||||
@ -241,7 +211,7 @@ namespace osu.Game.Screens.Multi
|
|||||||
|
|
||||||
beginHandlingTrack();
|
beginHandlingTrack();
|
||||||
|
|
||||||
updatePollingRate(isIdle.Value);
|
UpdatePollingRate(isIdle.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void OnSuspending(IScreen next)
|
public override void OnSuspending(IScreen next)
|
||||||
@ -251,12 +221,12 @@ namespace osu.Game.Screens.Multi
|
|||||||
|
|
||||||
endHandlingTrack();
|
endHandlingTrack();
|
||||||
|
|
||||||
updatePollingRate(isIdle.Value);
|
UpdatePollingRate(isIdle.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool OnExiting(IScreen next)
|
public override bool OnExiting(IScreen next)
|
||||||
{
|
{
|
||||||
roomManager.PartRoom();
|
RoomManager.PartRoom();
|
||||||
|
|
||||||
waves.Hide();
|
waves.Hide();
|
||||||
|
|
||||||
@ -344,12 +314,14 @@ namespace osu.Game.Screens.Multi
|
|||||||
if (newScreen is IOsuScreen newOsuScreen)
|
if (newScreen is IOsuScreen newOsuScreen)
|
||||||
((IBindable<UserActivity>)Activity).BindTo(newOsuScreen.Activity);
|
((IBindable<UserActivity>)Activity).BindTo(newOsuScreen.Activity);
|
||||||
|
|
||||||
updatePollingRate(isIdle.Value);
|
UpdatePollingRate(isIdle.Value);
|
||||||
createButton.FadeTo(newScreen is LoungeSubScreen ? 1 : 0, 200);
|
createButton.FadeTo(newScreen is LoungeSubScreen ? 1 : 0, 200);
|
||||||
|
|
||||||
updateTrack();
|
updateTrack();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected IScreen CurrentSubScreen => screenStack.CurrentScreen;
|
||||||
|
|
||||||
private void updateTrack(ValueChangedEvent<WorkingBeatmap> _ = null)
|
private void updateTrack(ValueChangedEvent<WorkingBeatmap> _ = null)
|
||||||
{
|
{
|
||||||
if (screenStack.CurrentScreen is MatchSubScreen)
|
if (screenStack.CurrentScreen is MatchSubScreen)
|
||||||
@ -381,6 +353,8 @@ namespace osu.Game.Screens.Multi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected abstract RoomManager CreateRoomManager();
|
||||||
|
|
||||||
private class MultiplayerWaveContainer : WaveContainer
|
private class MultiplayerWaveContainer : WaveContainer
|
||||||
{
|
{
|
||||||
protected override bool StartHidden => true;
|
protected override bool StartHidden => true;
|
||||||
|
@ -40,12 +40,12 @@ namespace osu.Game.Screens.Multi
|
|||||||
protected Bindable<int?> MaxParticipants { get; private set; }
|
protected Bindable<int?> MaxParticipants { get; private set; }
|
||||||
|
|
||||||
[Resolved(typeof(Room))]
|
[Resolved(typeof(Room))]
|
||||||
protected Bindable<DateTimeOffset> EndDate { get; private set; }
|
protected Bindable<DateTimeOffset?> EndDate { get; private set; }
|
||||||
|
|
||||||
[Resolved(typeof(Room))]
|
[Resolved(typeof(Room))]
|
||||||
protected Bindable<RoomAvailability> Availability { get; private set; }
|
protected Bindable<RoomAvailability> Availability { get; private set; }
|
||||||
|
|
||||||
[Resolved(typeof(Room))]
|
[Resolved(typeof(Room))]
|
||||||
protected Bindable<TimeSpan> Duration { get; private set; }
|
protected Bindable<TimeSpan?> Duration { get; private set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ using System;
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Logging;
|
using osu.Framework.Logging;
|
||||||
@ -95,19 +96,36 @@ namespace osu.Game.Screens.Multi.Play
|
|||||||
return new TimeshiftResultsScreen(score, roomId.Value.Value, playlistItem, true);
|
return new TimeshiftResultsScreen(score, roomId.Value.Value, playlistItem, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override ScoreInfo CreateScore()
|
protected override Score CreateScore()
|
||||||
{
|
{
|
||||||
var score = base.CreateScore();
|
var score = base.CreateScore();
|
||||||
score.TotalScore = (int)Math.Round(ScoreProcessor.GetStandardisedScore());
|
score.ScoreInfo.TotalScore = (int)Math.Round(ScoreProcessor.GetStandardisedScore());
|
||||||
|
return score;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task SubmitScore(Score score)
|
||||||
|
{
|
||||||
|
await base.SubmitScore(score);
|
||||||
|
|
||||||
Debug.Assert(token != null);
|
Debug.Assert(token != null);
|
||||||
|
|
||||||
var request = new SubmitRoomScoreRequest(token.Value, roomId.Value ?? 0, playlistItem.ID, score);
|
var tcs = new TaskCompletionSource<bool>();
|
||||||
request.Success += s => score.OnlineScoreID = s.ID;
|
var request = new SubmitRoomScoreRequest(token.Value, roomId.Value ?? 0, playlistItem.ID, score.ScoreInfo);
|
||||||
request.Failure += e => Logger.Error(e, "Failed to submit score");
|
|
||||||
api.Queue(request);
|
|
||||||
|
|
||||||
return score;
|
request.Success += s =>
|
||||||
|
{
|
||||||
|
score.ScoreInfo.OnlineScoreID = s.ID;
|
||||||
|
tcs.SetResult(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
request.Failure += e =>
|
||||||
|
{
|
||||||
|
Logger.Error(e, "Failed to submit score");
|
||||||
|
tcs.SetResult(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
api.Queue(request);
|
||||||
|
await tcs.Task;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Dispose(bool isDisposing)
|
protected override void Dispose(bool isDisposing)
|
||||||
|
@ -0,0 +1,189 @@
|
|||||||
|
// 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.Diagnostics;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Extensions.Color4Extensions;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Colour;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Cursor;
|
||||||
|
using osu.Framework.Graphics.Shapes;
|
||||||
|
using osu.Framework.Graphics.Sprites;
|
||||||
|
using osu.Framework.Graphics.UserInterface;
|
||||||
|
using osu.Game.Graphics;
|
||||||
|
using osu.Game.Graphics.Sprites;
|
||||||
|
using osu.Game.Graphics.UserInterface;
|
||||||
|
using osu.Game.Online.API;
|
||||||
|
using osu.Game.Online.RealtimeMultiplayer;
|
||||||
|
using osu.Game.Users;
|
||||||
|
using osu.Game.Users.Drawables;
|
||||||
|
using osuTK;
|
||||||
|
using osuTK.Graphics;
|
||||||
|
|
||||||
|
namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Participants
|
||||||
|
{
|
||||||
|
public class ParticipantPanel : RealtimeRoomComposite, IHasContextMenu
|
||||||
|
{
|
||||||
|
public readonly MultiplayerRoomUser User;
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private IAPIProvider api { get; set; }
|
||||||
|
|
||||||
|
private ReadyMark readyMark;
|
||||||
|
private SpriteIcon crown;
|
||||||
|
|
||||||
|
public ParticipantPanel(MultiplayerRoomUser user)
|
||||||
|
{
|
||||||
|
User = user;
|
||||||
|
|
||||||
|
RelativeSizeAxes = Axes.X;
|
||||||
|
Height = 40;
|
||||||
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load()
|
||||||
|
{
|
||||||
|
Debug.Assert(User.User != null);
|
||||||
|
|
||||||
|
var backgroundColour = Color4Extensions.FromHex("#33413C");
|
||||||
|
|
||||||
|
InternalChildren = new Drawable[]
|
||||||
|
{
|
||||||
|
crown = new SpriteIcon
|
||||||
|
{
|
||||||
|
Anchor = Anchor.CentreLeft,
|
||||||
|
Origin = Anchor.CentreLeft,
|
||||||
|
Icon = FontAwesome.Solid.Crown,
|
||||||
|
Size = new Vector2(14),
|
||||||
|
Colour = Color4Extensions.FromHex("#F7E65D"),
|
||||||
|
Alpha = 0
|
||||||
|
},
|
||||||
|
new Container
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Padding = new MarginPadding { Left = 24 },
|
||||||
|
Child = new Container
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Masking = true,
|
||||||
|
CornerRadius = 5,
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
new Box
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Colour = backgroundColour
|
||||||
|
},
|
||||||
|
new UserCoverBackground
|
||||||
|
{
|
||||||
|
Anchor = Anchor.CentreRight,
|
||||||
|
Origin = Anchor.CentreRight,
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Width = 0.75f,
|
||||||
|
User = User.User,
|
||||||
|
Colour = ColourInfo.GradientHorizontal(Color4.White.Opacity(0), Color4.White.Opacity(0.25f))
|
||||||
|
},
|
||||||
|
new FillFlowContainer
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Spacing = new Vector2(10),
|
||||||
|
Direction = FillDirection.Horizontal,
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
new UpdateableAvatar
|
||||||
|
{
|
||||||
|
Anchor = Anchor.CentreLeft,
|
||||||
|
Origin = Anchor.CentreLeft,
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
FillMode = FillMode.Fit,
|
||||||
|
User = User.User
|
||||||
|
},
|
||||||
|
new UpdateableFlag
|
||||||
|
{
|
||||||
|
Anchor = Anchor.CentreLeft,
|
||||||
|
Origin = Anchor.CentreLeft,
|
||||||
|
Size = new Vector2(30, 20),
|
||||||
|
Country = User.User.Country
|
||||||
|
},
|
||||||
|
new OsuSpriteText
|
||||||
|
{
|
||||||
|
Anchor = Anchor.CentreLeft,
|
||||||
|
Origin = Anchor.CentreLeft,
|
||||||
|
Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 18),
|
||||||
|
Text = User.User.Username
|
||||||
|
},
|
||||||
|
new OsuSpriteText
|
||||||
|
{
|
||||||
|
Anchor = Anchor.CentreLeft,
|
||||||
|
Origin = Anchor.CentreLeft,
|
||||||
|
Font = OsuFont.GetFont(size: 14),
|
||||||
|
Text = User.User.CurrentModeRank != null ? $"#{User.User.CurrentModeRank}" : string.Empty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
readyMark = new ReadyMark
|
||||||
|
{
|
||||||
|
Anchor = Anchor.CentreRight,
|
||||||
|
Origin = Anchor.CentreRight,
|
||||||
|
Margin = new MarginPadding { Right = 10 },
|
||||||
|
Alpha = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnRoomChanged()
|
||||||
|
{
|
||||||
|
base.OnRoomChanged();
|
||||||
|
|
||||||
|
if (Room == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const double fade_time = 50;
|
||||||
|
|
||||||
|
if (User.State == MultiplayerUserState.Ready)
|
||||||
|
readyMark.FadeIn(fade_time);
|
||||||
|
else
|
||||||
|
readyMark.FadeOut(fade_time);
|
||||||
|
|
||||||
|
if (Room.Host?.Equals(User) == true)
|
||||||
|
crown.FadeIn(fade_time);
|
||||||
|
else
|
||||||
|
crown.FadeOut(fade_time);
|
||||||
|
}
|
||||||
|
|
||||||
|
public MenuItem[] ContextMenuItems
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (Room == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
// If the local user is targetted.
|
||||||
|
if (User.UserID == api.LocalUser.Value.Id)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
// If the local user is not the host of the room.
|
||||||
|
if (Room.Host?.UserID != api.LocalUser.Value.Id)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
int targetUser = User.UserID;
|
||||||
|
|
||||||
|
return new MenuItem[]
|
||||||
|
{
|
||||||
|
new OsuMenuItem("Give host", MenuItemType.Standard, () =>
|
||||||
|
{
|
||||||
|
// Ensure the local user is still host.
|
||||||
|
if (Room.Host?.UserID != api.LocalUser.Value.Id)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Client.TransferHost(targetUser);
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,56 @@
|
|||||||
|
// 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 osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Game.Graphics.Containers;
|
||||||
|
using osu.Game.Graphics.Cursor;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
|
namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Participants
|
||||||
|
{
|
||||||
|
public class ParticipantsList : RealtimeRoomComposite
|
||||||
|
{
|
||||||
|
private FillFlowContainer<ParticipantPanel> panels;
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load()
|
||||||
|
{
|
||||||
|
InternalChild = new OsuContextMenuContainer
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Child = new OsuScrollContainer
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
ScrollbarVisible = false,
|
||||||
|
Child = panels = new FillFlowContainer<ParticipantPanel>
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
AutoSizeAxes = Axes.Y,
|
||||||
|
Direction = FillDirection.Vertical,
|
||||||
|
Spacing = new Vector2(0, 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnRoomChanged()
|
||||||
|
{
|
||||||
|
base.OnRoomChanged();
|
||||||
|
|
||||||
|
if (Room == null)
|
||||||
|
panels.Clear();
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Remove panels for users no longer in the room.
|
||||||
|
panels.RemoveAll(p => !Room.Users.Contains(p.User));
|
||||||
|
|
||||||
|
// Add panels for all users new to the room.
|
||||||
|
foreach (var user in Room.Users.Except(panels.Select(p => p.User)))
|
||||||
|
panels.Add(new ParticipantPanel(user));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,51 @@
|
|||||||
|
// 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.Allocation;
|
||||||
|
using osu.Framework.Extensions.Color4Extensions;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Sprites;
|
||||||
|
using osu.Game.Graphics;
|
||||||
|
using osu.Game.Graphics.Sprites;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
|
namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Participants
|
||||||
|
{
|
||||||
|
public class ReadyMark : CompositeDrawable
|
||||||
|
{
|
||||||
|
public ReadyMark()
|
||||||
|
{
|
||||||
|
AutoSizeAxes = Axes.Both;
|
||||||
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load()
|
||||||
|
{
|
||||||
|
InternalChild = new FillFlowContainer
|
||||||
|
{
|
||||||
|
AutoSizeAxes = Axes.Both,
|
||||||
|
Spacing = new Vector2(5),
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
new OsuSpriteText
|
||||||
|
{
|
||||||
|
Anchor = Anchor.CentreLeft,
|
||||||
|
Origin = Anchor.CentreLeft,
|
||||||
|
Font = OsuFont.GetFont(weight: FontWeight.Regular, size: 12),
|
||||||
|
Text = "ready",
|
||||||
|
Colour = Color4Extensions.FromHex("#DDFFFF")
|
||||||
|
},
|
||||||
|
new SpriteIcon
|
||||||
|
{
|
||||||
|
Anchor = Anchor.CentreLeft,
|
||||||
|
Origin = Anchor.CentreLeft,
|
||||||
|
Icon = FontAwesome.Solid.CheckCircle,
|
||||||
|
Size = new Vector2(12),
|
||||||
|
Colour = Color4Extensions.FromHex("#AADD00")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,123 @@
|
|||||||
|
// 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.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
|
using JetBrains.Annotations;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Game.Graphics;
|
||||||
|
using osu.Game.Graphics.Backgrounds;
|
||||||
|
using osu.Game.Online.API;
|
||||||
|
using osu.Game.Online.Multiplayer;
|
||||||
|
using osu.Game.Online.RealtimeMultiplayer;
|
||||||
|
using osu.Game.Screens.Multi.Components;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
|
namespace osu.Game.Screens.Multi.RealtimeMultiplayer
|
||||||
|
{
|
||||||
|
public class RealtimeReadyButton : RealtimeRoomComposite
|
||||||
|
{
|
||||||
|
public Bindable<PlaylistItem> SelectedItem => button.SelectedItem;
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private IAPIProvider api { get; set; }
|
||||||
|
|
||||||
|
[CanBeNull]
|
||||||
|
private MultiplayerRoomUser localUser;
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private OsuColour colours { get; set; }
|
||||||
|
|
||||||
|
private readonly ButtonWithTrianglesExposed button;
|
||||||
|
|
||||||
|
public RealtimeReadyButton()
|
||||||
|
{
|
||||||
|
InternalChild = button = new ButtonWithTrianglesExposed
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Size = Vector2.One,
|
||||||
|
Enabled = { Value = true },
|
||||||
|
Action = onClick
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnRoomChanged()
|
||||||
|
{
|
||||||
|
base.OnRoomChanged();
|
||||||
|
|
||||||
|
localUser = Room?.Users.Single(u => u.User?.Id == api.LocalUser.Value.Id);
|
||||||
|
button.Enabled.Value = Client.Room?.State == MultiplayerRoomState.Open;
|
||||||
|
updateState();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateState()
|
||||||
|
{
|
||||||
|
if (localUser == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Debug.Assert(Room != null);
|
||||||
|
|
||||||
|
switch (localUser.State)
|
||||||
|
{
|
||||||
|
case MultiplayerUserState.Idle:
|
||||||
|
button.Text = "Ready";
|
||||||
|
updateButtonColour(true);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MultiplayerUserState.Ready:
|
||||||
|
if (Room?.Host?.Equals(localUser) == true)
|
||||||
|
{
|
||||||
|
int countReady = Room.Users.Count(u => u.State == MultiplayerUserState.Ready);
|
||||||
|
button.Text = $"Start match ({countReady} / {Room.Users.Count} ready)";
|
||||||
|
updateButtonColour(true);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
button.Text = "Waiting for host...";
|
||||||
|
updateButtonColour(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateButtonColour(bool green)
|
||||||
|
{
|
||||||
|
if (green)
|
||||||
|
{
|
||||||
|
button.BackgroundColour = colours.Green;
|
||||||
|
button.Triangles.ColourDark = colours.Green;
|
||||||
|
button.Triangles.ColourLight = colours.GreenLight;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
button.BackgroundColour = colours.YellowDark;
|
||||||
|
button.Triangles.ColourDark = colours.YellowDark;
|
||||||
|
button.Triangles.ColourLight = colours.Yellow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onClick()
|
||||||
|
{
|
||||||
|
if (localUser == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (localUser.State == MultiplayerUserState.Idle)
|
||||||
|
Client.ChangeState(MultiplayerUserState.Ready);
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (Room?.Host?.Equals(localUser) == true)
|
||||||
|
Client.StartMatch();
|
||||||
|
else
|
||||||
|
Client.ChangeState(MultiplayerUserState.Idle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ButtonWithTrianglesExposed : ReadyButton
|
||||||
|
{
|
||||||
|
public new Triangles Triangles => base.Triangles;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,38 @@
|
|||||||
|
// 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.Allocation;
|
||||||
|
using osu.Game.Online.RealtimeMultiplayer;
|
||||||
|
|
||||||
|
namespace osu.Game.Screens.Multi.RealtimeMultiplayer
|
||||||
|
{
|
||||||
|
public abstract class RealtimeRoomComposite : MultiplayerComposite
|
||||||
|
{
|
||||||
|
[CanBeNull]
|
||||||
|
protected MultiplayerRoom Room => Client.Room;
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
protected StatefulMultiplayerClient Client { get; private set; }
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
Client.RoomChanged += OnRoomChanged;
|
||||||
|
OnRoomChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void OnRoomChanged()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool isDisposing)
|
||||||
|
{
|
||||||
|
if (Client != null)
|
||||||
|
Client.RoomChanged -= OnRoomChanged;
|
||||||
|
|
||||||
|
base.Dispose(isDisposing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,144 @@
|
|||||||
|
// 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.Diagnostics;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Logging;
|
||||||
|
using osu.Game.Online.Multiplayer;
|
||||||
|
using osu.Game.Online.RealtimeMultiplayer;
|
||||||
|
using osu.Game.Screens.Multi.Components;
|
||||||
|
|
||||||
|
namespace osu.Game.Screens.Multi.RealtimeMultiplayer
|
||||||
|
{
|
||||||
|
public class RealtimeRoomManager : RoomManager
|
||||||
|
{
|
||||||
|
[Resolved]
|
||||||
|
private StatefulMultiplayerClient multiplayerClient { get; set; }
|
||||||
|
|
||||||
|
public readonly Bindable<double> TimeBetweenListingPolls = new Bindable<double>();
|
||||||
|
public readonly Bindable<double> TimeBetweenSelectionPolls = new Bindable<double>();
|
||||||
|
private readonly IBindable<bool> isConnected = new Bindable<bool>();
|
||||||
|
private readonly Bindable<bool> allowPolling = new Bindable<bool>();
|
||||||
|
|
||||||
|
private ListingPollingComponent listingPollingComponent;
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
isConnected.BindTo(multiplayerClient.IsConnected);
|
||||||
|
isConnected.BindValueChanged(_ => Schedule(updatePolling));
|
||||||
|
JoinedRoom.BindValueChanged(_ => updatePolling());
|
||||||
|
|
||||||
|
updatePolling();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void CreateRoom(Room room, Action<Room> onSuccess = null, Action<string> onError = null)
|
||||||
|
=> base.CreateRoom(room, r => joinMultiplayerRoom(r, onSuccess), onError);
|
||||||
|
|
||||||
|
public override void JoinRoom(Room room, Action<Room> onSuccess = null, Action<string> onError = null)
|
||||||
|
=> base.JoinRoom(room, r => joinMultiplayerRoom(r, onSuccess), onError);
|
||||||
|
|
||||||
|
public override void PartRoom()
|
||||||
|
{
|
||||||
|
if (JoinedRoom.Value == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var joinedRoom = JoinedRoom.Value;
|
||||||
|
|
||||||
|
base.PartRoom();
|
||||||
|
multiplayerClient.LeaveRoom();
|
||||||
|
|
||||||
|
// Todo: This is not the way to do this. Basically when we're the only participant and the room closes, there's no way to know if this is actually the case.
|
||||||
|
// This is delayed one frame because upon exiting the match subscreen, multiplayer updates the polling rate and messes with polling.
|
||||||
|
Schedule(() =>
|
||||||
|
{
|
||||||
|
RemoveRoom(joinedRoom);
|
||||||
|
listingPollingComponent.PollImmediately();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void joinMultiplayerRoom(Room room, Action<Room> onSuccess = null)
|
||||||
|
{
|
||||||
|
Debug.Assert(room.RoomID.Value != null);
|
||||||
|
|
||||||
|
var joinTask = multiplayerClient.JoinRoom(room);
|
||||||
|
joinTask.ContinueWith(_ => onSuccess?.Invoke(room), TaskContinuationOptions.OnlyOnRanToCompletion);
|
||||||
|
joinTask.ContinueWith(t =>
|
||||||
|
{
|
||||||
|
PartRoom();
|
||||||
|
if (t.Exception != null)
|
||||||
|
Logger.Error(t.Exception, "Failed to join multiplayer room.");
|
||||||
|
}, TaskContinuationOptions.NotOnRanToCompletion);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updatePolling()
|
||||||
|
{
|
||||||
|
if (!isConnected.Value)
|
||||||
|
ClearRooms();
|
||||||
|
|
||||||
|
// Don't poll when not connected or when a room has been joined.
|
||||||
|
allowPolling.Value = isConnected.Value && JoinedRoom.Value == null;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override IEnumerable<RoomPollingComponent> CreatePollingComponents() => new RoomPollingComponent[]
|
||||||
|
{
|
||||||
|
listingPollingComponent = new RealtimeListingPollingComponent
|
||||||
|
{
|
||||||
|
TimeBetweenPolls = { BindTarget = TimeBetweenListingPolls },
|
||||||
|
AllowPolling = { BindTarget = allowPolling }
|
||||||
|
},
|
||||||
|
new RealtimeSelectionPollingComponent
|
||||||
|
{
|
||||||
|
TimeBetweenPolls = { BindTarget = TimeBetweenSelectionPolls },
|
||||||
|
AllowPolling = { BindTarget = allowPolling }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private class RealtimeListingPollingComponent : ListingPollingComponent
|
||||||
|
{
|
||||||
|
public readonly IBindable<bool> AllowPolling = new Bindable<bool>();
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
AllowPolling.BindValueChanged(allowPolling =>
|
||||||
|
{
|
||||||
|
if (!allowPolling.NewValue)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (IsLoaded)
|
||||||
|
PollImmediately();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Task Poll() => !AllowPolling.Value ? Task.CompletedTask : base.Poll();
|
||||||
|
}
|
||||||
|
|
||||||
|
private class RealtimeSelectionPollingComponent : SelectionPollingComponent
|
||||||
|
{
|
||||||
|
public readonly IBindable<bool> AllowPolling = new Bindable<bool>();
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
AllowPolling.BindValueChanged(allowPolling =>
|
||||||
|
{
|
||||||
|
if (!allowPolling.NewValue)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (IsLoaded)
|
||||||
|
PollImmediately();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Task Poll() => !AllowPolling.Value ? Task.CompletedTask : base.Poll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,337 +0,0 @@
|
|||||||
// 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.Diagnostics;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using osu.Framework.Allocation;
|
|
||||||
using osu.Framework.Bindables;
|
|
||||||
using osu.Framework.Graphics;
|
|
||||||
using osu.Framework.Graphics.Containers;
|
|
||||||
using osu.Framework.Logging;
|
|
||||||
using osu.Game.Beatmaps;
|
|
||||||
using osu.Game.Online;
|
|
||||||
using osu.Game.Online.API;
|
|
||||||
using osu.Game.Online.Multiplayer;
|
|
||||||
using osu.Game.Rulesets;
|
|
||||||
using osu.Game.Screens.Multi.Lounge.Components;
|
|
||||||
|
|
||||||
namespace osu.Game.Screens.Multi
|
|
||||||
{
|
|
||||||
public class RoomManager : CompositeDrawable, IRoomManager
|
|
||||||
{
|
|
||||||
public event Action RoomsUpdated;
|
|
||||||
|
|
||||||
private readonly BindableList<Room> rooms = new BindableList<Room>();
|
|
||||||
|
|
||||||
public Bindable<bool> InitialRoomsReceived { get; } = new Bindable<bool>();
|
|
||||||
|
|
||||||
public IBindableList<Room> Rooms => rooms;
|
|
||||||
|
|
||||||
public double TimeBetweenListingPolls
|
|
||||||
{
|
|
||||||
get => listingPollingComponent.TimeBetweenPolls;
|
|
||||||
set => listingPollingComponent.TimeBetweenPolls = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public double TimeBetweenSelectionPolls
|
|
||||||
{
|
|
||||||
get => selectionPollingComponent.TimeBetweenPolls;
|
|
||||||
set => selectionPollingComponent.TimeBetweenPolls = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
[Resolved]
|
|
||||||
private RulesetStore rulesets { get; set; }
|
|
||||||
|
|
||||||
[Resolved]
|
|
||||||
private BeatmapManager beatmaps { get; set; }
|
|
||||||
|
|
||||||
[Resolved]
|
|
||||||
private IAPIProvider api { get; set; }
|
|
||||||
|
|
||||||
[Resolved]
|
|
||||||
private Bindable<Room> selectedRoom { get; set; }
|
|
||||||
|
|
||||||
private readonly ListingPollingComponent listingPollingComponent;
|
|
||||||
private readonly SelectionPollingComponent selectionPollingComponent;
|
|
||||||
|
|
||||||
private Room joinedRoom;
|
|
||||||
|
|
||||||
public RoomManager()
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.Both;
|
|
||||||
|
|
||||||
InternalChildren = new Drawable[]
|
|
||||||
{
|
|
||||||
listingPollingComponent = new ListingPollingComponent
|
|
||||||
{
|
|
||||||
InitialRoomsReceived = { BindTarget = InitialRoomsReceived },
|
|
||||||
RoomsReceived = onListingReceived
|
|
||||||
},
|
|
||||||
selectionPollingComponent = new SelectionPollingComponent { RoomReceived = onSelectedRoomReceived }
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void Dispose(bool isDisposing)
|
|
||||||
{
|
|
||||||
base.Dispose(isDisposing);
|
|
||||||
PartRoom();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void CreateRoom(Room room, Action<Room> onSuccess = null, Action<string> onError = null)
|
|
||||||
{
|
|
||||||
room.Host.Value = api.LocalUser.Value;
|
|
||||||
|
|
||||||
var req = new CreateRoomRequest(room);
|
|
||||||
|
|
||||||
req.Success += result =>
|
|
||||||
{
|
|
||||||
joinedRoom = room;
|
|
||||||
|
|
||||||
update(room, result);
|
|
||||||
addRoom(room);
|
|
||||||
|
|
||||||
RoomsUpdated?.Invoke();
|
|
||||||
onSuccess?.Invoke(room);
|
|
||||||
};
|
|
||||||
|
|
||||||
req.Failure += exception =>
|
|
||||||
{
|
|
||||||
if (req.Result != null)
|
|
||||||
onError?.Invoke(req.Result.Error);
|
|
||||||
else
|
|
||||||
Logger.Log($"Failed to create the room: {exception}", level: LogLevel.Important);
|
|
||||||
};
|
|
||||||
|
|
||||||
api.Queue(req);
|
|
||||||
}
|
|
||||||
|
|
||||||
private JoinRoomRequest currentJoinRoomRequest;
|
|
||||||
|
|
||||||
public void JoinRoom(Room room, Action<Room> onSuccess = null, Action<string> onError = null)
|
|
||||||
{
|
|
||||||
currentJoinRoomRequest?.Cancel();
|
|
||||||
currentJoinRoomRequest = new JoinRoomRequest(room);
|
|
||||||
|
|
||||||
currentJoinRoomRequest.Success += () =>
|
|
||||||
{
|
|
||||||
joinedRoom = room;
|
|
||||||
onSuccess?.Invoke(room);
|
|
||||||
};
|
|
||||||
|
|
||||||
currentJoinRoomRequest.Failure += exception =>
|
|
||||||
{
|
|
||||||
if (!(exception is OperationCanceledException))
|
|
||||||
Logger.Log($"Failed to join room: {exception}", level: LogLevel.Important);
|
|
||||||
onError?.Invoke(exception.ToString());
|
|
||||||
};
|
|
||||||
|
|
||||||
api.Queue(currentJoinRoomRequest);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void PartRoom()
|
|
||||||
{
|
|
||||||
currentJoinRoomRequest?.Cancel();
|
|
||||||
|
|
||||||
if (joinedRoom == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
api.Queue(new PartRoomRequest(joinedRoom));
|
|
||||||
joinedRoom = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly HashSet<int> ignoredRooms = new HashSet<int>();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Invoked when the listing of all <see cref="Room"/>s is received from the server.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="listing">The listing.</param>
|
|
||||||
private void onListingReceived(List<Room> listing)
|
|
||||||
{
|
|
||||||
// Remove past matches
|
|
||||||
foreach (var r in rooms.ToList())
|
|
||||||
{
|
|
||||||
if (listing.All(e => e.RoomID.Value != r.RoomID.Value))
|
|
||||||
rooms.Remove(r);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i < listing.Count; i++)
|
|
||||||
{
|
|
||||||
if (selectedRoom.Value?.RoomID?.Value == listing[i].RoomID.Value)
|
|
||||||
{
|
|
||||||
// The listing request contains less data than the selection request, so data from the selection request is always preferred while the room is selected.
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var room = listing[i];
|
|
||||||
|
|
||||||
Debug.Assert(room.RoomID.Value != null);
|
|
||||||
|
|
||||||
if (ignoredRooms.Contains(room.RoomID.Value.Value))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
room.Position.Value = i;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
update(room, room);
|
|
||||||
addRoom(room);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Logger.Error(ex, $"Failed to update room: {room.Name.Value}.");
|
|
||||||
|
|
||||||
ignoredRooms.Add(room.RoomID.Value.Value);
|
|
||||||
rooms.Remove(room);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
RoomsUpdated?.Invoke();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Invoked when a <see cref="Room"/> is received from the server.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="toUpdate">The received <see cref="Room"/>.</param>
|
|
||||||
private void onSelectedRoomReceived(Room toUpdate)
|
|
||||||
{
|
|
||||||
foreach (var room in rooms)
|
|
||||||
{
|
|
||||||
if (room.RoomID.Value == toUpdate.RoomID.Value)
|
|
||||||
{
|
|
||||||
toUpdate.Position.Value = room.Position.Value;
|
|
||||||
update(room, toUpdate);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Updates a local <see cref="Room"/> with a remote copy.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="local">The local <see cref="Room"/> to update.</param>
|
|
||||||
/// <param name="remote">The remote <see cref="Room"/> to update with.</param>
|
|
||||||
private void update(Room local, Room remote)
|
|
||||||
{
|
|
||||||
foreach (var pi in remote.Playlist)
|
|
||||||
pi.MapObjects(beatmaps, rulesets);
|
|
||||||
|
|
||||||
local.CopyFrom(remote);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Adds a <see cref="Room"/> to the list of available rooms.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="room">The <see cref="Room"/> to add.</param>
|
|
||||||
private void addRoom(Room room)
|
|
||||||
{
|
|
||||||
var existing = rooms.FirstOrDefault(e => e.RoomID.Value == room.RoomID.Value);
|
|
||||||
if (existing == null)
|
|
||||||
rooms.Add(room);
|
|
||||||
else
|
|
||||||
existing.CopyFrom(room);
|
|
||||||
}
|
|
||||||
|
|
||||||
private class SelectionPollingComponent : PollingComponent
|
|
||||||
{
|
|
||||||
public Action<Room> RoomReceived;
|
|
||||||
|
|
||||||
[Resolved]
|
|
||||||
private IAPIProvider api { get; set; }
|
|
||||||
|
|
||||||
[Resolved]
|
|
||||||
private Bindable<Room> selectedRoom { get; set; }
|
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
|
||||||
private void load()
|
|
||||||
{
|
|
||||||
selectedRoom.BindValueChanged(_ =>
|
|
||||||
{
|
|
||||||
if (IsLoaded)
|
|
||||||
PollImmediately();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private GetRoomRequest pollReq;
|
|
||||||
|
|
||||||
protected override Task Poll()
|
|
||||||
{
|
|
||||||
if (!api.IsLoggedIn)
|
|
||||||
return base.Poll();
|
|
||||||
|
|
||||||
if (selectedRoom.Value?.RoomID.Value == null)
|
|
||||||
return base.Poll();
|
|
||||||
|
|
||||||
var tcs = new TaskCompletionSource<bool>();
|
|
||||||
|
|
||||||
pollReq?.Cancel();
|
|
||||||
pollReq = new GetRoomRequest(selectedRoom.Value.RoomID.Value.Value);
|
|
||||||
|
|
||||||
pollReq.Success += result =>
|
|
||||||
{
|
|
||||||
RoomReceived?.Invoke(result);
|
|
||||||
tcs.SetResult(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
pollReq.Failure += _ => tcs.SetResult(false);
|
|
||||||
|
|
||||||
api.Queue(pollReq);
|
|
||||||
|
|
||||||
return tcs.Task;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class ListingPollingComponent : PollingComponent
|
|
||||||
{
|
|
||||||
public Action<List<Room>> RoomsReceived;
|
|
||||||
|
|
||||||
public readonly Bindable<bool> InitialRoomsReceived = new Bindable<bool>();
|
|
||||||
|
|
||||||
[Resolved]
|
|
||||||
private IAPIProvider api { get; set; }
|
|
||||||
|
|
||||||
[Resolved]
|
|
||||||
private Bindable<FilterCriteria> currentFilter { get; set; }
|
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
|
||||||
private void load()
|
|
||||||
{
|
|
||||||
currentFilter.BindValueChanged(_ =>
|
|
||||||
{
|
|
||||||
InitialRoomsReceived.Value = false;
|
|
||||||
|
|
||||||
if (IsLoaded)
|
|
||||||
PollImmediately();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private GetRoomsRequest pollReq;
|
|
||||||
|
|
||||||
protected override Task Poll()
|
|
||||||
{
|
|
||||||
if (!api.IsLoggedIn)
|
|
||||||
return base.Poll();
|
|
||||||
|
|
||||||
var tcs = new TaskCompletionSource<bool>();
|
|
||||||
|
|
||||||
pollReq?.Cancel();
|
|
||||||
pollReq = new GetRoomsRequest(currentFilter.Value.Status, currentFilter.Value.Category);
|
|
||||||
|
|
||||||
pollReq.Success += result =>
|
|
||||||
{
|
|
||||||
InitialRoomsReceived.Value = true;
|
|
||||||
RoomsReceived?.Invoke(result);
|
|
||||||
tcs.SetResult(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
pollReq.Failure += _ => tcs.SetResult(false);
|
|
||||||
|
|
||||||
api.Queue(pollReq);
|
|
||||||
|
|
||||||
return tcs.Task;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
49
osu.Game/Screens/Multi/Timeshift/TimeshiftMultiplayer.cs
Normal file
49
osu.Game/Screens/Multi/Timeshift/TimeshiftMultiplayer.cs
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
// 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.Logging;
|
||||||
|
using osu.Framework.Screens;
|
||||||
|
using osu.Game.Screens.Multi.Components;
|
||||||
|
using osu.Game.Screens.Multi.Lounge;
|
||||||
|
using osu.Game.Screens.Multi.Match;
|
||||||
|
|
||||||
|
namespace osu.Game.Screens.Multi.Timeshift
|
||||||
|
{
|
||||||
|
public class TimeshiftMultiplayer : Multiplayer
|
||||||
|
{
|
||||||
|
protected override void UpdatePollingRate(bool isIdle)
|
||||||
|
{
|
||||||
|
var timeshiftManager = (TimeshiftRoomManager)RoomManager;
|
||||||
|
|
||||||
|
if (!this.IsCurrentScreen())
|
||||||
|
{
|
||||||
|
timeshiftManager.TimeBetweenListingPolls.Value = 0;
|
||||||
|
timeshiftManager.TimeBetweenSelectionPolls.Value = 0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
switch (CurrentSubScreen)
|
||||||
|
{
|
||||||
|
case LoungeSubScreen _:
|
||||||
|
timeshiftManager.TimeBetweenListingPolls.Value = isIdle ? 120000 : 15000;
|
||||||
|
timeshiftManager.TimeBetweenSelectionPolls.Value = isIdle ? 120000 : 15000;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MatchSubScreen _:
|
||||||
|
timeshiftManager.TimeBetweenListingPolls.Value = 0;
|
||||||
|
timeshiftManager.TimeBetweenSelectionPolls.Value = isIdle ? 30000 : 5000;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
timeshiftManager.TimeBetweenListingPolls.Value = 0;
|
||||||
|
timeshiftManager.TimeBetweenSelectionPolls.Value = 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.Log($"Polling adjusted (listing: {timeshiftManager.TimeBetweenListingPolls.Value}, selection: {timeshiftManager.TimeBetweenSelectionPolls.Value})");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override RoomManager CreateRoomManager() => new TimeshiftRoomManager();
|
||||||
|
}
|
||||||
|
}
|
38
osu.Game/Screens/Multi/Timeshift/TimeshiftReadyButton.cs
Normal file
38
osu.Game/Screens/Multi/Timeshift/TimeshiftReadyButton.cs
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
// 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 osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Game.Graphics;
|
||||||
|
using osu.Game.Online.Multiplayer;
|
||||||
|
using osu.Game.Screens.Multi.Components;
|
||||||
|
|
||||||
|
namespace osu.Game.Screens.Multi.Timeshift
|
||||||
|
{
|
||||||
|
public class TimeshiftReadyButton : ReadyButton
|
||||||
|
{
|
||||||
|
[Resolved(typeof(Room), nameof(Room.EndDate))]
|
||||||
|
private Bindable<DateTimeOffset?> endDate { get; set; }
|
||||||
|
|
||||||
|
public TimeshiftReadyButton()
|
||||||
|
{
|
||||||
|
Text = "Start";
|
||||||
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load(OsuColour colours)
|
||||||
|
{
|
||||||
|
BackgroundColour = colours.Green;
|
||||||
|
Triangles.ColourDark = colours.Green;
|
||||||
|
Triangles.ColourLight = colours.GreenLight;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Update()
|
||||||
|
{
|
||||||
|
base.Update();
|
||||||
|
|
||||||
|
Enabled.Value = endDate.Value != null && DateTimeOffset.UtcNow.AddSeconds(30).AddMilliseconds(GameBeatmap.Value.Track.Length) < endDate.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
21
osu.Game/Screens/Multi/Timeshift/TimeshiftRoomManager.cs
Normal file
21
osu.Game/Screens/Multi/Timeshift/TimeshiftRoomManager.cs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
// 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 osu.Framework.Bindables;
|
||||||
|
using osu.Game.Screens.Multi.Components;
|
||||||
|
|
||||||
|
namespace osu.Game.Screens.Multi.Timeshift
|
||||||
|
{
|
||||||
|
public class TimeshiftRoomManager : RoomManager
|
||||||
|
{
|
||||||
|
public readonly Bindable<double> TimeBetweenListingPolls = new Bindable<double>();
|
||||||
|
public readonly Bindable<double> TimeBetweenSelectionPolls = new Bindable<double>();
|
||||||
|
|
||||||
|
protected override IEnumerable<RoomPollingComponent> CreatePollingComponents() => new RoomPollingComponent[]
|
||||||
|
{
|
||||||
|
new ListingPollingComponent { TimeBetweenPolls = { BindTarget = TimeBetweenListingPolls } },
|
||||||
|
new SelectionPollingComponent { TimeBetweenPolls = { BindTarget = TimeBetweenSelectionPolls } }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +1,8 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using JetBrains.Annotations;
|
|
||||||
using osu.Framework.Bindables;
|
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Game.Users;
|
using osu.Game.Users;
|
||||||
@ -15,8 +14,7 @@ namespace osu.Game.Screens.Play.HUD
|
|||||||
{
|
{
|
||||||
public GameplayLeaderboard()
|
public GameplayLeaderboard()
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.X;
|
Width = GameplayLeaderboardScore.EXTENDED_WIDTH + GameplayLeaderboardScore.SHEAR_WIDTH;
|
||||||
AutoSizeAxes = Axes.Y;
|
|
||||||
|
|
||||||
Direction = FillDirection.Vertical;
|
Direction = FillDirection.Vertical;
|
||||||
|
|
||||||
@ -29,32 +27,35 @@ namespace osu.Game.Screens.Play.HUD
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Adds a player to the leaderboard.
|
/// Adds a player to the leaderboard.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="currentScore">The bindable current score of the player.</param>
|
|
||||||
/// <param name="user">The player.</param>
|
/// <param name="user">The player.</param>
|
||||||
public void AddPlayer([NotNull] BindableDouble currentScore, [NotNull] User user)
|
/// <param name="isTracked">
|
||||||
|
/// Whether the player should be tracked on the leaderboard.
|
||||||
|
/// Set to <c>true</c> for the local player or a player whose replay is currently being played.
|
||||||
|
/// </param>
|
||||||
|
public ILeaderboardScore AddPlayer(User user, bool isTracked)
|
||||||
{
|
{
|
||||||
var scoreItem = addScore(currentScore.Value, user);
|
var drawable = new GameplayLeaderboardScore(user, isTracked)
|
||||||
currentScore.ValueChanged += s => scoreItem.TotalScore = s.NewValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
private GameplayLeaderboardScore addScore(double totalScore, User user)
|
|
||||||
{
|
|
||||||
var scoreItem = new GameplayLeaderboardScore
|
|
||||||
{
|
{
|
||||||
User = user,
|
Anchor = Anchor.TopRight,
|
||||||
TotalScore = totalScore,
|
Origin = Anchor.TopRight,
|
||||||
OnScoreChange = updateScores,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Add(scoreItem);
|
base.Add(drawable);
|
||||||
updateScores();
|
drawable.TotalScore.BindValueChanged(_ => Scheduler.AddOnce(sort), true);
|
||||||
|
|
||||||
return scoreItem;
|
Height = Count * (GameplayLeaderboardScore.PANEL_HEIGHT + Spacing.Y);
|
||||||
|
|
||||||
|
return drawable;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateScores()
|
public sealed override void Add(GameplayLeaderboardScore drawable)
|
||||||
{
|
{
|
||||||
var orderedByScore = this.OrderByDescending(i => i.TotalScore).ToList();
|
throw new NotSupportedException($"Use {nameof(AddPlayer)} instead.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sort()
|
||||||
|
{
|
||||||
|
var orderedByScore = this.OrderByDescending(i => i.TotalScore.Value).ToList();
|
||||||
|
|
||||||
for (int i = 0; i < Count; i++)
|
for (int i = 0; i < Count; i++)
|
||||||
{
|
{
|
||||||
|
@ -1,25 +1,39 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
|
||||||
using Humanizer;
|
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Extensions.Color4Extensions;
|
using osu.Framework.Extensions.Color4Extensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Shapes;
|
||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Graphics.Sprites;
|
using osu.Game.Graphics.Sprites;
|
||||||
using osu.Game.Users;
|
using osu.Game.Users;
|
||||||
|
using osu.Game.Users.Drawables;
|
||||||
|
using osu.Game.Utils;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
using osuTK.Graphics;
|
||||||
|
|
||||||
namespace osu.Game.Screens.Play.HUD
|
namespace osu.Game.Screens.Play.HUD
|
||||||
{
|
{
|
||||||
public class GameplayLeaderboardScore : CompositeDrawable
|
public class GameplayLeaderboardScore : CompositeDrawable, ILeaderboardScore
|
||||||
{
|
{
|
||||||
private readonly OsuSpriteText positionText, positionSymbol, userString;
|
public const float EXTENDED_WIDTH = 255f;
|
||||||
private readonly GlowingSpriteText scoreText;
|
|
||||||
|
|
||||||
public Action OnScoreChange;
|
private const float regular_width = 235f;
|
||||||
|
|
||||||
|
public const float PANEL_HEIGHT = 35f;
|
||||||
|
|
||||||
|
public const float SHEAR_WIDTH = PANEL_HEIGHT * panel_shear;
|
||||||
|
|
||||||
|
private const float panel_shear = 0.15f;
|
||||||
|
|
||||||
|
private OsuSpriteText positionText, scoreText, accuracyText, comboText, usernameText;
|
||||||
|
|
||||||
|
public BindableDouble TotalScore { get; } = new BindableDouble();
|
||||||
|
public BindableDouble Accuracy { get; } = new BindableDouble(1);
|
||||||
|
public BindableInt Combo { get; } = new BindableInt();
|
||||||
|
|
||||||
private int? scorePosition;
|
private int? scorePosition;
|
||||||
|
|
||||||
@ -28,109 +42,249 @@ namespace osu.Game.Screens.Play.HUD
|
|||||||
get => scorePosition;
|
get => scorePosition;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
|
if (value == scorePosition)
|
||||||
|
return;
|
||||||
|
|
||||||
scorePosition = value;
|
scorePosition = value;
|
||||||
|
|
||||||
if (scorePosition.HasValue)
|
if (scorePosition.HasValue)
|
||||||
positionText.Text = $"#{scorePosition.Value.ToMetric(decimals: scorePosition < 100000 ? 1 : 0)}";
|
positionText.Text = $"#{scorePosition.Value.FormatRank()}";
|
||||||
|
|
||||||
positionText.FadeTo(scorePosition.HasValue ? 1 : 0);
|
positionText.FadeTo(scorePosition.HasValue ? 1 : 0);
|
||||||
positionSymbol.FadeTo(scorePosition.HasValue ? 1 : 0);
|
updateColour();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private double totalScore;
|
public User User { get; }
|
||||||
|
|
||||||
public double TotalScore
|
private readonly bool trackedPlayer;
|
||||||
|
|
||||||
|
private Container mainFillContainer;
|
||||||
|
private Box centralFill;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new <see cref="GameplayLeaderboardScore"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="user">The score's player.</param>
|
||||||
|
/// <param name="trackedPlayer">Whether the player is the local user or a replay player.</param>
|
||||||
|
public GameplayLeaderboardScore(User user, bool trackedPlayer)
|
||||||
{
|
{
|
||||||
get => totalScore;
|
User = user;
|
||||||
set
|
this.trackedPlayer = trackedPlayer;
|
||||||
{
|
|
||||||
totalScore = value;
|
|
||||||
scoreText.Text = totalScore.ToString("N0");
|
|
||||||
|
|
||||||
OnScoreChange?.Invoke();
|
Size = new Vector2(EXTENDED_WIDTH, PANEL_HEIGHT);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private User user;
|
|
||||||
|
|
||||||
public User User
|
|
||||||
{
|
|
||||||
get => user;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
user = value;
|
|
||||||
userString.Text = user?.Username;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public GameplayLeaderboardScore()
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.X;
|
|
||||||
AutoSizeAxes = Axes.Y;
|
|
||||||
|
|
||||||
InternalChild = new Container
|
|
||||||
{
|
|
||||||
Masking = true,
|
|
||||||
Anchor = Anchor.TopCentre,
|
|
||||||
Origin = Anchor.TopCentre,
|
|
||||||
AutoSizeAxes = Axes.Both,
|
|
||||||
Children = new Drawable[]
|
|
||||||
{
|
|
||||||
new FillFlowContainer
|
|
||||||
{
|
|
||||||
Anchor = Anchor.TopCentre,
|
|
||||||
Origin = Anchor.TopRight,
|
|
||||||
AutoSizeAxes = Axes.Both,
|
|
||||||
Direction = FillDirection.Horizontal,
|
|
||||||
Margin = new MarginPadding { Right = 2.5f },
|
|
||||||
Spacing = new Vector2(2.5f),
|
|
||||||
Children = new[]
|
|
||||||
{
|
|
||||||
positionText = new OsuSpriteText
|
|
||||||
{
|
|
||||||
Alpha = 0,
|
|
||||||
Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold),
|
|
||||||
},
|
|
||||||
positionSymbol = new OsuSpriteText
|
|
||||||
{
|
|
||||||
Alpha = 0,
|
|
||||||
Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold),
|
|
||||||
Text = ">",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
new FillFlowContainer
|
|
||||||
{
|
|
||||||
Anchor = Anchor.TopCentre,
|
|
||||||
Origin = Anchor.TopLeft,
|
|
||||||
AutoSizeAxes = Axes.Both,
|
|
||||||
Direction = FillDirection.Horizontal,
|
|
||||||
Margin = new MarginPadding { Left = 2.5f },
|
|
||||||
Spacing = new Vector2(2.5f),
|
|
||||||
Children = new Drawable[]
|
|
||||||
{
|
|
||||||
userString = new OsuSpriteText
|
|
||||||
{
|
|
||||||
Size = new Vector2(80, 16),
|
|
||||||
Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold, italics: true),
|
|
||||||
},
|
|
||||||
scoreText = new GlowingSpriteText
|
|
||||||
{
|
|
||||||
GlowColour = Color4Extensions.FromHex(@"83ccfa"),
|
|
||||||
Font = OsuFont.Numeric.With(size: 14),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(OsuColour colours)
|
private void load(OsuColour colours)
|
||||||
{
|
{
|
||||||
positionText.Colour = colours.YellowLight;
|
InternalChildren = new Drawable[]
|
||||||
positionSymbol.Colour = colours.Yellow;
|
{
|
||||||
|
mainFillContainer = new Container
|
||||||
|
{
|
||||||
|
Width = regular_width,
|
||||||
|
RelativeSizeAxes = Axes.Y,
|
||||||
|
Anchor = Anchor.TopRight,
|
||||||
|
Origin = Anchor.TopRight,
|
||||||
|
Masking = true,
|
||||||
|
CornerRadius = 5f,
|
||||||
|
Shear = new Vector2(panel_shear, 0f),
|
||||||
|
Child = new Box
|
||||||
|
{
|
||||||
|
Alpha = 0.5f,
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new GridContainer
|
||||||
|
{
|
||||||
|
Width = regular_width,
|
||||||
|
RelativeSizeAxes = Axes.Y,
|
||||||
|
Anchor = Anchor.TopRight,
|
||||||
|
Origin = Anchor.TopRight,
|
||||||
|
ColumnDimensions = new[]
|
||||||
|
{
|
||||||
|
new Dimension(GridSizeMode.Absolute, 35f),
|
||||||
|
new Dimension(),
|
||||||
|
new Dimension(GridSizeMode.Absolute, 85f),
|
||||||
|
},
|
||||||
|
Content = new[]
|
||||||
|
{
|
||||||
|
new Drawable[]
|
||||||
|
{
|
||||||
|
positionText = new OsuSpriteText
|
||||||
|
{
|
||||||
|
Padding = new MarginPadding { Right = SHEAR_WIDTH / 2 },
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
Colour = Color4.White,
|
||||||
|
Font = OsuFont.Torus.With(size: 14, weight: FontWeight.Bold),
|
||||||
|
Shadow = false,
|
||||||
|
},
|
||||||
|
new Container
|
||||||
|
{
|
||||||
|
Padding = new MarginPadding { Horizontal = SHEAR_WIDTH / 3 },
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
new Container
|
||||||
|
{
|
||||||
|
Masking = true,
|
||||||
|
CornerRadius = 5f,
|
||||||
|
Shear = new Vector2(panel_shear, 0f),
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Children = new[]
|
||||||
|
{
|
||||||
|
centralFill = new Box
|
||||||
|
{
|
||||||
|
Alpha = 0.5f,
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Colour = Color4Extensions.FromHex("3399cc"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new FillFlowContainer
|
||||||
|
{
|
||||||
|
Padding = new MarginPadding { Left = SHEAR_WIDTH },
|
||||||
|
Anchor = Anchor.CentreLeft,
|
||||||
|
Origin = Anchor.CentreLeft,
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Direction = FillDirection.Horizontal,
|
||||||
|
Spacing = new Vector2(4f, 0f),
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
new CircularContainer
|
||||||
|
{
|
||||||
|
Masking = true,
|
||||||
|
Anchor = Anchor.CentreLeft,
|
||||||
|
Origin = Anchor.CentreLeft,
|
||||||
|
Size = new Vector2(25f),
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
new Box
|
||||||
|
{
|
||||||
|
Name = "Placeholder while avatar loads",
|
||||||
|
Alpha = 0.3f,
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Colour = colours.Gray4,
|
||||||
|
},
|
||||||
|
new UpdateableAvatar(User)
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
usernameText = new OsuSpriteText
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
Width = 0.6f,
|
||||||
|
Anchor = Anchor.CentreLeft,
|
||||||
|
Origin = Anchor.CentreLeft,
|
||||||
|
Colour = Color4.White,
|
||||||
|
Font = OsuFont.Torus.With(size: 14, weight: FontWeight.SemiBold),
|
||||||
|
Text = User.Username,
|
||||||
|
Truncate = true,
|
||||||
|
Shadow = false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new Container
|
||||||
|
{
|
||||||
|
Padding = new MarginPadding { Top = 2f, Right = 17.5f, Bottom = 5f },
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Anchor = Anchor.CentreLeft,
|
||||||
|
Origin = Anchor.CentreLeft,
|
||||||
|
Colour = Color4.White,
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
scoreText = new OsuSpriteText
|
||||||
|
{
|
||||||
|
Spacing = new Vector2(-1f, 0f),
|
||||||
|
Font = OsuFont.Torus.With(size: 16, weight: FontWeight.SemiBold, fixedWidth: true),
|
||||||
|
Shadow = false,
|
||||||
|
},
|
||||||
|
accuracyText = new OsuSpriteText
|
||||||
|
{
|
||||||
|
Anchor = Anchor.BottomLeft,
|
||||||
|
Origin = Anchor.BottomLeft,
|
||||||
|
Font = OsuFont.Torus.With(size: 12, weight: FontWeight.SemiBold, fixedWidth: true),
|
||||||
|
Spacing = new Vector2(-1f, 0f),
|
||||||
|
Shadow = false,
|
||||||
|
},
|
||||||
|
comboText = new OsuSpriteText
|
||||||
|
{
|
||||||
|
Anchor = Anchor.BottomRight,
|
||||||
|
Origin = Anchor.BottomRight,
|
||||||
|
Spacing = new Vector2(-1f, 0f),
|
||||||
|
Font = OsuFont.Torus.With(size: 12, weight: FontWeight.SemiBold, fixedWidth: true),
|
||||||
|
Shadow = false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
TotalScore.BindValueChanged(v => scoreText.Text = v.NewValue.ToString("N0"), true);
|
||||||
|
Accuracy.BindValueChanged(v => accuracyText.Text = v.NewValue.FormatAccuracy(), true);
|
||||||
|
Combo.BindValueChanged(v => comboText.Text = $"{v.NewValue}x", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
updateColour();
|
||||||
|
FinishTransforms(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private const double panel_transition_duration = 500;
|
||||||
|
|
||||||
|
private void updateColour()
|
||||||
|
{
|
||||||
|
if (scorePosition == 1)
|
||||||
|
{
|
||||||
|
mainFillContainer.ResizeWidthTo(EXTENDED_WIDTH, panel_transition_duration, Easing.OutElastic);
|
||||||
|
panelColour = Color4Extensions.FromHex("7fcc33");
|
||||||
|
textColour = Color4.White;
|
||||||
|
}
|
||||||
|
else if (trackedPlayer)
|
||||||
|
{
|
||||||
|
mainFillContainer.ResizeWidthTo(EXTENDED_WIDTH, panel_transition_duration, Easing.OutElastic);
|
||||||
|
panelColour = Color4Extensions.FromHex("ffd966");
|
||||||
|
textColour = Color4Extensions.FromHex("2e576b");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
mainFillContainer.ResizeWidthTo(regular_width, panel_transition_duration, Easing.OutElastic);
|
||||||
|
panelColour = Color4Extensions.FromHex("3399cc");
|
||||||
|
textColour = Color4.White;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Color4 panelColour
|
||||||
|
{
|
||||||
|
set
|
||||||
|
{
|
||||||
|
mainFillContainer.FadeColour(value, panel_transition_duration, Easing.OutQuint);
|
||||||
|
centralFill.FadeColour(value, panel_transition_duration, Easing.OutQuint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private const double text_transition_duration = 200;
|
||||||
|
|
||||||
|
private Color4 textColour
|
||||||
|
{
|
||||||
|
set
|
||||||
|
{
|
||||||
|
scoreText.FadeColour(value, text_transition_duration, Easing.OutQuint);
|
||||||
|
accuracyText.FadeColour(value, text_transition_duration, Easing.OutQuint);
|
||||||
|
comboText.FadeColour(value, text_transition_duration, Easing.OutQuint);
|
||||||
|
usernameText.FadeColour(value, text_transition_duration, Easing.OutQuint);
|
||||||
|
positionText.FadeColour(value, text_transition_duration, Easing.OutQuint);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
14
osu.Game/Screens/Play/HUD/ILeaderboardScore.cs
Normal file
14
osu.Game/Screens/Play/HUD/ILeaderboardScore.cs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
// 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.Bindables;
|
||||||
|
|
||||||
|
namespace osu.Game.Screens.Play.HUD
|
||||||
|
{
|
||||||
|
public interface ILeaderboardScore
|
||||||
|
{
|
||||||
|
BindableDouble TotalScore { get; }
|
||||||
|
BindableDouble Accuracy { get; }
|
||||||
|
BindableInt Combo { get; }
|
||||||
|
}
|
||||||
|
}
|
131
osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs
Normal file
131
osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
// 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 JetBrains.Annotations;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Game.Configuration;
|
||||||
|
using osu.Game.Database;
|
||||||
|
using osu.Game.Online.API;
|
||||||
|
using osu.Game.Online.Spectator;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
|
||||||
|
namespace osu.Game.Screens.Play.HUD
|
||||||
|
{
|
||||||
|
[LongRunningLoad]
|
||||||
|
public class MultiplayerGameplayLeaderboard : GameplayLeaderboard
|
||||||
|
{
|
||||||
|
private readonly ScoreProcessor scoreProcessor;
|
||||||
|
|
||||||
|
private readonly int[] userIds;
|
||||||
|
|
||||||
|
private readonly Dictionary<int, TrackedUserData> userScores = new Dictionary<int, TrackedUserData>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Construct a new leaderboard.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="scoreProcessor">A score processor instance to handle score calculation for scores of users in the match.</param>
|
||||||
|
/// <param name="userIds">IDs of all users in this match.</param>
|
||||||
|
public MultiplayerGameplayLeaderboard(ScoreProcessor scoreProcessor, int[] userIds)
|
||||||
|
{
|
||||||
|
// todo: this will eventually need to be created per user to support different mod combinations.
|
||||||
|
this.scoreProcessor = scoreProcessor;
|
||||||
|
|
||||||
|
// todo: this will likely be passed in as User instances.
|
||||||
|
this.userIds = userIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private SpectatorStreamingClient streamingClient { get; set; }
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private UserLookupCache userLookupCache { get; set; }
|
||||||
|
|
||||||
|
private Bindable<ScoringMode> scoringMode;
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load(OsuConfigManager config, IAPIProvider api)
|
||||||
|
{
|
||||||
|
streamingClient.OnNewFrames += handleIncomingFrames;
|
||||||
|
|
||||||
|
foreach (var user in userIds)
|
||||||
|
{
|
||||||
|
streamingClient.WatchUser(user);
|
||||||
|
|
||||||
|
// probably won't be required in the final implementation.
|
||||||
|
var resolvedUser = userLookupCache.GetUserAsync(user).Result;
|
||||||
|
|
||||||
|
var trackedUser = new TrackedUserData();
|
||||||
|
|
||||||
|
userScores[user] = trackedUser;
|
||||||
|
var leaderboardScore = AddPlayer(resolvedUser, resolvedUser.Id == api.LocalUser.Value.Id);
|
||||||
|
|
||||||
|
((IBindable<double>)leaderboardScore.Accuracy).BindTo(trackedUser.Accuracy);
|
||||||
|
((IBindable<double>)leaderboardScore.TotalScore).BindTo(trackedUser.Score);
|
||||||
|
((IBindable<int>)leaderboardScore.Combo).BindTo(trackedUser.CurrentCombo);
|
||||||
|
}
|
||||||
|
|
||||||
|
scoringMode = config.GetBindable<ScoringMode>(OsuSetting.ScoreDisplayMode);
|
||||||
|
scoringMode.BindValueChanged(updateAllScores, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateAllScores(ValueChangedEvent<ScoringMode> mode)
|
||||||
|
{
|
||||||
|
foreach (var trackedData in userScores.Values)
|
||||||
|
trackedData.UpdateScore(scoreProcessor, mode.NewValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleIncomingFrames(int userId, FrameDataBundle bundle)
|
||||||
|
{
|
||||||
|
if (userScores.TryGetValue(userId, out var trackedData))
|
||||||
|
{
|
||||||
|
trackedData.LastHeader = bundle.Header;
|
||||||
|
trackedData.UpdateScore(scoreProcessor, scoringMode.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool isDisposing)
|
||||||
|
{
|
||||||
|
base.Dispose(isDisposing);
|
||||||
|
|
||||||
|
if (streamingClient != null)
|
||||||
|
{
|
||||||
|
foreach (var user in userIds)
|
||||||
|
{
|
||||||
|
streamingClient.StopWatchingUser(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
streamingClient.OnNewFrames -= handleIncomingFrames;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TrackedUserData
|
||||||
|
{
|
||||||
|
public IBindableNumber<double> Score => score;
|
||||||
|
|
||||||
|
private readonly BindableDouble score = new BindableDouble();
|
||||||
|
|
||||||
|
public IBindableNumber<double> Accuracy => accuracy;
|
||||||
|
|
||||||
|
private readonly BindableDouble accuracy = new BindableDouble(1);
|
||||||
|
|
||||||
|
public IBindableNumber<int> CurrentCombo => currentCombo;
|
||||||
|
|
||||||
|
private readonly BindableInt currentCombo = new BindableInt();
|
||||||
|
|
||||||
|
[CanBeNull]
|
||||||
|
public FrameHeader LastHeader;
|
||||||
|
|
||||||
|
public void UpdateScore(ScoreProcessor processor, ScoringMode mode)
|
||||||
|
{
|
||||||
|
if (LastHeader == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
(score.Value, accuracy.Value) = processor.GetScoreAndAccuracy(mode, LastHeader.MaxCombo, LastHeader.Statistics);
|
||||||
|
|
||||||
|
currentCombo.Value = LastHeader.Combo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -5,6 +5,7 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Audio;
|
using osu.Framework.Audio;
|
||||||
@ -22,8 +23,10 @@ using osu.Game.Graphics.Containers;
|
|||||||
using osu.Game.IO.Archives;
|
using osu.Game.IO.Archives;
|
||||||
using osu.Game.Online.API;
|
using osu.Game.Online.API;
|
||||||
using osu.Game.Overlays;
|
using osu.Game.Overlays;
|
||||||
|
using osu.Game.Replays;
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
|
using osu.Game.Rulesets.Replays;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Rulesets.UI;
|
using osu.Game.Rulesets.UI;
|
||||||
using osu.Game.Scoring;
|
using osu.Game.Scoring;
|
||||||
@ -501,6 +504,7 @@ namespace osu.Game.Screens.Play
|
|||||||
}
|
}
|
||||||
|
|
||||||
private ScheduledDelegate completionProgressDelegate;
|
private ScheduledDelegate completionProgressDelegate;
|
||||||
|
private Task<ScoreInfo> scoreSubmissionTask;
|
||||||
|
|
||||||
private void updateCompletionState(ValueChangedEvent<bool> completionState)
|
private void updateCompletionState(ValueChangedEvent<bool> completionState)
|
||||||
{
|
{
|
||||||
@ -527,33 +531,50 @@ namespace osu.Game.Screens.Play
|
|||||||
|
|
||||||
if (!showResults) return;
|
if (!showResults) return;
|
||||||
|
|
||||||
using (BeginDelayedSequence(RESULTS_DISPLAY_DELAY))
|
scoreSubmissionTask ??= Task.Run(async () =>
|
||||||
completionProgressDelegate = Schedule(GotoRanking);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected virtual ScoreInfo CreateScore()
|
|
||||||
{
|
|
||||||
var score = new ScoreInfo
|
|
||||||
{
|
{
|
||||||
Beatmap = Beatmap.Value.BeatmapInfo,
|
var score = CreateScore();
|
||||||
Ruleset = rulesetInfo,
|
|
||||||
Mods = Mods.Value.ToArray(),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (DrawableRuleset.ReplayScore != null)
|
try
|
||||||
score.User = DrawableRuleset.ReplayScore.ScoreInfo?.User ?? new GuestUser();
|
{
|
||||||
else
|
await SubmitScore(score);
|
||||||
score.User = api.LocalUser.Value;
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.Error(ex, "Score submission failed!");
|
||||||
|
}
|
||||||
|
|
||||||
ScoreProcessor.PopulateScore(score);
|
try
|
||||||
|
{
|
||||||
|
await ImportScore(score);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.Error(ex, "Score import failed!");
|
||||||
|
}
|
||||||
|
|
||||||
return score;
|
return score.ScoreInfo;
|
||||||
|
});
|
||||||
|
|
||||||
|
using (BeginDelayedSequence(RESULTS_DISPLAY_DELAY))
|
||||||
|
scheduleCompletion();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void scheduleCompletion() => completionProgressDelegate = Schedule(() =>
|
||||||
|
{
|
||||||
|
if (!scoreSubmissionTask.IsCompleted)
|
||||||
|
{
|
||||||
|
scheduleCompletion();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// screen may be in the exiting transition phase.
|
||||||
|
if (this.IsCurrentScreen())
|
||||||
|
this.Push(CreateResults(scoreSubmissionTask.Result));
|
||||||
|
});
|
||||||
|
|
||||||
protected override bool OnScroll(ScrollEvent e) => mouseWheelDisabled.Value && !GameplayClockContainer.IsPaused.Value;
|
protected override bool OnScroll(ScrollEvent e) => mouseWheelDisabled.Value && !GameplayClockContainer.IsPaused.Value;
|
||||||
|
|
||||||
protected virtual ResultsScreen CreateResults(ScoreInfo score) => new SoloResultsScreen(score, true);
|
|
||||||
|
|
||||||
#region Fail Logic
|
#region Fail Logic
|
||||||
|
|
||||||
protected FailOverlay FailOverlay { get; private set; }
|
protected FailOverlay FailOverlay { get; private set; }
|
||||||
@ -748,39 +769,74 @@ namespace osu.Game.Screens.Play
|
|||||||
return base.OnExiting(next);
|
return base.OnExiting(next);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual void GotoRanking()
|
/// <summary>
|
||||||
|
/// Creates the player's <see cref="Score"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The <see cref="Score"/>.</returns>
|
||||||
|
protected virtual Score CreateScore()
|
||||||
{
|
{
|
||||||
|
var score = new Score
|
||||||
|
{
|
||||||
|
ScoreInfo = new ScoreInfo
|
||||||
|
{
|
||||||
|
Beatmap = Beatmap.Value.BeatmapInfo,
|
||||||
|
Ruleset = rulesetInfo,
|
||||||
|
Mods = Mods.Value.ToArray(),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (DrawableRuleset.ReplayScore != null)
|
if (DrawableRuleset.ReplayScore != null)
|
||||||
{
|
{
|
||||||
// if a replay is present, we likely don't want to import into the local database.
|
score.ScoreInfo.User = DrawableRuleset.ReplayScore.ScoreInfo?.User ?? new GuestUser();
|
||||||
this.Push(CreateResults(CreateScore()));
|
score.Replay = DrawableRuleset.ReplayScore.Replay;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
else
|
||||||
LegacyByteArrayReader replayReader = null;
|
|
||||||
|
|
||||||
var score = new Score { ScoreInfo = CreateScore() };
|
|
||||||
|
|
||||||
if (recordingScore?.Replay.Frames.Count > 0)
|
|
||||||
{
|
{
|
||||||
score.Replay = recordingScore.Replay;
|
score.ScoreInfo.User = api.LocalUser.Value;
|
||||||
|
score.Replay = new Replay { Frames = recordingScore?.Replay.Frames.ToList() ?? new List<ReplayFrame>() };
|
||||||
using (var stream = new MemoryStream())
|
|
||||||
{
|
|
||||||
new LegacyScoreEncoder(score, gameplayBeatmap.PlayableBeatmap).Encode(stream);
|
|
||||||
replayReader = new LegacyByteArrayReader(stream.ToArray(), "replay.osr");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
scoreManager.Import(score.ScoreInfo, replayReader)
|
ScoreProcessor.PopulateScore(score.ScoreInfo);
|
||||||
.ContinueWith(imported => Schedule(() =>
|
|
||||||
{
|
return score;
|
||||||
// screen may be in the exiting transition phase.
|
|
||||||
if (this.IsCurrentScreen())
|
|
||||||
this.Push(CreateResults(imported.Result));
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Imports the player's <see cref="Score"/> to the local database.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="score">The <see cref="Score"/> to import.</param>
|
||||||
|
/// <returns>The imported score.</returns>
|
||||||
|
protected virtual Task ImportScore(Score score)
|
||||||
|
{
|
||||||
|
// Replays are already populated and present in the game's database, so should not be re-imported.
|
||||||
|
if (DrawableRuleset.ReplayScore != null)
|
||||||
|
return Task.CompletedTask;
|
||||||
|
|
||||||
|
LegacyByteArrayReader replayReader;
|
||||||
|
|
||||||
|
using (var stream = new MemoryStream())
|
||||||
|
{
|
||||||
|
new LegacyScoreEncoder(score, gameplayBeatmap.PlayableBeatmap).Encode(stream);
|
||||||
|
replayReader = new LegacyByteArrayReader(stream.ToArray(), "replay.osr");
|
||||||
|
}
|
||||||
|
|
||||||
|
return scoreManager.Import(score.ScoreInfo, replayReader);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Submits the player's <see cref="Score"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="score">The <see cref="Score"/> to submit.</param>
|
||||||
|
/// <returns>The submitted score.</returns>
|
||||||
|
protected virtual Task SubmitScore(Score score) => Task.CompletedTask;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates the <see cref="ResultsScreen"/> for a <see cref="ScoreInfo"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="score">The <see cref="ScoreInfo"/> to be displayed in the results screen.</param>
|
||||||
|
/// <returns>The <see cref="ResultsScreen"/>.</returns>
|
||||||
|
protected virtual ResultsScreen CreateResults(ScoreInfo score) => new SoloResultsScreen(score, true);
|
||||||
|
|
||||||
private void fadeOut(bool instant = false)
|
private void fadeOut(bool instant = false)
|
||||||
{
|
{
|
||||||
float fadeOutDuration = instant ? 0 : 250;
|
float fadeOutDuration = instant ? 0 : 250;
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Threading.Tasks;
|
||||||
using osu.Framework.Input.Bindings;
|
using osu.Framework.Input.Bindings;
|
||||||
using osu.Game.Input.Bindings;
|
using osu.Game.Input.Bindings;
|
||||||
using osu.Game.Scoring;
|
using osu.Game.Scoring;
|
||||||
@ -26,18 +27,21 @@ namespace osu.Game.Screens.Play
|
|||||||
DrawableRuleset?.SetReplayScore(Score);
|
DrawableRuleset?.SetReplayScore(Score);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override ResultsScreen CreateResults(ScoreInfo score) => new SoloResultsScreen(score, false);
|
protected override Score CreateScore()
|
||||||
|
|
||||||
protected override ScoreInfo CreateScore()
|
|
||||||
{
|
{
|
||||||
var baseScore = base.CreateScore();
|
var baseScore = base.CreateScore();
|
||||||
|
|
||||||
// Since the replay score doesn't contain statistics, we'll pass them through here.
|
// Since the replay score doesn't contain statistics, we'll pass them through here.
|
||||||
Score.ScoreInfo.HitEvents = baseScore.HitEvents;
|
Score.ScoreInfo.HitEvents = baseScore.ScoreInfo.HitEvents;
|
||||||
|
|
||||||
return Score.ScoreInfo;
|
return Score;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Don't re-import replay scores as they're already present in the database.
|
||||||
|
protected override Task ImportScore(Score score) => Task.CompletedTask;
|
||||||
|
|
||||||
|
protected override ResultsScreen CreateResults(ScoreInfo score) => new SoloResultsScreen(score, false);
|
||||||
|
|
||||||
public bool OnPressed(GlobalAction action)
|
public bool OnPressed(GlobalAction action)
|
||||||
{
|
{
|
||||||
switch (action)
|
switch (action)
|
||||||
|
@ -8,6 +8,7 @@ using osu.Framework.Allocation;
|
|||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Textures;
|
using osu.Framework.Graphics.Textures;
|
||||||
|
using osu.Framework.Platform;
|
||||||
using osu.Game.IO;
|
using osu.Game.IO;
|
||||||
using osu.Game.Screens.Play;
|
using osu.Game.Screens.Play;
|
||||||
|
|
||||||
@ -59,12 +60,12 @@ namespace osu.Game.Storyboards.Drawables
|
|||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader(true)]
|
[BackgroundDependencyLoader(true)]
|
||||||
private void load(FileStore fileStore, GameplayClock clock, CancellationToken? cancellationToken)
|
private void load(FileStore fileStore, GameplayClock clock, CancellationToken? cancellationToken, GameHost host)
|
||||||
{
|
{
|
||||||
if (clock != null)
|
if (clock != null)
|
||||||
Clock = clock;
|
Clock = clock;
|
||||||
|
|
||||||
dependencies.Cache(new TextureStore(new TextureLoaderStore(fileStore.Store), false, scaleAdjust: 1));
|
dependencies.Cache(new TextureStore(host.CreateTextureLoaderStore(fileStore.Store), false, scaleAdjust: 1));
|
||||||
|
|
||||||
foreach (var layer in Storyboard.Layers)
|
foreach (var layer in Storyboard.Layers)
|
||||||
{
|
{
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Game.Online.Multiplayer;
|
using osu.Game.Online.Multiplayer;
|
||||||
@ -12,11 +13,7 @@ namespace osu.Game.Tests.Visual
|
|||||||
[Cached]
|
[Cached]
|
||||||
private readonly Bindable<Room> currentRoom = new Bindable<Room>();
|
private readonly Bindable<Room> currentRoom = new Bindable<Room>();
|
||||||
|
|
||||||
protected Room Room
|
protected Room Room => currentRoom.Value;
|
||||||
{
|
|
||||||
get => currentRoom.Value;
|
|
||||||
set => currentRoom.Value = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
private CachedModelDependencyContainer<Room> dependencies;
|
private CachedModelDependencyContainer<Room> dependencies;
|
||||||
|
|
||||||
@ -26,5 +23,11 @@ namespace osu.Game.Tests.Visual
|
|||||||
dependencies.Model.BindTo(currentRoom);
|
dependencies.Model.BindTo(currentRoom);
|
||||||
return dependencies;
|
return dependencies;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void Setup() => Schedule(() =>
|
||||||
|
{
|
||||||
|
currentRoom.Value = new Room();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,50 @@
|
|||||||
|
// 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.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Game.Online.RealtimeMultiplayer;
|
||||||
|
using osu.Game.Screens.Multi.Lounge.Components;
|
||||||
|
using osu.Game.Screens.Multi.RealtimeMultiplayer;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Visual.RealtimeMultiplayer
|
||||||
|
{
|
||||||
|
public abstract class RealtimeMultiplayerTestScene : MultiplayerTestScene
|
||||||
|
{
|
||||||
|
[Cached(typeof(StatefulMultiplayerClient))]
|
||||||
|
public TestRealtimeMultiplayerClient Client { get; }
|
||||||
|
|
||||||
|
[Cached(typeof(RealtimeRoomManager))]
|
||||||
|
public TestRealtimeRoomManager RoomManager { get; }
|
||||||
|
|
||||||
|
[Cached]
|
||||||
|
public Bindable<FilterCriteria> Filter { get; }
|
||||||
|
|
||||||
|
protected override Container<Drawable> Content => content;
|
||||||
|
private readonly TestRealtimeRoomContainer content;
|
||||||
|
|
||||||
|
private readonly bool joinRoom;
|
||||||
|
|
||||||
|
protected RealtimeMultiplayerTestScene(bool joinRoom = true)
|
||||||
|
{
|
||||||
|
this.joinRoom = joinRoom;
|
||||||
|
base.Content.Add(content = new TestRealtimeRoomContainer { RelativeSizeAxes = Axes.Both });
|
||||||
|
|
||||||
|
Client = content.Client;
|
||||||
|
RoomManager = content.RoomManager;
|
||||||
|
Filter = content.Filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public new void Setup() => Schedule(() =>
|
||||||
|
{
|
||||||
|
RoomManager.Schedule(() => RoomManager.PartRoom());
|
||||||
|
|
||||||
|
if (joinRoom)
|
||||||
|
RoomManager.Schedule(() => RoomManager.CreateRoom(Room));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,119 @@
|
|||||||
|
// 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.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Game.Online.API;
|
||||||
|
using osu.Game.Online.RealtimeMultiplayer;
|
||||||
|
using osu.Game.Users;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Visual.RealtimeMultiplayer
|
||||||
|
{
|
||||||
|
public class TestRealtimeMultiplayerClient : StatefulMultiplayerClient
|
||||||
|
{
|
||||||
|
public override IBindable<bool> IsConnected => isConnected;
|
||||||
|
private readonly Bindable<bool> isConnected = new Bindable<bool>(true);
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private IAPIProvider api { get; set; } = null!;
|
||||||
|
|
||||||
|
public void Connect() => isConnected.Value = true;
|
||||||
|
|
||||||
|
public void Disconnect() => isConnected.Value = false;
|
||||||
|
|
||||||
|
public void AddUser(User user) => ((IMultiplayerClient)this).UserJoined(new MultiplayerRoomUser(user.Id) { User = user });
|
||||||
|
|
||||||
|
public void RemoveUser(User user)
|
||||||
|
{
|
||||||
|
Debug.Assert(Room != null);
|
||||||
|
((IMultiplayerClient)this).UserLeft(Room.Users.Single(u => u.User == user));
|
||||||
|
|
||||||
|
Schedule(() =>
|
||||||
|
{
|
||||||
|
if (Room.Users.Any())
|
||||||
|
TransferHost(Room.Users.First().UserID);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ChangeUserState(int userId, MultiplayerUserState newState)
|
||||||
|
{
|
||||||
|
Debug.Assert(Room != null);
|
||||||
|
|
||||||
|
((IMultiplayerClient)this).UserStateChanged(userId, newState);
|
||||||
|
|
||||||
|
Schedule(() =>
|
||||||
|
{
|
||||||
|
switch (newState)
|
||||||
|
{
|
||||||
|
case MultiplayerUserState.Loaded:
|
||||||
|
if (Room.Users.All(u => u.State != MultiplayerUserState.WaitingForLoad))
|
||||||
|
{
|
||||||
|
foreach (var u in Room.Users.Where(u => u.State == MultiplayerUserState.Loaded))
|
||||||
|
ChangeUserState(u.UserID, MultiplayerUserState.Playing);
|
||||||
|
|
||||||
|
((IMultiplayerClient)this).MatchStarted();
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MultiplayerUserState.FinishedPlay:
|
||||||
|
if (Room.Users.All(u => u.State != MultiplayerUserState.Playing))
|
||||||
|
{
|
||||||
|
foreach (var u in Room.Users.Where(u => u.State == MultiplayerUserState.FinishedPlay))
|
||||||
|
ChangeUserState(u.UserID, MultiplayerUserState.Results);
|
||||||
|
|
||||||
|
((IMultiplayerClient)this).ResultsReady();
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Task<MultiplayerRoom> JoinRoom(long roomId)
|
||||||
|
{
|
||||||
|
var user = new MultiplayerRoomUser(api.LocalUser.Value.Id) { User = api.LocalUser.Value };
|
||||||
|
|
||||||
|
var room = new MultiplayerRoom(roomId);
|
||||||
|
room.Users.Add(user);
|
||||||
|
|
||||||
|
if (room.Users.Count == 1)
|
||||||
|
room.Host = user;
|
||||||
|
|
||||||
|
return Task.FromResult(room);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Task TransferHost(int userId) => ((IMultiplayerClient)this).HostChanged(userId);
|
||||||
|
|
||||||
|
public override async Task ChangeSettings(MultiplayerRoomSettings settings)
|
||||||
|
{
|
||||||
|
Debug.Assert(Room != null);
|
||||||
|
|
||||||
|
await ((IMultiplayerClient)this).SettingsChanged(settings);
|
||||||
|
|
||||||
|
foreach (var user in Room.Users.Where(u => u.State == MultiplayerUserState.Ready))
|
||||||
|
ChangeUserState(user.UserID, MultiplayerUserState.Idle);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Task ChangeState(MultiplayerUserState newState)
|
||||||
|
{
|
||||||
|
ChangeUserState(api.LocalUser.Value.Id, newState);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Task StartMatch()
|
||||||
|
{
|
||||||
|
Debug.Assert(Room != null);
|
||||||
|
|
||||||
|
foreach (var user in Room.Users.Where(u => u.State == MultiplayerUserState.Ready))
|
||||||
|
ChangeUserState(user.UserID, MultiplayerUserState.WaitingForLoad);
|
||||||
|
|
||||||
|
return ((IMultiplayerClient)this).LoadRequested();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,40 @@
|
|||||||
|
// 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.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Game.Online.RealtimeMultiplayer;
|
||||||
|
using osu.Game.Screens.Multi.Lounge.Components;
|
||||||
|
using osu.Game.Screens.Multi.RealtimeMultiplayer;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Visual.RealtimeMultiplayer
|
||||||
|
{
|
||||||
|
public class TestRealtimeRoomContainer : Container
|
||||||
|
{
|
||||||
|
protected override Container<Drawable> Content => content;
|
||||||
|
private readonly Container content;
|
||||||
|
|
||||||
|
[Cached(typeof(StatefulMultiplayerClient))]
|
||||||
|
public readonly TestRealtimeMultiplayerClient Client;
|
||||||
|
|
||||||
|
[Cached(typeof(RealtimeRoomManager))]
|
||||||
|
public readonly TestRealtimeRoomManager RoomManager;
|
||||||
|
|
||||||
|
[Cached]
|
||||||
|
public readonly Bindable<FilterCriteria> Filter = new Bindable<FilterCriteria>(new FilterCriteria());
|
||||||
|
|
||||||
|
public TestRealtimeRoomContainer()
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both;
|
||||||
|
|
||||||
|
AddRangeInternal(new Drawable[]
|
||||||
|
{
|
||||||
|
Client = new TestRealtimeMultiplayerClient(),
|
||||||
|
RoomManager = new TestRealtimeRoomManager(),
|
||||||
|
content = new Container { RelativeSizeAxes = Axes.Both }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,116 @@
|
|||||||
|
// 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 osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Game.Online.API;
|
||||||
|
using osu.Game.Online.API.Requests;
|
||||||
|
using osu.Game.Online.Multiplayer;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
using osu.Game.Scoring;
|
||||||
|
using osu.Game.Screens.Multi.Lounge.Components;
|
||||||
|
using osu.Game.Screens.Multi.RealtimeMultiplayer;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Visual.RealtimeMultiplayer
|
||||||
|
{
|
||||||
|
public class TestRealtimeRoomManager : RealtimeRoomManager
|
||||||
|
{
|
||||||
|
[Resolved]
|
||||||
|
private IAPIProvider api { get; set; }
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private OsuGameBase game { get; set; }
|
||||||
|
|
||||||
|
[Cached]
|
||||||
|
public readonly Bindable<FilterCriteria> Filter = new Bindable<FilterCriteria>(new FilterCriteria());
|
||||||
|
|
||||||
|
private readonly List<Room> rooms = new List<Room>();
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
int currentScoreId = 0;
|
||||||
|
int currentRoomId = 0;
|
||||||
|
|
||||||
|
((DummyAPIAccess)api).HandleRequest = req =>
|
||||||
|
{
|
||||||
|
switch (req)
|
||||||
|
{
|
||||||
|
case CreateRoomRequest createRoomRequest:
|
||||||
|
var createdRoom = new APICreatedRoom();
|
||||||
|
|
||||||
|
createdRoom.CopyFrom(createRoomRequest.Room);
|
||||||
|
createdRoom.RoomID.Value ??= currentRoomId++;
|
||||||
|
|
||||||
|
rooms.Add(createdRoom);
|
||||||
|
createRoomRequest.TriggerSuccess(createdRoom);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case JoinRoomRequest joinRoomRequest:
|
||||||
|
joinRoomRequest.TriggerSuccess();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PartRoomRequest partRoomRequest:
|
||||||
|
partRoomRequest.TriggerSuccess();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case GetRoomsRequest getRoomsRequest:
|
||||||
|
var roomsWithoutParticipants = new List<Room>();
|
||||||
|
|
||||||
|
foreach (var r in rooms)
|
||||||
|
{
|
||||||
|
var newRoom = new Room();
|
||||||
|
|
||||||
|
newRoom.CopyFrom(r);
|
||||||
|
newRoom.RecentParticipants.Clear();
|
||||||
|
|
||||||
|
roomsWithoutParticipants.Add(newRoom);
|
||||||
|
}
|
||||||
|
|
||||||
|
getRoomsRequest.TriggerSuccess(roomsWithoutParticipants);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case GetRoomRequest getRoomRequest:
|
||||||
|
getRoomRequest.TriggerSuccess(rooms.Single(r => r.RoomID.Value == getRoomRequest.RoomId));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case GetBeatmapSetRequest getBeatmapSetRequest:
|
||||||
|
var onlineReq = new GetBeatmapSetRequest(getBeatmapSetRequest.ID, getBeatmapSetRequest.Type);
|
||||||
|
onlineReq.Success += res => getBeatmapSetRequest.TriggerSuccess(res);
|
||||||
|
onlineReq.Failure += e => getBeatmapSetRequest.TriggerFailure(e);
|
||||||
|
|
||||||
|
// Get the online API from the game's dependencies.
|
||||||
|
game.Dependencies.Get<IAPIProvider>().Queue(onlineReq);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CreateRoomScoreRequest createRoomScoreRequest:
|
||||||
|
createRoomScoreRequest.TriggerSuccess(new APIScoreToken { ID = 1 });
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SubmitRoomScoreRequest submitRoomScoreRequest:
|
||||||
|
submitRoomScoreRequest.TriggerSuccess(new MultiplayerScore
|
||||||
|
{
|
||||||
|
ID = currentScoreId++,
|
||||||
|
Accuracy = 1,
|
||||||
|
EndedAt = DateTimeOffset.Now,
|
||||||
|
Passed = true,
|
||||||
|
Rank = ScoreRank.S,
|
||||||
|
MaxCombo = 1000,
|
||||||
|
TotalScore = 1000000,
|
||||||
|
User = api.LocalUser.Value,
|
||||||
|
Statistics = new Dictionary<HitResult, int>()
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public new void ClearRooms() => base.ClearRooms();
|
||||||
|
|
||||||
|
public new void Schedule(Action action) => base.Schedule(action);
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,8 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using Humanizer;
|
||||||
|
|
||||||
namespace osu.Game.Utils
|
namespace osu.Game.Utils
|
||||||
{
|
{
|
||||||
public static class FormatUtils
|
public static class FormatUtils
|
||||||
@ -18,5 +20,11 @@ namespace osu.Game.Utils
|
|||||||
/// <param name="accuracy">The accuracy to be formatted</param>
|
/// <param name="accuracy">The accuracy to be formatted</param>
|
||||||
/// <returns>formatted accuracy in percentage</returns>
|
/// <returns>formatted accuracy in percentage</returns>
|
||||||
public static string FormatAccuracy(this decimal accuracy) => $"{accuracy:0.00}%";
|
public static string FormatAccuracy(this decimal accuracy) => $"{accuracy:0.00}%";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Formats the supplied rank/leaderboard position in a consistent, simplified way.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="rank">The rank/position to be formatted.</param>
|
||||||
|
public static string FormatRank(this int rank) => rank.ToMetric(decimals: rank < 100_000 ? 1 : 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user