mirror of
https://github.com/ppy/osu.git
synced 2024-11-06 09:07:25 +08:00
Merge branch 'master' into refactor-selection-rotate-logic
This commit is contained in:
commit
bb2fae55ce
@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
|
|||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Width = 0.5f,
|
Width = 0.5f,
|
||||||
Child = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.ColumnBackground, 0), _ => new DefaultColumnBackground())
|
Child = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.ColumnBackground), _ => new DefaultColumnBackground())
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both
|
RelativeSizeAxes = Axes.Both
|
||||||
}
|
}
|
||||||
@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
|
|||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Width = 0.5f,
|
Width = 0.5f,
|
||||||
Child = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.ColumnBackground, 1), _ => new DefaultColumnBackground())
|
Child = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.ColumnBackground), _ => new DefaultColumnBackground())
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both
|
RelativeSizeAxes = Axes.Both
|
||||||
}
|
}
|
||||||
|
@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
|
|||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Width = 0.5f,
|
Width = 0.5f,
|
||||||
Child = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.KeyArea, 0), _ => new DefaultKeyArea())
|
Child = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.KeyArea), _ => new DefaultKeyArea())
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both
|
RelativeSizeAxes = Axes.Both
|
||||||
},
|
},
|
||||||
@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
|
|||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Width = 0.5f,
|
Width = 0.5f,
|
||||||
Child = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.KeyArea, 1), _ => new DefaultKeyArea())
|
Child = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.KeyArea), _ => new DefaultKeyArea())
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both
|
RelativeSizeAxes = Axes.Both
|
||||||
},
|
},
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Screens;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Beatmaps.ControlPoints;
|
using osu.Game.Beatmaps.ControlPoints;
|
||||||
using osu.Game.Replays;
|
using osu.Game.Replays;
|
||||||
@ -408,6 +409,9 @@ namespace osu.Game.Rulesets.Mania.Tests
|
|||||||
judgementResults = new List<JudgementResult>();
|
judgementResults = new List<JudgementResult>();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0);
|
||||||
|
AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen());
|
||||||
|
|
||||||
AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor?.HasCompleted.Value == true);
|
AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor?.HasCompleted.Value == true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,8 +58,9 @@ namespace osu.Game.Rulesets.Mania.Edit
|
|||||||
|
|
||||||
EditorBeatmap.PerformOnSelection(h =>
|
EditorBeatmap.PerformOnSelection(h =>
|
||||||
{
|
{
|
||||||
if (h is ManiaHitObject maniaObj)
|
maniaPlayfield.Remove(h);
|
||||||
maniaObj.Column += columnDelta;
|
((ManiaHitObject)h).Column += columnDelta;
|
||||||
|
maniaPlayfield.Add(h);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,12 +9,6 @@ namespace osu.Game.Rulesets.Mania
|
|||||||
{
|
{
|
||||||
public class ManiaSkinComponent : GameplaySkinComponent<ManiaSkinComponents>
|
public class ManiaSkinComponent : GameplaySkinComponent<ManiaSkinComponents>
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// The intended <see cref="Column"/> index for this component.
|
|
||||||
/// May be null if the component does not exist in a <see cref="Column"/>.
|
|
||||||
/// </summary>
|
|
||||||
public readonly int? TargetColumn;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The intended <see cref="StageDefinition"/> for this component.
|
/// The intended <see cref="StageDefinition"/> for this component.
|
||||||
/// May be null if the component is not a direct member of a <see cref="Stage"/>.
|
/// May be null if the component is not a direct member of a <see cref="Stage"/>.
|
||||||
@ -25,12 +19,10 @@ namespace osu.Game.Rulesets.Mania
|
|||||||
/// Creates a new <see cref="ManiaSkinComponent"/>.
|
/// Creates a new <see cref="ManiaSkinComponent"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="component">The component.</param>
|
/// <param name="component">The component.</param>
|
||||||
/// <param name="targetColumn">The intended <see cref="Column"/> index for this component. May be null if the component does not exist in a <see cref="Column"/>.</param>
|
|
||||||
/// <param name="stageDefinition">The intended <see cref="StageDefinition"/> for this component. May be null if the component is not a direct member of a <see cref="Stage"/>.</param>
|
/// <param name="stageDefinition">The intended <see cref="StageDefinition"/> for this component. May be null if the component is not a direct member of a <see cref="Stage"/>.</param>
|
||||||
public ManiaSkinComponent(ManiaSkinComponents component, int? targetColumn = null, StageDefinition? stageDefinition = null)
|
public ManiaSkinComponent(ManiaSkinComponents component, StageDefinition? stageDefinition = null)
|
||||||
: base(component)
|
: base(component)
|
||||||
{
|
{
|
||||||
TargetColumn = targetColumn;
|
|
||||||
StageDefinition = stageDefinition;
|
StageDefinition = stageDefinition;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
@ -12,6 +13,7 @@ using osu.Game.Rulesets.Objects.Drawables;
|
|||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Rulesets.UI.Scrolling;
|
using osu.Game.Rulesets.UI.Scrolling;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||||
{
|
{
|
||||||
@ -29,21 +31,21 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
|||||||
public DrawableHoldNoteHead Head => headContainer.Child;
|
public DrawableHoldNoteHead Head => headContainer.Child;
|
||||||
public DrawableHoldNoteTail Tail => tailContainer.Child;
|
public DrawableHoldNoteTail Tail => tailContainer.Child;
|
||||||
|
|
||||||
private readonly Container<DrawableHoldNoteHead> headContainer;
|
private Container<DrawableHoldNoteHead> headContainer;
|
||||||
private readonly Container<DrawableHoldNoteTail> tailContainer;
|
private Container<DrawableHoldNoteTail> tailContainer;
|
||||||
private readonly Container<DrawableHoldNoteTick> tickContainer;
|
private Container<DrawableHoldNoteTick> tickContainer;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Contains the size of the hold note covering the whole head/tail bounds. The size of this container changes as the hold note is being pressed.
|
/// Contains the size of the hold note covering the whole head/tail bounds. The size of this container changes as the hold note is being pressed.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly Container sizingContainer;
|
private Container sizingContainer;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Contains the contents of the hold note that should be masked as the hold note is being pressed. Follows changes in the size of <see cref="sizingContainer"/>.
|
/// Contains the contents of the hold note that should be masked as the hold note is being pressed. Follows changes in the size of <see cref="sizingContainer"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly Container maskingContainer;
|
private Container maskingContainer;
|
||||||
|
|
||||||
private readonly SkinnableDrawable bodyPiece;
|
private SkinnableDrawable bodyPiece;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Time at which the user started holding this hold note. Null if the user is not holding this hold note.
|
/// Time at which the user started holding this hold note. Null if the user is not holding this hold note.
|
||||||
@ -60,11 +62,19 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private double? releaseTime;
|
private double? releaseTime;
|
||||||
|
|
||||||
|
public DrawableHoldNote()
|
||||||
|
: this(null)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
public DrawableHoldNote(HoldNote hitObject)
|
public DrawableHoldNote(HoldNote hitObject)
|
||||||
: base(hitObject)
|
: base(hitObject)
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.X;
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load()
|
||||||
|
{
|
||||||
Container maskedContents;
|
Container maskedContents;
|
||||||
|
|
||||||
AddRangeInternal(new Drawable[]
|
AddRangeInternal(new Drawable[]
|
||||||
@ -86,7 +96,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
|||||||
headContainer = new Container<DrawableHoldNoteHead> { RelativeSizeAxes = Axes.Both }
|
headContainer = new Container<DrawableHoldNoteHead> { RelativeSizeAxes = Axes.Both }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
bodyPiece = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HoldNoteBody, hitObject.Column), _ => new DefaultBodyPiece
|
bodyPiece = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HoldNoteBody), _ => new DefaultBodyPiece
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
})
|
})
|
||||||
@ -105,6 +115,16 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void OnApply()
|
||||||
|
{
|
||||||
|
base.OnApply();
|
||||||
|
|
||||||
|
sizingContainer.Size = Vector2.One;
|
||||||
|
HoldStartTime = null;
|
||||||
|
HoldBrokenTime = null;
|
||||||
|
releaseTime = null;
|
||||||
|
}
|
||||||
|
|
||||||
protected override void AddNestedHitObject(DrawableHitObject hitObject)
|
protected override void AddNestedHitObject(DrawableHitObject hitObject)
|
||||||
{
|
{
|
||||||
base.AddNestedHitObject(hitObject);
|
base.AddNestedHitObject(hitObject);
|
||||||
@ -128,37 +148,23 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
|||||||
protected override void ClearNestedHitObjects()
|
protected override void ClearNestedHitObjects()
|
||||||
{
|
{
|
||||||
base.ClearNestedHitObjects();
|
base.ClearNestedHitObjects();
|
||||||
headContainer.Clear();
|
headContainer.Clear(false);
|
||||||
tailContainer.Clear();
|
tailContainer.Clear(false);
|
||||||
tickContainer.Clear();
|
tickContainer.Clear(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject)
|
protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject)
|
||||||
{
|
{
|
||||||
switch (hitObject)
|
switch (hitObject)
|
||||||
{
|
{
|
||||||
case TailNote _:
|
case TailNote tail:
|
||||||
return new DrawableHoldNoteTail(this)
|
return new DrawableHoldNoteTail(tail);
|
||||||
{
|
|
||||||
Anchor = Anchor.TopCentre,
|
|
||||||
Origin = Anchor.TopCentre,
|
|
||||||
AccentColour = { BindTarget = AccentColour }
|
|
||||||
};
|
|
||||||
|
|
||||||
case Note _:
|
case HeadNote head:
|
||||||
return new DrawableHoldNoteHead(this)
|
return new DrawableHoldNoteHead(head);
|
||||||
{
|
|
||||||
Anchor = Anchor.TopCentre,
|
|
||||||
Origin = Anchor.TopCentre,
|
|
||||||
AccentColour = { BindTarget = AccentColour }
|
|
||||||
};
|
|
||||||
|
|
||||||
case HoldNoteTick tick:
|
case HoldNoteTick tick:
|
||||||
return new DrawableHoldNoteTick(tick)
|
return new DrawableHoldNoteTick(tick);
|
||||||
{
|
|
||||||
HoldStartTime = () => HoldStartTime,
|
|
||||||
AccentColour = { BindTarget = AccentColour }
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return base.CreateNestedHitObject(hitObject);
|
return base.CreateNestedHitObject(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 osu.Framework.Graphics;
|
||||||
using osu.Game.Rulesets.Objects.Drawables;
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||||
@ -12,11 +13,18 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
|||||||
{
|
{
|
||||||
protected override ManiaSkinComponents Component => ManiaSkinComponents.HoldNoteHead;
|
protected override ManiaSkinComponents Component => ManiaSkinComponents.HoldNoteHead;
|
||||||
|
|
||||||
public DrawableHoldNoteHead(DrawableHoldNote holdNote)
|
public DrawableHoldNoteHead()
|
||||||
: base(holdNote.HitObject.Head)
|
: this(null)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public DrawableHoldNoteHead(HeadNote headNote)
|
||||||
|
: base(headNote)
|
||||||
|
{
|
||||||
|
Anchor = Anchor.TopCentre;
|
||||||
|
Origin = Anchor.TopCentre;
|
||||||
|
}
|
||||||
|
|
||||||
public void UpdateResult() => base.UpdateResult(true);
|
public void UpdateResult() => base.UpdateResult(true);
|
||||||
|
|
||||||
protected override void UpdateInitialTransforms()
|
protected override void UpdateInitialTransforms()
|
||||||
|
@ -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.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||||
@ -20,12 +21,18 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
|||||||
|
|
||||||
protected override ManiaSkinComponents Component => ManiaSkinComponents.HoldNoteTail;
|
protected override ManiaSkinComponents Component => ManiaSkinComponents.HoldNoteTail;
|
||||||
|
|
||||||
private readonly DrawableHoldNote holdNote;
|
protected DrawableHoldNote HoldNote => (DrawableHoldNote)ParentHitObject;
|
||||||
|
|
||||||
public DrawableHoldNoteTail(DrawableHoldNote holdNote)
|
public DrawableHoldNoteTail()
|
||||||
: base(holdNote.HitObject.Tail)
|
: this(null)
|
||||||
{
|
{
|
||||||
this.holdNote = holdNote;
|
}
|
||||||
|
|
||||||
|
public DrawableHoldNoteTail(TailNote tailNote)
|
||||||
|
: base(tailNote)
|
||||||
|
{
|
||||||
|
Anchor = Anchor.TopCentre;
|
||||||
|
Origin = Anchor.TopCentre;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UpdateResult() => base.UpdateResult(true);
|
public void UpdateResult() => base.UpdateResult(true);
|
||||||
@ -54,7 +61,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
|||||||
ApplyResult(r =>
|
ApplyResult(r =>
|
||||||
{
|
{
|
||||||
// If the head wasn't hit or the hold note was broken, cap the max score to Meh.
|
// If the head wasn't hit or the hold note was broken, cap the max score to Meh.
|
||||||
if (result > HitResult.Meh && (!holdNote.Head.IsHit || holdNote.HoldBrokenTime != null))
|
if (result > HitResult.Meh && (!HoldNote.Head.IsHit || HoldNote.HoldBrokenTime != null))
|
||||||
result = HitResult.Meh;
|
result = HitResult.Meh;
|
||||||
|
|
||||||
r.Type = result;
|
r.Type = result;
|
||||||
|
@ -2,7 +2,8 @@
|
|||||||
// 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 osuTK;
|
using System.Diagnostics;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
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;
|
||||||
@ -19,38 +20,48 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// References the time at which the user started holding the hold note.
|
/// References the time at which the user started holding the hold note.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Func<double?> HoldStartTime;
|
private Func<double?> holdStartTime;
|
||||||
|
|
||||||
|
private Container glowContainer;
|
||||||
|
|
||||||
|
public DrawableHoldNoteTick()
|
||||||
|
: this(null)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
public DrawableHoldNoteTick(HoldNoteTick hitObject)
|
public DrawableHoldNoteTick(HoldNoteTick hitObject)
|
||||||
: base(hitObject)
|
: base(hitObject)
|
||||||
{
|
{
|
||||||
Container glowContainer;
|
|
||||||
|
|
||||||
Anchor = Anchor.TopCentre;
|
Anchor = Anchor.TopCentre;
|
||||||
Origin = Anchor.TopCentre;
|
Origin = Anchor.TopCentre;
|
||||||
|
|
||||||
RelativeSizeAxes = Axes.X;
|
RelativeSizeAxes = Axes.X;
|
||||||
Size = new Vector2(1);
|
}
|
||||||
|
|
||||||
AddRangeInternal(new[]
|
[BackgroundDependencyLoader]
|
||||||
|
private void load()
|
||||||
|
{
|
||||||
|
AddInternal(glowContainer = new CircularContainer
|
||||||
{
|
{
|
||||||
glowContainer = new CircularContainer
|
Anchor = Anchor.TopCentre,
|
||||||
|
Origin = Anchor.TopCentre,
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Masking = true,
|
||||||
|
Children = new[]
|
||||||
{
|
{
|
||||||
Anchor = Anchor.TopCentre,
|
new Box
|
||||||
Origin = Anchor.TopCentre,
|
|
||||||
RelativeSizeAxes = Axes.Both,
|
|
||||||
Masking = true,
|
|
||||||
Children = new[]
|
|
||||||
{
|
{
|
||||||
new Box
|
RelativeSizeAxes = Axes.Both,
|
||||||
{
|
Alpha = 0,
|
||||||
RelativeSizeAxes = Axes.Both,
|
AlwaysPresent = true
|
||||||
Alpha = 0,
|
|
||||||
AlwaysPresent = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
AccentColour.BindValueChanged(colour =>
|
AccentColour.BindValueChanged(colour =>
|
||||||
{
|
{
|
||||||
@ -64,12 +75,29 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
|||||||
}, true);
|
}, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void OnApply()
|
||||||
|
{
|
||||||
|
base.OnApply();
|
||||||
|
|
||||||
|
Debug.Assert(ParentHitObject != null);
|
||||||
|
|
||||||
|
var holdNote = (DrawableHoldNote)ParentHitObject;
|
||||||
|
holdStartTime = () => holdNote.HoldStartTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnFree()
|
||||||
|
{
|
||||||
|
base.OnFree();
|
||||||
|
|
||||||
|
holdStartTime = null;
|
||||||
|
}
|
||||||
|
|
||||||
protected override void CheckForResult(bool userTriggered, double timeOffset)
|
protected override void CheckForResult(bool userTriggered, double timeOffset)
|
||||||
{
|
{
|
||||||
if (Time.Current < HitObject.StartTime)
|
if (Time.Current < HitObject.StartTime)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var startTime = HoldStartTime?.Invoke();
|
var startTime = holdStartTime?.Invoke();
|
||||||
|
|
||||||
if (startTime == null || startTime > HitObject.StartTime)
|
if (startTime == null || startTime > HitObject.StartTime)
|
||||||
ApplyResult(r => r.Type = r.Judgement.MinResult);
|
ApplyResult(r => r.Type = r.Judgement.MinResult);
|
||||||
|
@ -50,6 +50,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
|||||||
protected DrawableManiaHitObject(ManiaHitObject hitObject)
|
protected DrawableManiaHitObject(ManiaHitObject hitObject)
|
||||||
: base(hitObject)
|
: base(hitObject)
|
||||||
{
|
{
|
||||||
|
RelativeSizeAxes = Axes.X;
|
||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader(true)]
|
[BackgroundDependencyLoader(true)]
|
||||||
@ -59,9 +60,31 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
|||||||
Action.BindTo(action);
|
Action.BindTo(action);
|
||||||
|
|
||||||
Direction.BindTo(scrollingInfo.Direction);
|
Direction.BindTo(scrollingInfo.Direction);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
Direction.BindValueChanged(OnDirectionChanged, true);
|
Direction.BindValueChanged(OnDirectionChanged, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void OnApply()
|
||||||
|
{
|
||||||
|
base.OnApply();
|
||||||
|
|
||||||
|
if (ParentHitObject != null)
|
||||||
|
AccentColour.BindTo(ParentHitObject.AccentColour);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnFree()
|
||||||
|
{
|
||||||
|
base.OnFree();
|
||||||
|
|
||||||
|
if (ParentHitObject != null)
|
||||||
|
AccentColour.UnbindFrom(ParentHitObject.AccentColour);
|
||||||
|
}
|
||||||
|
|
||||||
private double computedLifetimeStart;
|
private double computedLifetimeStart;
|
||||||
|
|
||||||
public override double LifetimeStart
|
public override double LifetimeStart
|
||||||
@ -147,12 +170,11 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
|||||||
public abstract class DrawableManiaHitObject<TObject> : DrawableManiaHitObject
|
public abstract class DrawableManiaHitObject<TObject> : DrawableManiaHitObject
|
||||||
where TObject : ManiaHitObject
|
where TObject : ManiaHitObject
|
||||||
{
|
{
|
||||||
public new readonly TObject HitObject;
|
public new TObject HitObject => (TObject)base.HitObject;
|
||||||
|
|
||||||
protected DrawableManiaHitObject(TObject hitObject)
|
protected DrawableManiaHitObject(TObject hitObject)
|
||||||
: base(hitObject)
|
: base(hitObject)
|
||||||
{
|
{
|
||||||
HitObject = hitObject;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -33,31 +33,37 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
|||||||
|
|
||||||
protected virtual ManiaSkinComponents Component => ManiaSkinComponents.Note;
|
protected virtual ManiaSkinComponents Component => ManiaSkinComponents.Note;
|
||||||
|
|
||||||
private readonly Drawable headPiece;
|
private Drawable headPiece;
|
||||||
|
|
||||||
|
public DrawableNote()
|
||||||
|
: this(null)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
public DrawableNote(Note hitObject)
|
public DrawableNote(Note hitObject)
|
||||||
: base(hitObject)
|
: base(hitObject)
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.X;
|
|
||||||
AutoSizeAxes = Axes.Y;
|
AutoSizeAxes = Axes.Y;
|
||||||
|
|
||||||
AddInternal(headPiece = new SkinnableDrawable(new ManiaSkinComponent(Component, hitObject.Column), _ => new DefaultNotePiece())
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.X,
|
|
||||||
AutoSizeAxes = Axes.Y
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader(true)]
|
[BackgroundDependencyLoader(true)]
|
||||||
private void load(ManiaRulesetConfigManager rulesetConfig)
|
private void load(ManiaRulesetConfigManager rulesetConfig)
|
||||||
{
|
{
|
||||||
rulesetConfig?.BindWith(ManiaRulesetSetting.TimingBasedNoteColouring, configTimingBasedNoteColouring);
|
rulesetConfig?.BindWith(ManiaRulesetSetting.TimingBasedNoteColouring, configTimingBasedNoteColouring);
|
||||||
|
|
||||||
|
AddInternal(headPiece = new SkinnableDrawable(new ManiaSkinComponent(Component), _ => new DefaultNotePiece())
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
AutoSizeAxes = Axes.Y
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
{
|
{
|
||||||
HitObject.StartTimeBindable.BindValueChanged(_ => updateSnapColour());
|
base.LoadComplete();
|
||||||
configTimingBasedNoteColouring.BindValueChanged(_ => updateSnapColour(), true);
|
|
||||||
|
configTimingBasedNoteColouring.BindValueChanged(_ => updateSnapColour());
|
||||||
|
StartTimeBindable.BindValueChanged(_ => updateSnapColour(), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnDirectionChanged(ValueChangedEvent<ScrollingDirection> e)
|
protected override void OnDirectionChanged(ValueChangedEvent<ScrollingDirection> e)
|
||||||
@ -102,7 +108,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
|||||||
|
|
||||||
private void updateSnapColour()
|
private void updateSnapColour()
|
||||||
{
|
{
|
||||||
if (beatmap == null) return;
|
if (beatmap == null || HitObject == null) return;
|
||||||
|
|
||||||
int snapDivisor = beatmap.ControlPointInfo.GetClosestBeatDivisor(HitObject.StartTime);
|
int snapDivisor = beatmap.ControlPointInfo.GetClosestBeatDivisor(HitObject.StartTime);
|
||||||
|
|
||||||
|
9
osu.Game.Rulesets.Mania/Objects/HeadNote.cs
Normal file
9
osu.Game.Rulesets.Mania/Objects/HeadNote.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Mania.Objects
|
||||||
|
{
|
||||||
|
public class HeadNote : Note
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
@ -72,7 +72,7 @@ namespace osu.Game.Rulesets.Mania.Objects
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The head note of the hold.
|
/// The head note of the hold.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Note Head { get; private set; }
|
public HeadNote Head { get; private set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The tail note of the hold.
|
/// The tail note of the hold.
|
||||||
@ -98,7 +98,7 @@ namespace osu.Game.Rulesets.Mania.Objects
|
|||||||
|
|
||||||
createTicks(cancellationToken);
|
createTicks(cancellationToken);
|
||||||
|
|
||||||
AddNested(Head = new Note
|
AddNested(Head = new HeadNote
|
||||||
{
|
{
|
||||||
StartTime = StartTime,
|
StartTime = StartTime,
|
||||||
Column = Column,
|
Column = Column,
|
||||||
|
@ -17,6 +17,7 @@ using osu.Game.Rulesets.UI.Scrolling;
|
|||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||||
|
using osu.Game.Rulesets.Mania.Objects;
|
||||||
using osu.Game.Rulesets.Mania.Objects.Drawables;
|
using osu.Game.Rulesets.Mania.Objects.Drawables;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Mania.UI
|
namespace osu.Game.Rulesets.Mania.UI
|
||||||
@ -55,7 +56,7 @@ namespace osu.Game.Rulesets.Mania.UI
|
|||||||
RelativeSizeAxes = Axes.Y;
|
RelativeSizeAxes = Axes.Y;
|
||||||
Width = COLUMN_WIDTH;
|
Width = COLUMN_WIDTH;
|
||||||
|
|
||||||
Drawable background = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.ColumnBackground, Index), _ => new DefaultColumnBackground())
|
Drawable background = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.ColumnBackground), _ => new DefaultColumnBackground())
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both
|
RelativeSizeAxes = Axes.Both
|
||||||
};
|
};
|
||||||
@ -66,7 +67,7 @@ namespace osu.Game.Rulesets.Mania.UI
|
|||||||
// For input purposes, the background is added at the highest depth, but is then proxied back below all other elements
|
// For input purposes, the background is added at the highest depth, but is then proxied back below all other elements
|
||||||
background.CreateProxy(),
|
background.CreateProxy(),
|
||||||
HitObjectArea = new ColumnHitObjectArea(Index, HitObjectContainer) { RelativeSizeAxes = Axes.Both },
|
HitObjectArea = new ColumnHitObjectArea(Index, HitObjectContainer) { RelativeSizeAxes = Axes.Both },
|
||||||
new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.KeyArea, Index), _ => new DefaultKeyArea())
|
new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.KeyArea), _ => new DefaultKeyArea())
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both
|
RelativeSizeAxes = Axes.Both
|
||||||
},
|
},
|
||||||
@ -83,6 +84,19 @@ namespace osu.Game.Rulesets.Mania.UI
|
|||||||
hitPolicy = new OrderedHitPolicy(HitObjectContainer);
|
hitPolicy = new OrderedHitPolicy(HitObjectContainer);
|
||||||
|
|
||||||
TopLevelContainer.Add(HitObjectArea.Explosions.CreateProxy());
|
TopLevelContainer.Add(HitObjectArea.Explosions.CreateProxy());
|
||||||
|
|
||||||
|
RegisterPool<Note, DrawableNote>(10, 50);
|
||||||
|
RegisterPool<HoldNote, DrawableHoldNote>(10, 50);
|
||||||
|
RegisterPool<HeadNote, DrawableHoldNoteHead>(10, 50);
|
||||||
|
RegisterPool<TailNote, DrawableHoldNoteTail>(10, 50);
|
||||||
|
RegisterPool<HoldNoteTick, DrawableHoldNoteTick>(50, 250);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
NewResult += OnNewResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ColumnType ColumnType { get; set; }
|
public ColumnType ColumnType { get; set; }
|
||||||
@ -98,28 +112,14 @@ namespace osu.Game.Rulesets.Mania.UI
|
|||||||
return dependencies;
|
return dependencies;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
protected override void OnNewDrawableHitObject(DrawableHitObject drawableHitObject)
|
||||||
/// Adds a DrawableHitObject to this Playfield.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="hitObject">The DrawableHitObject to add.</param>
|
|
||||||
public override void Add(DrawableHitObject hitObject)
|
|
||||||
{
|
{
|
||||||
hitObject.AccentColour.Value = AccentColour;
|
base.OnNewDrawableHitObject(drawableHitObject);
|
||||||
hitObject.OnNewResult += OnNewResult;
|
|
||||||
|
|
||||||
DrawableManiaHitObject maniaObject = (DrawableManiaHitObject)hitObject;
|
DrawableManiaHitObject maniaObject = (DrawableManiaHitObject)drawableHitObject;
|
||||||
|
|
||||||
|
maniaObject.AccentColour.Value = AccentColour;
|
||||||
maniaObject.CheckHittable = hitPolicy.IsHittable;
|
maniaObject.CheckHittable = hitPolicy.IsHittable;
|
||||||
|
|
||||||
base.Add(hitObject);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override bool Remove(DrawableHitObject h)
|
|
||||||
{
|
|
||||||
if (!base.Remove(h))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
h.OnNewResult -= OnNewResult;
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void OnNewResult(DrawableHitObject judgedObject, JudgementResult result)
|
internal void OnNewResult(DrawableHitObject judgedObject, JudgementResult result)
|
||||||
|
@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Mania.UI.Components
|
|||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Depth = 2,
|
Depth = 2,
|
||||||
},
|
},
|
||||||
hitTarget = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HitTarget, columnIndex), _ => new DefaultHitTarget())
|
hitTarget = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HitTarget), _ => new DefaultHitTarget())
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.X,
|
RelativeSizeAxes = Axes.X,
|
||||||
Depth = 1
|
Depth = 1
|
||||||
|
@ -18,7 +18,6 @@ using osu.Game.Replays;
|
|||||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||||
using osu.Game.Rulesets.Mania.Configuration;
|
using osu.Game.Rulesets.Mania.Configuration;
|
||||||
using osu.Game.Rulesets.Mania.Objects;
|
using osu.Game.Rulesets.Mania.Objects;
|
||||||
using osu.Game.Rulesets.Mania.Objects.Drawables;
|
|
||||||
using osu.Game.Rulesets.Mania.Replays;
|
using osu.Game.Rulesets.Mania.Replays;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
@ -134,20 +133,7 @@ namespace osu.Game.Rulesets.Mania.UI
|
|||||||
|
|
||||||
protected override PassThroughInputManager CreateInputManager() => new ManiaInputManager(Ruleset.RulesetInfo, Variant);
|
protected override PassThroughInputManager CreateInputManager() => new ManiaInputManager(Ruleset.RulesetInfo, Variant);
|
||||||
|
|
||||||
public override DrawableHitObject<ManiaHitObject> CreateDrawableRepresentation(ManiaHitObject h)
|
public override DrawableHitObject<ManiaHitObject> CreateDrawableRepresentation(ManiaHitObject h) => null;
|
||||||
{
|
|
||||||
switch (h)
|
|
||||||
{
|
|
||||||
case HoldNote holdNote:
|
|
||||||
return new DrawableHoldNote(holdNote);
|
|
||||||
|
|
||||||
case Note note:
|
|
||||||
return new DrawableNote(note);
|
|
||||||
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new ManiaFramedReplayInputHandler(replay);
|
protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new ManiaFramedReplayInputHandler(replay);
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ using System.Linq;
|
|||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||||
using osu.Game.Rulesets.Mania.Objects;
|
using osu.Game.Rulesets.Mania.Objects;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
using osu.Game.Rulesets.Objects.Drawables;
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
using osu.Game.Rulesets.UI.Scrolling;
|
using osu.Game.Rulesets.UI.Scrolling;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
@ -56,6 +57,10 @@ namespace osu.Game.Rulesets.Mania.UI
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override void Add(HitObject hitObject) => getStageByColumn(((ManiaHitObject)hitObject).Column).Add(hitObject);
|
||||||
|
|
||||||
|
public override bool Remove(HitObject hitObject) => getStageByColumn(((ManiaHitObject)hitObject).Column).Remove(hitObject);
|
||||||
|
|
||||||
public override void Add(DrawableHitObject h) => getStageByColumn(((ManiaHitObject)h.HitObject).Column).Add(h);
|
public override void Add(DrawableHitObject h) => getStageByColumn(((ManiaHitObject)h.HitObject).Column).Add(h);
|
||||||
|
|
||||||
public override bool Remove(DrawableHitObject h) => getStageByColumn(((ManiaHitObject)h.HitObject).Column).Remove(h);
|
public override bool Remove(DrawableHitObject h) => getStageByColumn(((ManiaHitObject)h.HitObject).Column).Remove(h);
|
||||||
|
@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Mania.UI
|
|||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load()
|
private void load()
|
||||||
{
|
{
|
||||||
InternalChild = skinnableExplosion = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HitExplosion, column.Index), _ => new DefaultHitExplosion())
|
InternalChild = skinnableExplosion = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HitExplosion), _ => new DefaultHitExplosion())
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both
|
RelativeSizeAxes = Axes.Both
|
||||||
};
|
};
|
||||||
|
@ -11,6 +11,7 @@ using osu.Game.Rulesets.Mania.Beatmaps;
|
|||||||
using osu.Game.Rulesets.Mania.Objects;
|
using osu.Game.Rulesets.Mania.Objects;
|
||||||
using osu.Game.Rulesets.Mania.Objects.Drawables;
|
using osu.Game.Rulesets.Mania.Objects.Drawables;
|
||||||
using osu.Game.Rulesets.Mania.UI.Components;
|
using osu.Game.Rulesets.Mania.UI.Components;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
using osu.Game.Rulesets.Objects.Drawables;
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
using osu.Game.Rulesets.UI;
|
using osu.Game.Rulesets.UI;
|
||||||
using osu.Game.Rulesets.UI.Scrolling;
|
using osu.Game.Rulesets.UI.Scrolling;
|
||||||
@ -132,33 +133,19 @@ namespace osu.Game.Rulesets.Mania.UI
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Add(DrawableHitObject h)
|
protected override void LoadComplete()
|
||||||
{
|
{
|
||||||
var maniaObject = (ManiaHitObject)h.HitObject;
|
base.LoadComplete();
|
||||||
|
NewResult += OnNewResult;
|
||||||
int columnIndex = -1;
|
|
||||||
|
|
||||||
maniaObject.ColumnBindable.BindValueChanged(_ =>
|
|
||||||
{
|
|
||||||
if (columnIndex != -1)
|
|
||||||
Columns.ElementAt(columnIndex).Remove(h);
|
|
||||||
|
|
||||||
columnIndex = maniaObject.Column - firstColumnIndex;
|
|
||||||
Columns.ElementAt(columnIndex).Add(h);
|
|
||||||
}, true);
|
|
||||||
|
|
||||||
h.OnNewResult += OnNewResult;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool Remove(DrawableHitObject h)
|
public override void Add(HitObject hitObject) => Columns.ElementAt(((ManiaHitObject)hitObject).Column - firstColumnIndex).Add(hitObject);
|
||||||
{
|
|
||||||
var maniaObject = (ManiaHitObject)h.HitObject;
|
|
||||||
int columnIndex = maniaObject.Column - firstColumnIndex;
|
|
||||||
Columns.ElementAt(columnIndex).Remove(h);
|
|
||||||
|
|
||||||
h.OnNewResult -= OnNewResult;
|
public override bool Remove(HitObject hitObject) => Columns.ElementAt(((ManiaHitObject)hitObject).Column - firstColumnIndex).Remove(hitObject);
|
||||||
return true;
|
|
||||||
}
|
public override void Add(DrawableHitObject h) => Columns.ElementAt(((ManiaHitObject)h.HitObject).Column - firstColumnIndex).Add(h);
|
||||||
|
|
||||||
|
public override bool Remove(DrawableHitObject h) => Columns.ElementAt(((ManiaHitObject)h.HitObject).Column - firstColumnIndex).Remove(h);
|
||||||
|
|
||||||
public void Add(BarLine barline) => base.Add(new DrawableBarLine(barline));
|
public void Add(BarLine barline) => base.Add(new DrawableBarLine(barline));
|
||||||
|
|
||||||
|
@ -243,7 +243,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
|||||||
int totalCount = Pieces.Count(p => p.IsSelected.Value);
|
int totalCount = Pieces.Count(p => p.IsSelected.Value);
|
||||||
int countOfState = Pieces.Where(p => p.IsSelected.Value).Count(p => p.ControlPoint.Type.Value == type);
|
int countOfState = Pieces.Where(p => p.IsSelected.Value).Count(p => p.ControlPoint.Type.Value == type);
|
||||||
|
|
||||||
var item = new PathTypeMenuItem(type, () =>
|
var item = new TernaryStateRadioMenuItem(type == null ? "Inherit" : type.ToString().Humanize(), MenuItemType.Standard, _ =>
|
||||||
{
|
{
|
||||||
foreach (var p in Pieces.Where(p => p.IsSelected.Value))
|
foreach (var p in Pieces.Where(p => p.IsSelected.Value))
|
||||||
updatePathType(p, type);
|
updatePathType(p, type);
|
||||||
@ -258,15 +258,5 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
|||||||
|
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
private class PathTypeMenuItem : TernaryStateMenuItem
|
|
||||||
{
|
|
||||||
public PathTypeMenuItem(PathType? type, Action action)
|
|
||||||
: base(type == null ? "Inherit" : type.ToString().Humanize(), changeState, MenuItemType.Standard, _ => action?.Invoke())
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
private static TernaryState changeState(TernaryState state) => TernaryState.True;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -76,10 +76,10 @@ namespace osu.Game.Rulesets.Taiko.Edit
|
|||||||
protected override IEnumerable<MenuItem> GetContextMenuItemsForSelection(IEnumerable<SelectionBlueprint<HitObject>> selection)
|
protected override IEnumerable<MenuItem> GetContextMenuItemsForSelection(IEnumerable<SelectionBlueprint<HitObject>> selection)
|
||||||
{
|
{
|
||||||
if (selection.All(s => s.Item is Hit))
|
if (selection.All(s => s.Item is Hit))
|
||||||
yield return new TernaryStateMenuItem("Rim") { State = { BindTarget = selectionRimState } };
|
yield return new TernaryStateToggleMenuItem("Rim") { State = { BindTarget = selectionRimState } };
|
||||||
|
|
||||||
if (selection.All(s => s.Item is TaikoHitObject))
|
if (selection.All(s => s.Item is TaikoHitObject))
|
||||||
yield return new TernaryStateMenuItem("Strong") { State = { BindTarget = selectionStrongState } };
|
yield return new TernaryStateToggleMenuItem("Strong") { State = { BindTarget = selectionStrongState } };
|
||||||
|
|
||||||
foreach (var item in base.GetContextMenuItemsForSelection(selection))
|
foreach (var item in base.GetContextMenuItemsForSelection(selection))
|
||||||
yield return item;
|
yield return item;
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
// 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.Collections.Generic;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
@ -11,29 +14,35 @@ using osu.Game.Graphics.Sprites;
|
|||||||
using osu.Game.Rulesets.Catch.Scoring;
|
using osu.Game.Rulesets.Catch.Scoring;
|
||||||
using osu.Game.Rulesets.Judgements;
|
using osu.Game.Rulesets.Judgements;
|
||||||
using osu.Game.Rulesets.Mania.Scoring;
|
using osu.Game.Rulesets.Mania.Scoring;
|
||||||
|
using osu.Game.Rulesets.Mods;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Rulesets.Osu;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using osu.Game.Rulesets.Osu.Scoring;
|
using osu.Game.Rulesets.Osu.Scoring;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Rulesets.Taiko.Scoring;
|
using osu.Game.Rulesets.Taiko.Scoring;
|
||||||
|
using osu.Game.Rulesets.UI;
|
||||||
|
using osu.Game.Scoring;
|
||||||
using osu.Game.Screens.Play.HUD.HitErrorMeters;
|
using osu.Game.Screens.Play.HUD.HitErrorMeters;
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.Gameplay
|
namespace osu.Game.Tests.Visual.Gameplay
|
||||||
{
|
{
|
||||||
public class TestSceneHitErrorMeter : OsuTestScene
|
public class TestSceneHitErrorMeter : OsuTestScene
|
||||||
{
|
{
|
||||||
private HitWindows hitWindows;
|
|
||||||
|
|
||||||
[Cached]
|
[Cached]
|
||||||
private ScoreProcessor scoreProcessor = new ScoreProcessor();
|
private ScoreProcessor scoreProcessor = new ScoreProcessor();
|
||||||
|
|
||||||
|
[Cached(typeof(DrawableRuleset))]
|
||||||
|
private TestDrawableRuleset drawableRuleset = new TestDrawableRuleset();
|
||||||
|
|
||||||
public TestSceneHitErrorMeter()
|
public TestSceneHitErrorMeter()
|
||||||
{
|
{
|
||||||
recreateDisplay(new OsuHitWindows(), 5);
|
recreateDisplay(new OsuHitWindows(), 5);
|
||||||
|
|
||||||
AddRepeatStep("New random judgement", () => newJudgement(), 40);
|
AddRepeatStep("New random judgement", () => newJudgement(), 40);
|
||||||
|
|
||||||
AddRepeatStep("New max negative", () => newJudgement(-hitWindows.WindowFor(HitResult.Meh)), 20);
|
AddRepeatStep("New max negative", () => newJudgement(-drawableRuleset.HitWindows.WindowFor(HitResult.Meh)), 20);
|
||||||
AddRepeatStep("New max positive", () => newJudgement(hitWindows.WindowFor(HitResult.Meh)), 20);
|
AddRepeatStep("New max positive", () => newJudgement(drawableRuleset.HitWindows.WindowFor(HitResult.Meh)), 20);
|
||||||
AddStep("New fixed judgement (50ms)", () => newJudgement(50));
|
AddStep("New fixed judgement (50ms)", () => newJudgement(50));
|
||||||
|
|
||||||
AddStep("Judgement barrage", () =>
|
AddStep("Judgement barrage", () =>
|
||||||
@ -83,10 +92,10 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
|
|
||||||
private void recreateDisplay(HitWindows hitWindows, float overallDifficulty)
|
private void recreateDisplay(HitWindows hitWindows, float overallDifficulty)
|
||||||
{
|
{
|
||||||
this.hitWindows = hitWindows;
|
|
||||||
|
|
||||||
hitWindows?.SetDifficulty(overallDifficulty);
|
hitWindows?.SetDifficulty(overallDifficulty);
|
||||||
|
|
||||||
|
drawableRuleset.HitWindows = hitWindows;
|
||||||
|
|
||||||
Clear();
|
Clear();
|
||||||
|
|
||||||
Add(new FillFlowContainer
|
Add(new FillFlowContainer
|
||||||
@ -103,40 +112,40 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Add(new BarHitErrorMeter(hitWindows, true)
|
Add(new BarHitErrorMeter
|
||||||
{
|
{
|
||||||
Anchor = Anchor.CentreRight,
|
Anchor = Anchor.CentreRight,
|
||||||
Origin = Anchor.CentreRight,
|
Origin = Anchor.CentreRight,
|
||||||
});
|
});
|
||||||
|
|
||||||
Add(new BarHitErrorMeter(hitWindows, false)
|
Add(new BarHitErrorMeter
|
||||||
{
|
{
|
||||||
Anchor = Anchor.CentreLeft,
|
Anchor = Anchor.CentreLeft,
|
||||||
Origin = Anchor.CentreLeft,
|
Origin = Anchor.CentreLeft,
|
||||||
});
|
});
|
||||||
|
|
||||||
Add(new BarHitErrorMeter(hitWindows, true)
|
Add(new BarHitErrorMeter
|
||||||
{
|
{
|
||||||
Anchor = Anchor.BottomCentre,
|
Anchor = Anchor.BottomCentre,
|
||||||
Origin = Anchor.CentreLeft,
|
Origin = Anchor.CentreLeft,
|
||||||
Rotation = 270,
|
Rotation = 270,
|
||||||
});
|
});
|
||||||
|
|
||||||
Add(new ColourHitErrorMeter(hitWindows)
|
Add(new ColourHitErrorMeter
|
||||||
{
|
{
|
||||||
Anchor = Anchor.CentreRight,
|
Anchor = Anchor.CentreRight,
|
||||||
Origin = Anchor.CentreRight,
|
Origin = Anchor.CentreRight,
|
||||||
Margin = new MarginPadding { Right = 50 }
|
Margin = new MarginPadding { Right = 50 }
|
||||||
});
|
});
|
||||||
|
|
||||||
Add(new ColourHitErrorMeter(hitWindows)
|
Add(new ColourHitErrorMeter
|
||||||
{
|
{
|
||||||
Anchor = Anchor.CentreLeft,
|
Anchor = Anchor.CentreLeft,
|
||||||
Origin = Anchor.CentreLeft,
|
Origin = Anchor.CentreLeft,
|
||||||
Margin = new MarginPadding { Left = 50 }
|
Margin = new MarginPadding { Left = 50 }
|
||||||
});
|
});
|
||||||
|
|
||||||
Add(new ColourHitErrorMeter(hitWindows)
|
Add(new ColourHitErrorMeter
|
||||||
{
|
{
|
||||||
Anchor = Anchor.BottomCentre,
|
Anchor = Anchor.BottomCentre,
|
||||||
Origin = Anchor.CentreLeft,
|
Origin = Anchor.CentreLeft,
|
||||||
@ -147,11 +156,47 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
|
|
||||||
private void newJudgement(double offset = 0)
|
private void newJudgement(double offset = 0)
|
||||||
{
|
{
|
||||||
scoreProcessor.ApplyResult(new JudgementResult(new HitCircle { HitWindows = hitWindows }, new Judgement())
|
scoreProcessor.ApplyResult(new JudgementResult(new HitCircle { HitWindows = drawableRuleset.HitWindows }, new Judgement())
|
||||||
{
|
{
|
||||||
TimeOffset = offset == 0 ? RNG.Next(-150, 150) : offset,
|
TimeOffset = offset == 0 ? RNG.Next(-150, 150) : offset,
|
||||||
Type = HitResult.Perfect,
|
Type = HitResult.Perfect,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SuppressMessage("ReSharper", "UnassignedGetOnlyAutoProperty")]
|
||||||
|
private class TestDrawableRuleset : DrawableRuleset
|
||||||
|
{
|
||||||
|
public HitWindows HitWindows;
|
||||||
|
|
||||||
|
public override IEnumerable<HitObject> Objects => new[] { new HitCircle { HitWindows = HitWindows } };
|
||||||
|
|
||||||
|
public override event Action<JudgementResult> NewResult;
|
||||||
|
public override event Action<JudgementResult> RevertResult;
|
||||||
|
|
||||||
|
public override Playfield Playfield { get; }
|
||||||
|
public override Container Overlays { get; }
|
||||||
|
public override Container FrameStableComponents { get; }
|
||||||
|
public override IFrameStableClock FrameStableClock { get; }
|
||||||
|
public override IReadOnlyList<Mod> Mods { get; }
|
||||||
|
|
||||||
|
public override double GameplayStartTime { get; }
|
||||||
|
public override GameplayCursorContainer Cursor { get; }
|
||||||
|
|
||||||
|
public TestDrawableRuleset()
|
||||||
|
: base(new OsuRuleset())
|
||||||
|
{
|
||||||
|
// won't compile without this.
|
||||||
|
NewResult?.Invoke(null);
|
||||||
|
RevertResult?.Invoke(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void SetReplayScore(Score replayScore) => throw new NotImplementedException();
|
||||||
|
|
||||||
|
public override void SetRecordTarget(Score score) => throw new NotImplementedException();
|
||||||
|
|
||||||
|
public override void RequestResume(Action continueResume) => throw new NotImplementedException();
|
||||||
|
|
||||||
|
public override void CancelResume() => throw new NotImplementedException();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -27,8 +27,8 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
{
|
{
|
||||||
private readonly User streamingUser = new User { Id = MultiplayerTestScene.PLAYER_1_ID, Username = "Test user" };
|
private readonly User streamingUser = new User { Id = MultiplayerTestScene.PLAYER_1_ID, Username = "Test user" };
|
||||||
|
|
||||||
[Cached(typeof(SpectatorStreamingClient))]
|
[Cached(typeof(SpectatorClient))]
|
||||||
private TestSpectatorStreamingClient testSpectatorStreamingClient = new TestSpectatorStreamingClient();
|
private TestSpectatorClient testSpectatorClient = new TestSpectatorClient();
|
||||||
|
|
||||||
[Cached(typeof(UserLookupCache))]
|
[Cached(typeof(UserLookupCache))]
|
||||||
private UserLookupCache lookupCache = new TestUserLookupCache();
|
private UserLookupCache lookupCache = new TestUserLookupCache();
|
||||||
@ -61,8 +61,8 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
|
|
||||||
AddStep("add streaming client", () =>
|
AddStep("add streaming client", () =>
|
||||||
{
|
{
|
||||||
Remove(testSpectatorStreamingClient);
|
Remove(testSpectatorClient);
|
||||||
Add(testSpectatorStreamingClient);
|
Add(testSpectatorClient);
|
||||||
});
|
});
|
||||||
|
|
||||||
finish();
|
finish();
|
||||||
@ -212,9 +212,9 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
|
|
||||||
private void waitForPlayer() => AddUntilStep("wait for player", () => Stack.CurrentScreen is Player);
|
private void waitForPlayer() => AddUntilStep("wait for player", () => Stack.CurrentScreen is Player);
|
||||||
|
|
||||||
private void start(int? beatmapId = null) => AddStep("start play", () => testSpectatorStreamingClient.StartPlay(streamingUser.Id, beatmapId ?? importedBeatmapId));
|
private void start(int? beatmapId = null) => AddStep("start play", () => testSpectatorClient.StartPlay(streamingUser.Id, beatmapId ?? importedBeatmapId));
|
||||||
|
|
||||||
private void finish(int? beatmapId = null) => AddStep("end play", () => testSpectatorStreamingClient.EndPlay(streamingUser.Id, beatmapId ?? importedBeatmapId));
|
private void finish() => AddStep("end play", () => testSpectatorClient.EndPlay(streamingUser.Id));
|
||||||
|
|
||||||
private void checkPaused(bool state) =>
|
private void checkPaused(bool state) =>
|
||||||
AddUntilStep($"game is {(state ? "paused" : "playing")}", () => player.ChildrenOfType<DrawableRuleset>().First().IsPaused.Value == state);
|
AddUntilStep($"game is {(state ? "paused" : "playing")}", () => player.ChildrenOfType<DrawableRuleset>().First().IsPaused.Value == state);
|
||||||
@ -223,7 +223,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
{
|
{
|
||||||
AddStep("send frames", () =>
|
AddStep("send frames", () =>
|
||||||
{
|
{
|
||||||
testSpectatorStreamingClient.SendFrames(streamingUser.Id, nextFrame, count);
|
testSpectatorClient.SendFrames(streamingUser.Id, nextFrame, count);
|
||||||
nextFrame += count;
|
nextFrame += count;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -59,7 +59,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
private IAPIProvider api { get; set; }
|
private IAPIProvider api { get; set; }
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private SpectatorStreamingClient streamingClient { get; set; }
|
private SpectatorClient spectatorClient { get; set; }
|
||||||
|
|
||||||
[Cached]
|
[Cached]
|
||||||
private GameplayBeatmap gameplayBeatmap = new GameplayBeatmap(new Beatmap());
|
private GameplayBeatmap gameplayBeatmap = new GameplayBeatmap(new Beatmap());
|
||||||
@ -69,7 +69,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
{
|
{
|
||||||
replay = new Replay();
|
replay = new Replay();
|
||||||
|
|
||||||
users.BindTo(streamingClient.PlayingUsers);
|
users.BindTo(spectatorClient.PlayingUsers);
|
||||||
users.BindCollectionChanged((obj, args) =>
|
users.BindCollectionChanged((obj, args) =>
|
||||||
{
|
{
|
||||||
switch (args.Action)
|
switch (args.Action)
|
||||||
@ -80,7 +80,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
foreach (int user in args.NewItems)
|
foreach (int user in args.NewItems)
|
||||||
{
|
{
|
||||||
if (user == api.LocalUser.Value.Id)
|
if (user == api.LocalUser.Value.Id)
|
||||||
streamingClient.WatchUser(user);
|
spectatorClient.WatchUser(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
@ -91,14 +91,14 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
foreach (int user in args.OldItems)
|
foreach (int user in args.OldItems)
|
||||||
{
|
{
|
||||||
if (user == api.LocalUser.Value.Id)
|
if (user == api.LocalUser.Value.Id)
|
||||||
streamingClient.StopWatchingUser(user);
|
spectatorClient.StopWatchingUser(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
streamingClient.OnNewFrames += onNewFrames;
|
spectatorClient.OnNewFrames += onNewFrames;
|
||||||
|
|
||||||
Add(new GridContainer
|
Add(new GridContainer
|
||||||
{
|
{
|
||||||
@ -189,7 +189,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
private double latency = SpectatorStreamingClient.TIME_BETWEEN_SENDS;
|
private double latency = SpectatorClient.TIME_BETWEEN_SENDS;
|
||||||
|
|
||||||
protected override void Update()
|
protected override void Update()
|
||||||
{
|
{
|
||||||
@ -233,7 +233,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
AddStep("stop recorder", () =>
|
AddStep("stop recorder", () =>
|
||||||
{
|
{
|
||||||
recorder.Expire();
|
recorder.Expire();
|
||||||
streamingClient.OnNewFrames -= onNewFrames;
|
spectatorClient.OnNewFrames -= onNewFrames;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,8 +23,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
{
|
{
|
||||||
public class TestSceneMultiSpectatorLeaderboard : MultiplayerTestScene
|
public class TestSceneMultiSpectatorLeaderboard : MultiplayerTestScene
|
||||||
{
|
{
|
||||||
[Cached(typeof(SpectatorStreamingClient))]
|
[Cached(typeof(SpectatorClient))]
|
||||||
private TestSpectatorStreamingClient streamingClient = new TestSpectatorStreamingClient();
|
private TestSpectatorClient spectatorClient = new TestSpectatorClient();
|
||||||
|
|
||||||
[Cached(typeof(UserLookupCache))]
|
[Cached(typeof(UserLookupCache))]
|
||||||
private UserLookupCache lookupCache = new TestUserLookupCache();
|
private UserLookupCache lookupCache = new TestUserLookupCache();
|
||||||
@ -42,7 +42,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
{
|
{
|
||||||
base.Content.AddRange(new Drawable[]
|
base.Content.AddRange(new Drawable[]
|
||||||
{
|
{
|
||||||
streamingClient,
|
spectatorClient,
|
||||||
lookupCache,
|
lookupCache,
|
||||||
content = new Container { RelativeSizeAxes = Axes.Both }
|
content = new Container { RelativeSizeAxes = Axes.Both }
|
||||||
});
|
});
|
||||||
@ -59,7 +59,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
|
|
||||||
foreach (var (userId, clock) in clocks)
|
foreach (var (userId, clock) in clocks)
|
||||||
{
|
{
|
||||||
streamingClient.EndPlay(userId, 0);
|
spectatorClient.EndPlay(userId);
|
||||||
clock.CurrentTime = 0;
|
clock.CurrentTime = 0;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -67,7 +67,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
AddStep("create leaderboard", () =>
|
AddStep("create leaderboard", () =>
|
||||||
{
|
{
|
||||||
foreach (var (userId, _) in clocks)
|
foreach (var (userId, _) in clocks)
|
||||||
streamingClient.StartPlay(userId, 0);
|
spectatorClient.StartPlay(userId, 0);
|
||||||
|
|
||||||
Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value);
|
Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value);
|
||||||
|
|
||||||
@ -96,10 +96,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
// For player 2, send frames in sets of 10.
|
// For player 2, send frames in sets of 10.
|
||||||
for (int i = 0; i < 100; i++)
|
for (int i = 0; i < 100; i++)
|
||||||
{
|
{
|
||||||
streamingClient.SendFrames(PLAYER_1_ID, i, 1);
|
spectatorClient.SendFrames(PLAYER_1_ID, i, 1);
|
||||||
|
|
||||||
if (i % 10 == 0)
|
if (i % 10 == 0)
|
||||||
streamingClient.SendFrames(PLAYER_2_ID, i, 10);
|
spectatorClient.SendFrames(PLAYER_2_ID, i, 10);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -22,8 +22,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
{
|
{
|
||||||
public class TestSceneMultiSpectatorScreen : MultiplayerTestScene
|
public class TestSceneMultiSpectatorScreen : MultiplayerTestScene
|
||||||
{
|
{
|
||||||
[Cached(typeof(SpectatorStreamingClient))]
|
[Cached(typeof(SpectatorClient))]
|
||||||
private TestSpectatorStreamingClient streamingClient = new TestSpectatorStreamingClient();
|
private TestSpectatorClient spectatorClient = new TestSpectatorClient();
|
||||||
|
|
||||||
[Cached(typeof(UserLookupCache))]
|
[Cached(typeof(UserLookupCache))]
|
||||||
private UserLookupCache lookupCache = new TestUserLookupCache();
|
private UserLookupCache lookupCache = new TestUserLookupCache();
|
||||||
@ -59,14 +59,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
|
|
||||||
AddStep("add streaming client", () =>
|
AddStep("add streaming client", () =>
|
||||||
{
|
{
|
||||||
Remove(streamingClient);
|
Remove(spectatorClient);
|
||||||
Add(streamingClient);
|
Add(spectatorClient);
|
||||||
});
|
});
|
||||||
|
|
||||||
AddStep("finish previous gameplay", () =>
|
AddStep("finish previous gameplay", () =>
|
||||||
{
|
{
|
||||||
foreach (var id in playingUserIds)
|
foreach (var id in playingUserIds)
|
||||||
streamingClient.EndPlay(id, importedBeatmapId);
|
spectatorClient.EndPlay(id);
|
||||||
playingUserIds.Clear();
|
playingUserIds.Clear();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -87,11 +87,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
loadSpectateScreen(false);
|
loadSpectateScreen(false);
|
||||||
|
|
||||||
AddWaitStep("wait a bit", 10);
|
AddWaitStep("wait a bit", 10);
|
||||||
AddStep("load player first_player_id", () => streamingClient.StartPlay(PLAYER_1_ID, importedBeatmapId));
|
AddStep("load player first_player_id", () => spectatorClient.StartPlay(PLAYER_1_ID, importedBeatmapId));
|
||||||
AddUntilStep("one player added", () => spectatorScreen.ChildrenOfType<Player>().Count() == 1);
|
AddUntilStep("one player added", () => spectatorScreen.ChildrenOfType<Player>().Count() == 1);
|
||||||
|
|
||||||
AddWaitStep("wait a bit", 10);
|
AddWaitStep("wait a bit", 10);
|
||||||
AddStep("load player second_player_id", () => streamingClient.StartPlay(PLAYER_2_ID, importedBeatmapId));
|
AddStep("load player second_player_id", () => spectatorClient.StartPlay(PLAYER_2_ID, importedBeatmapId));
|
||||||
AddUntilStep("two players added", () => spectatorScreen.ChildrenOfType<Player>().Count() == 2);
|
AddUntilStep("two players added", () => spectatorScreen.ChildrenOfType<Player>().Count() == 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -251,18 +251,18 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
foreach (int id in userIds)
|
foreach (int id in userIds)
|
||||||
{
|
{
|
||||||
Client.CurrentMatchPlayingUserIds.Add(id);
|
Client.CurrentMatchPlayingUserIds.Add(id);
|
||||||
streamingClient.StartPlay(id, beatmapId ?? importedBeatmapId);
|
spectatorClient.StartPlay(id, beatmapId ?? importedBeatmapId);
|
||||||
playingUserIds.Add(id);
|
playingUserIds.Add(id);
|
||||||
nextFrame[id] = 0;
|
nextFrame[id] = 0;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void finish(int userId, int? beatmapId = null)
|
private void finish(int userId)
|
||||||
{
|
{
|
||||||
AddStep("end play", () =>
|
AddStep("end play", () =>
|
||||||
{
|
{
|
||||||
streamingClient.EndPlay(userId, beatmapId ?? importedBeatmapId);
|
spectatorClient.EndPlay(userId);
|
||||||
playingUserIds.Remove(userId);
|
playingUserIds.Remove(userId);
|
||||||
nextFrame.Remove(userId);
|
nextFrame.Remove(userId);
|
||||||
});
|
});
|
||||||
@ -276,7 +276,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
{
|
{
|
||||||
foreach (int id in userIds)
|
foreach (int id in userIds)
|
||||||
{
|
{
|
||||||
streamingClient.SendFrames(id, nextFrame[id], count);
|
spectatorClient.SendFrames(id, nextFrame[id], count);
|
||||||
nextFrame[id] += count;
|
nextFrame[id] += count;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -195,7 +195,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
|
|
||||||
private class TestMultiplayer : Screens.OnlinePlay.Multiplayer.Multiplayer
|
private class TestMultiplayer : Screens.OnlinePlay.Multiplayer.Multiplayer
|
||||||
{
|
{
|
||||||
[Cached(typeof(StatefulMultiplayerClient))]
|
[Cached(typeof(MultiplayerClient))]
|
||||||
public readonly TestMultiplayerClient Client;
|
public readonly TestMultiplayerClient Client;
|
||||||
|
|
||||||
public TestMultiplayer()
|
public TestMultiplayer()
|
||||||
|
@ -28,8 +28,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
{
|
{
|
||||||
private const int users = 16;
|
private const int users = 16;
|
||||||
|
|
||||||
[Cached(typeof(SpectatorStreamingClient))]
|
[Cached(typeof(SpectatorClient))]
|
||||||
private TestMultiplayerStreaming streamingClient = new TestMultiplayerStreaming();
|
private TestMultiplayerSpectatorClient spectatorClient = new TestMultiplayerSpectatorClient();
|
||||||
|
|
||||||
[Cached(typeof(UserLookupCache))]
|
[Cached(typeof(UserLookupCache))]
|
||||||
private UserLookupCache lookupCache = new TestSceneCurrentlyPlayingDisplay.TestUserLookupCache();
|
private UserLookupCache lookupCache = new TestSceneCurrentlyPlayingDisplay.TestUserLookupCache();
|
||||||
@ -44,7 +44,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
{
|
{
|
||||||
base.Content.Children = new Drawable[]
|
base.Content.Children = new Drawable[]
|
||||||
{
|
{
|
||||||
streamingClient,
|
spectatorClient,
|
||||||
lookupCache,
|
lookupCache,
|
||||||
Content
|
Content
|
||||||
};
|
};
|
||||||
@ -71,10 +71,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
var playable = Beatmap.Value.GetPlayableBeatmap(Ruleset.Value);
|
var playable = Beatmap.Value.GetPlayableBeatmap(Ruleset.Value);
|
||||||
|
|
||||||
for (int i = 0; i < users; i++)
|
for (int i = 0; i < users; i++)
|
||||||
streamingClient.StartPlay(i, Beatmap.Value.BeatmapInfo.OnlineBeatmapID ?? 0);
|
spectatorClient.StartPlay(i, Beatmap.Value.BeatmapInfo.OnlineBeatmapID ?? 0);
|
||||||
|
|
||||||
Client.CurrentMatchPlayingUserIds.Clear();
|
Client.CurrentMatchPlayingUserIds.Clear();
|
||||||
Client.CurrentMatchPlayingUserIds.AddRange(streamingClient.PlayingUsers);
|
Client.CurrentMatchPlayingUserIds.AddRange(spectatorClient.PlayingUsers);
|
||||||
|
|
||||||
Children = new Drawable[]
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
@ -83,7 +83,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
|
|
||||||
scoreProcessor.ApplyBeatmap(playable);
|
scoreProcessor.ApplyBeatmap(playable);
|
||||||
|
|
||||||
LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(scoreProcessor, streamingClient.PlayingUsers.ToArray())
|
LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(scoreProcessor, spectatorClient.PlayingUsers.ToArray())
|
||||||
{
|
{
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
@ -96,7 +96,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestScoreUpdates()
|
public void TestScoreUpdates()
|
||||||
{
|
{
|
||||||
AddRepeatStep("update state", () => streamingClient.RandomlyUpdateState(), 100);
|
AddRepeatStep("update state", () => spectatorClient.RandomlyUpdateState(), 100);
|
||||||
AddToggleStep("switch compact mode", expanded => leaderboard.Expanded.Value = expanded);
|
AddToggleStep("switch compact mode", expanded => leaderboard.Expanded.Value = expanded);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -109,12 +109,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestChangeScoringMode()
|
public void TestChangeScoringMode()
|
||||||
{
|
{
|
||||||
AddRepeatStep("update state", () => streamingClient.RandomlyUpdateState(), 5);
|
AddRepeatStep("update state", () => spectatorClient.RandomlyUpdateState(), 5);
|
||||||
AddStep("change to classic", () => config.SetValue(OsuSetting.ScoreDisplayMode, ScoringMode.Classic));
|
AddStep("change to classic", () => config.SetValue(OsuSetting.ScoreDisplayMode, ScoringMode.Classic));
|
||||||
AddStep("change to standardised", () => config.SetValue(OsuSetting.ScoreDisplayMode, ScoringMode.Standardised));
|
AddStep("change to standardised", () => config.SetValue(OsuSetting.ScoreDisplayMode, ScoringMode.Standardised));
|
||||||
}
|
}
|
||||||
|
|
||||||
public class TestMultiplayerStreaming : TestSpectatorStreamingClient
|
public class TestMultiplayerSpectatorClient : TestSpectatorClient
|
||||||
{
|
{
|
||||||
private readonly Dictionary<int, FrameHeader> lastHeaders = new Dictionary<int, FrameHeader>();
|
private readonly Dictionary<int, FrameHeader> lastHeaders = new Dictionary<int, FrameHeader>();
|
||||||
|
|
||||||
|
@ -1,12 +1,16 @@
|
|||||||
// 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.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Testing;
|
||||||
using osu.Game.Overlays;
|
using osu.Game.Overlays;
|
||||||
|
using osu.Game.Overlays.AccountCreation;
|
||||||
|
using osu.Game.Overlays.Settings;
|
||||||
using osu.Game.Users;
|
using osu.Game.Users;
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.Online
|
namespace osu.Game.Tests.Visual.Online
|
||||||
@ -36,8 +40,6 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load()
|
private void load()
|
||||||
{
|
{
|
||||||
API.Logout();
|
|
||||||
|
|
||||||
localUser = API.LocalUser.GetBoundCopy();
|
localUser = API.LocalUser.GetBoundCopy();
|
||||||
localUser.BindValueChanged(user => { userPanelArea.Child = new UserGridPanel(user.NewValue) { Width = 200 }; }, true);
|
localUser.BindValueChanged(user => { userPanelArea.Child = new UserGridPanel(user.NewValue) { Width = 200 }; }, true);
|
||||||
}
|
}
|
||||||
@ -46,11 +48,14 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
public void TestOverlayVisibility()
|
public void TestOverlayVisibility()
|
||||||
{
|
{
|
||||||
AddStep("start hidden", () => accountCreation.Hide());
|
AddStep("start hidden", () => accountCreation.Hide());
|
||||||
AddStep("log out", API.Logout);
|
AddStep("log out", () => API.Logout());
|
||||||
|
|
||||||
AddStep("show manually", () => accountCreation.Show());
|
AddStep("show manually", () => accountCreation.Show());
|
||||||
AddUntilStep("overlay is visible", () => accountCreation.State.Value == Visibility.Visible);
|
AddUntilStep("overlay is visible", () => accountCreation.State.Value == Visibility.Visible);
|
||||||
|
|
||||||
|
AddStep("click button", () => accountCreation.ChildrenOfType<SettingsButton>().Single().Click());
|
||||||
|
AddUntilStep("warning screen is present", () => accountCreation.ChildrenOfType<ScreenWarning>().SingleOrDefault()?.IsPresent == true);
|
||||||
|
|
||||||
AddStep("log back in", () => API.Login("dummy", "password"));
|
AddStep("log back in", () => API.Login("dummy", "password"));
|
||||||
AddUntilStep("overlay is hidden", () => accountCreation.State.Value == Visibility.Hidden);
|
AddUntilStep("overlay is hidden", () => accountCreation.State.Value == Visibility.Hidden);
|
||||||
}
|
}
|
||||||
|
@ -19,8 +19,10 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
{
|
{
|
||||||
public class TestSceneCurrentlyPlayingDisplay : OsuTestScene
|
public class TestSceneCurrentlyPlayingDisplay : OsuTestScene
|
||||||
{
|
{
|
||||||
[Cached(typeof(SpectatorStreamingClient))]
|
private readonly User streamingUser = new User { Id = 2, Username = "Test user" };
|
||||||
private TestSpectatorStreamingClient testSpectatorStreamingClient = new TestSpectatorStreamingClient();
|
|
||||||
|
[Cached(typeof(SpectatorClient))]
|
||||||
|
private TestSpectatorClient testSpectatorClient = new TestSpectatorClient();
|
||||||
|
|
||||||
private CurrentlyPlayingDisplay currentlyPlaying;
|
private CurrentlyPlayingDisplay currentlyPlaying;
|
||||||
|
|
||||||
@ -34,7 +36,7 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
{
|
{
|
||||||
AddStep("add streaming client", () =>
|
AddStep("add streaming client", () =>
|
||||||
{
|
{
|
||||||
nestedContainer?.Remove(testSpectatorStreamingClient);
|
nestedContainer?.Remove(testSpectatorClient);
|
||||||
Remove(lookupCache);
|
Remove(lookupCache);
|
||||||
|
|
||||||
Children = new Drawable[]
|
Children = new Drawable[]
|
||||||
@ -45,7 +47,7 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Children = new Drawable[]
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
testSpectatorStreamingClient,
|
testSpectatorClient,
|
||||||
currentlyPlaying = new CurrentlyPlayingDisplay
|
currentlyPlaying = new CurrentlyPlayingDisplay
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
@ -55,15 +57,15 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
AddStep("Reset players", () => testSpectatorStreamingClient.PlayingUsers.Clear());
|
AddStep("Reset players", () => testSpectatorClient.EndPlay(streamingUser.Id));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestBasicDisplay()
|
public void TestBasicDisplay()
|
||||||
{
|
{
|
||||||
AddStep("Add playing user", () => testSpectatorStreamingClient.PlayingUsers.Add(2));
|
AddStep("Add playing user", () => testSpectatorClient.StartPlay(streamingUser.Id, 0));
|
||||||
AddUntilStep("Panel loaded", () => currentlyPlaying.ChildrenOfType<UserGridPanel>()?.FirstOrDefault()?.User.Id == 2);
|
AddUntilStep("Panel loaded", () => currentlyPlaying.ChildrenOfType<UserGridPanel>()?.FirstOrDefault()?.User.Id == 2);
|
||||||
AddStep("Remove playing user", () => testSpectatorStreamingClient.PlayingUsers.Remove(2));
|
AddStep("Remove playing user", () => testSpectatorClient.EndPlay(streamingUser.Id));
|
||||||
AddUntilStep("Panel no longer present", () => !currentlyPlaying.ChildrenOfType<UserGridPanel>().Any());
|
AddUntilStep("Panel no longer present", () => !currentlyPlaying.ChildrenOfType<UserGridPanel>().Any());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,86 @@
|
|||||||
|
// 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.Graphics;
|
||||||
|
using osu.Game.Overlays;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Visual.UserInterface
|
||||||
|
{
|
||||||
|
public class TestSceneBreadcrumbControlHeader : OsuTestScene
|
||||||
|
{
|
||||||
|
private static readonly string[] items = { "first", "second", "third", "fourth", "fifth" };
|
||||||
|
|
||||||
|
[Cached]
|
||||||
|
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Red);
|
||||||
|
|
||||||
|
private TestHeader header;
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void SetUp() => Schedule(() =>
|
||||||
|
{
|
||||||
|
Child = header = new TestHeader
|
||||||
|
{
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestAddAndRemoveItem()
|
||||||
|
{
|
||||||
|
foreach (var item in items.Skip(1))
|
||||||
|
AddStep($"Add {item} item", () => header.AddItem(item));
|
||||||
|
|
||||||
|
foreach (var item in items.Reverse().SkipLast(3))
|
||||||
|
AddStep($"Remove {item} item", () => header.RemoveItem(item));
|
||||||
|
|
||||||
|
AddStep("Clear items", () => header.ClearItems());
|
||||||
|
|
||||||
|
foreach (var item in items)
|
||||||
|
AddStep($"Add {item} item", () => header.AddItem(item));
|
||||||
|
|
||||||
|
foreach (var item in items)
|
||||||
|
AddStep($"Remove {item} item", () => header.RemoveItem(item));
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestHeader : BreadcrumbControlOverlayHeader
|
||||||
|
{
|
||||||
|
public TestHeader()
|
||||||
|
{
|
||||||
|
TabControl.AddItem(items[0]);
|
||||||
|
Current.Value = items[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddItem(string value)
|
||||||
|
{
|
||||||
|
TabControl.AddItem(value);
|
||||||
|
Current.Value = TabControl.Items.LastOrDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveItem(string value)
|
||||||
|
{
|
||||||
|
TabControl.RemoveItem(value);
|
||||||
|
Current.Value = TabControl.Items.LastOrDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ClearItems()
|
||||||
|
{
|
||||||
|
TabControl.Clear();
|
||||||
|
Current.Value = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override OverlayTitle CreateTitle() => new TestTitle();
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestTitle : OverlayTitle
|
||||||
|
{
|
||||||
|
public TestTitle()
|
||||||
|
{
|
||||||
|
Title = "Test Title";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -14,7 +14,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
public class TestSceneStatefulMenuItem : OsuManualInputManagerTestScene
|
public class TestSceneStatefulMenuItem : OsuManualInputManagerTestScene
|
||||||
{
|
{
|
||||||
[Test]
|
[Test]
|
||||||
public void TestTernaryMenuItem()
|
public void TestTernaryRadioMenuItem()
|
||||||
{
|
{
|
||||||
OsuMenu menu = null;
|
OsuMenu menu = null;
|
||||||
|
|
||||||
@ -30,9 +30,57 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
Items = new[]
|
Items = new[]
|
||||||
{
|
{
|
||||||
new TernaryStateMenuItem("First"),
|
new TernaryStateRadioMenuItem("First"),
|
||||||
new TernaryStateMenuItem("Second") { State = { BindTarget = state } },
|
new TernaryStateRadioMenuItem("Second") { State = { BindTarget = state } },
|
||||||
new TernaryStateMenuItem("Third") { State = { Value = TernaryState.True } },
|
new TernaryStateRadioMenuItem("Third") { State = { Value = TernaryState.True } },
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
checkState(TernaryState.Indeterminate);
|
||||||
|
|
||||||
|
click();
|
||||||
|
checkState(TernaryState.True);
|
||||||
|
|
||||||
|
click();
|
||||||
|
checkState(TernaryState.True);
|
||||||
|
|
||||||
|
click();
|
||||||
|
checkState(TernaryState.True);
|
||||||
|
|
||||||
|
AddStep("change state via bindable", () => state.Value = TernaryState.True);
|
||||||
|
|
||||||
|
void click() =>
|
||||||
|
AddStep("click", () =>
|
||||||
|
{
|
||||||
|
InputManager.MoveMouseTo(menu.ScreenSpaceDrawQuad.Centre);
|
||||||
|
InputManager.Click(MouseButton.Left);
|
||||||
|
});
|
||||||
|
|
||||||
|
void checkState(TernaryState expected)
|
||||||
|
=> AddAssert($"state is {expected}", () => state.Value == expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestTernaryToggleMenuItem()
|
||||||
|
{
|
||||||
|
OsuMenu menu = null;
|
||||||
|
|
||||||
|
Bindable<TernaryState> state = new Bindable<TernaryState>(TernaryState.Indeterminate);
|
||||||
|
|
||||||
|
AddStep("create menu", () =>
|
||||||
|
{
|
||||||
|
state.Value = TernaryState.Indeterminate;
|
||||||
|
|
||||||
|
Child = menu = new OsuMenu(Direction.Vertical, true)
|
||||||
|
{
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
Items = new[]
|
||||||
|
{
|
||||||
|
new TernaryStateToggleMenuItem("First"),
|
||||||
|
new TernaryStateToggleMenuItem("Second") { State = { BindTarget = state } },
|
||||||
|
new TernaryStateToggleMenuItem("Third") { State = { Value = TernaryState.True } },
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@ -104,7 +104,6 @@ namespace osu.Game.Configuration
|
|||||||
SetDefault(OsuSetting.KeyOverlay, false);
|
SetDefault(OsuSetting.KeyOverlay, false);
|
||||||
SetDefault(OsuSetting.PositionalHitSounds, true);
|
SetDefault(OsuSetting.PositionalHitSounds, true);
|
||||||
SetDefault(OsuSetting.AlwaysPlayFirstComboBreak, true);
|
SetDefault(OsuSetting.AlwaysPlayFirstComboBreak, true);
|
||||||
SetDefault(OsuSetting.ScoreMeter, ScoreMeterType.HitErrorBoth);
|
|
||||||
|
|
||||||
SetDefault(OsuSetting.FloatingComments, false);
|
SetDefault(OsuSetting.FloatingComments, false);
|
||||||
|
|
||||||
@ -213,7 +212,6 @@ namespace osu.Game.Configuration
|
|||||||
KeyOverlay,
|
KeyOverlay,
|
||||||
PositionalHitSounds,
|
PositionalHitSounds,
|
||||||
AlwaysPlayFirstComboBreak,
|
AlwaysPlayFirstComboBreak,
|
||||||
ScoreMeter,
|
|
||||||
FloatingComments,
|
FloatingComments,
|
||||||
HUDVisibilityMode,
|
HUDVisibilityMode,
|
||||||
ShowProgressGraph,
|
ShowProgressGraph,
|
||||||
|
@ -1,37 +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.ComponentModel;
|
|
||||||
|
|
||||||
namespace osu.Game.Configuration
|
|
||||||
{
|
|
||||||
public enum ScoreMeterType
|
|
||||||
{
|
|
||||||
[Description("None")]
|
|
||||||
None,
|
|
||||||
|
|
||||||
[Description("Hit Error (left)")]
|
|
||||||
HitErrorLeft,
|
|
||||||
|
|
||||||
[Description("Hit Error (right)")]
|
|
||||||
HitErrorRight,
|
|
||||||
|
|
||||||
[Description("Hit Error (left+right)")]
|
|
||||||
HitErrorBoth,
|
|
||||||
|
|
||||||
[Description("Hit Error (bottom)")]
|
|
||||||
HitErrorBottom,
|
|
||||||
|
|
||||||
[Description("Colour (left)")]
|
|
||||||
ColourLeft,
|
|
||||||
|
|
||||||
[Description("Colour (right)")]
|
|
||||||
ColourRight,
|
|
||||||
|
|
||||||
[Description("Colour (left+right)")]
|
|
||||||
ColourBoth,
|
|
||||||
|
|
||||||
[Description("Colour (bottom)")]
|
|
||||||
ColourBottom,
|
|
||||||
}
|
|
||||||
}
|
|
@ -9,28 +9,17 @@ namespace osu.Game.Graphics.UserInterface
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// An <see cref="OsuMenuItem"/> with three possible states.
|
/// An <see cref="OsuMenuItem"/> with three possible states.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class TernaryStateMenuItem : StatefulMenuItem<TernaryState>
|
public abstract class TernaryStateMenuItem : StatefulMenuItem<TernaryState>
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a new <see cref="TernaryStateMenuItem"/>.
|
/// Creates a new <see cref="TernaryStateMenuItem"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="text">The text to display.</param>
|
/// <param name="text">The text to display.</param>
|
||||||
|
/// <param name="nextStateFunction">A function to inform what the next state should be when this item is clicked.</param>
|
||||||
/// <param name="type">The type of action which this <see cref="TernaryStateMenuItem"/> performs.</param>
|
/// <param name="type">The type of action which this <see cref="TernaryStateMenuItem"/> performs.</param>
|
||||||
/// <param name="action">A delegate to be invoked when this <see cref="TernaryStateMenuItem"/> is pressed.</param>
|
/// <param name="action">A delegate to be invoked when this <see cref="TernaryStateMenuItem"/> is pressed.</param>
|
||||||
public TernaryStateMenuItem(string text, MenuItemType type = MenuItemType.Standard, Action<TernaryState> action = null)
|
protected TernaryStateMenuItem(string text, Func<TernaryState, TernaryState> nextStateFunction, MenuItemType type = MenuItemType.Standard, Action<TernaryState> action = null)
|
||||||
: this(text, getNextState, type, action)
|
: base(text, nextStateFunction, type, action)
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates a new <see cref="TernaryStateMenuItem"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="text">The text to display.</param>
|
|
||||||
/// <param name="changeStateFunc">A function that mutates a state to another state after this <see cref="TernaryStateMenuItem"/> is pressed.</param>
|
|
||||||
/// <param name="type">The type of action which this <see cref="TernaryStateMenuItem"/> performs.</param>
|
|
||||||
/// <param name="action">A delegate to be invoked when this <see cref="TernaryStateMenuItem"/> is pressed.</param>
|
|
||||||
protected TernaryStateMenuItem(string text, Func<TernaryState, TernaryState> changeStateFunc, MenuItemType type, Action<TernaryState> action)
|
|
||||||
: base(text, changeStateFunc, type, action)
|
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,23 +36,5 @@ namespace osu.Game.Graphics.UserInterface
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static TernaryState getNextState(TernaryState state)
|
|
||||||
{
|
|
||||||
switch (state)
|
|
||||||
{
|
|
||||||
case TernaryState.False:
|
|
||||||
return TernaryState.True;
|
|
||||||
|
|
||||||
case TernaryState.Indeterminate:
|
|
||||||
return TernaryState.True;
|
|
||||||
|
|
||||||
case TernaryState.True:
|
|
||||||
return TernaryState.False;
|
|
||||||
|
|
||||||
default:
|
|
||||||
throw new ArgumentOutOfRangeException(nameof(state), state, null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
26
osu.Game/Graphics/UserInterface/TernaryStateRadioMenuItem.cs
Normal file
26
osu.Game/Graphics/UserInterface/TernaryStateRadioMenuItem.cs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace osu.Game.Graphics.UserInterface
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A ternary state menu item which will always set the item to <c>true</c> on click, even if already <c>true</c>.
|
||||||
|
/// </summary>
|
||||||
|
public class TernaryStateRadioMenuItem : TernaryStateMenuItem
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new <see cref="TernaryStateMenuItem"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="text">The text to display.</param>
|
||||||
|
/// <param name="type">The type of action which this <see cref="TernaryStateMenuItem"/> performs.</param>
|
||||||
|
/// <param name="action">A delegate to be invoked when this <see cref="TernaryStateMenuItem"/> is pressed.</param>
|
||||||
|
public TernaryStateRadioMenuItem(string text, MenuItemType type = MenuItemType.Standard, Action<TernaryState> action = null)
|
||||||
|
: base(text, getNextState, type, action)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TernaryState getNextState(TernaryState state) => TernaryState.True;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,42 @@
|
|||||||
|
// 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;
|
||||||
|
|
||||||
|
namespace osu.Game.Graphics.UserInterface
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A ternary state menu item which toggles the state of this item <c>false</c> if clicked when <c>true</c>.
|
||||||
|
/// </summary>
|
||||||
|
public class TernaryStateToggleMenuItem : TernaryStateMenuItem
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new <see cref="TernaryStateToggleMenuItem"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="text">The text to display.</param>
|
||||||
|
/// <param name="type">The type of action which this <see cref="TernaryStateMenuItem"/> performs.</param>
|
||||||
|
/// <param name="action">A delegate to be invoked when this <see cref="TernaryStateMenuItem"/> is pressed.</param>
|
||||||
|
public TernaryStateToggleMenuItem(string text, MenuItemType type = MenuItemType.Standard, Action<TernaryState> action = null)
|
||||||
|
: base(text, getNextState, type, action)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TernaryState getNextState(TernaryState state)
|
||||||
|
{
|
||||||
|
switch (state)
|
||||||
|
{
|
||||||
|
case TernaryState.False:
|
||||||
|
return TernaryState.True;
|
||||||
|
|
||||||
|
case TernaryState.Indeterminate:
|
||||||
|
return TernaryState.True;
|
||||||
|
|
||||||
|
case TernaryState.True:
|
||||||
|
return TernaryState.False;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(state), state, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -8,10 +8,12 @@ namespace osu.Game.Online.API.Requests
|
|||||||
{
|
{
|
||||||
public class GetNewsRequest : APIRequest<GetNewsResponse>
|
public class GetNewsRequest : APIRequest<GetNewsResponse>
|
||||||
{
|
{
|
||||||
|
private readonly int? year;
|
||||||
private readonly Cursor cursor;
|
private readonly Cursor cursor;
|
||||||
|
|
||||||
public GetNewsRequest(Cursor cursor = null)
|
public GetNewsRequest(int? year = null, Cursor cursor = null)
|
||||||
{
|
{
|
||||||
|
this.year = year;
|
||||||
this.cursor = cursor;
|
this.cursor = cursor;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -19,6 +21,10 @@ namespace osu.Game.Online.API.Requests
|
|||||||
{
|
{
|
||||||
var req = base.CreateWebRequest();
|
var req = base.CreateWebRequest();
|
||||||
req.AddCursor(cursor);
|
req.AddCursor(cursor);
|
||||||
|
|
||||||
|
if (year.HasValue)
|
||||||
|
req.AddParameter("year", year.Value.ToString());
|
||||||
|
|
||||||
return req;
|
return req;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,132 +3,621 @@
|
|||||||
|
|
||||||
#nullable enable
|
#nullable enable
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.SignalR.Client;
|
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Extensions.ObjectExtensions;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Logging;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Database;
|
||||||
using osu.Game.Online.API;
|
using osu.Game.Online.API;
|
||||||
using osu.Game.Online.API.Requests;
|
|
||||||
using osu.Game.Online.Rooms;
|
using osu.Game.Online.Rooms;
|
||||||
|
using osu.Game.Online.Rooms.RoomStatuses;
|
||||||
|
using osu.Game.Rulesets;
|
||||||
|
using osu.Game.Rulesets.Mods;
|
||||||
|
using osu.Game.Users;
|
||||||
|
using osu.Game.Utils;
|
||||||
|
|
||||||
namespace osu.Game.Online.Multiplayer
|
namespace osu.Game.Online.Multiplayer
|
||||||
{
|
{
|
||||||
public class MultiplayerClient : StatefulMultiplayerClient
|
public abstract class MultiplayerClient : Component, IMultiplayerClient, IMultiplayerRoomServer
|
||||||
{
|
{
|
||||||
private readonly string endpoint;
|
/// <summary>
|
||||||
|
/// Invoked when any change occurs to the multiplayer room.
|
||||||
|
/// </summary>
|
||||||
|
public event Action? RoomUpdated;
|
||||||
|
|
||||||
private IHubClientConnector? connector;
|
/// <summary>
|
||||||
|
/// Invoked when the multiplayer server requests the current beatmap to be loaded into play.
|
||||||
|
/// </summary>
|
||||||
|
public event Action? LoadRequested;
|
||||||
|
|
||||||
public override IBindable<bool> IsConnected { get; } = new BindableBool();
|
/// <summary>
|
||||||
|
/// Invoked when the multiplayer server requests gameplay to be started.
|
||||||
|
/// </summary>
|
||||||
|
public event Action? MatchStarted;
|
||||||
|
|
||||||
private HubConnection? connection => connector?.CurrentConnection;
|
/// <summary>
|
||||||
|
/// Invoked when the multiplayer server has finished collating results.
|
||||||
|
/// </summary>
|
||||||
|
public event Action? ResultsReady;
|
||||||
|
|
||||||
public MultiplayerClient(EndpointConfiguration endpoints)
|
/// <summary>
|
||||||
|
/// Whether the <see cref="MultiplayerClient"/> is currently connected.
|
||||||
|
/// This is NOT thread safe and usage should be scheduled.
|
||||||
|
/// </summary>
|
||||||
|
public abstract IBindable<bool> IsConnected { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The joined <see cref="MultiplayerRoom"/>.
|
||||||
|
/// </summary>
|
||||||
|
public MultiplayerRoom? Room { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The users in the joined <see cref="Room"/> which are participating in the current gameplay loop.
|
||||||
|
/// </summary>
|
||||||
|
public readonly BindableList<int> CurrentMatchPlayingUserIds = new BindableList<int>();
|
||||||
|
|
||||||
|
public readonly Bindable<PlaylistItem?> CurrentMatchPlayingItem = new Bindable<PlaylistItem?>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The <see cref="MultiplayerRoomUser"/> corresponding to the local player, if available.
|
||||||
|
/// </summary>
|
||||||
|
public MultiplayerRoomUser? LocalUser => Room?.Users.SingleOrDefault(u => u.User?.Id == API.LocalUser.Value.Id);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the <see cref="LocalUser"/> is the host in <see cref="Room"/>.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsHost
|
||||||
{
|
{
|
||||||
endpoint = endpoints.MultiplayerEndpointUrl;
|
get
|
||||||
}
|
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
|
||||||
private void load(IAPIProvider api)
|
|
||||||
{
|
|
||||||
connector = api.GetHubConnector(nameof(MultiplayerClient), endpoint);
|
|
||||||
|
|
||||||
if (connector != null)
|
|
||||||
{
|
{
|
||||||
connector.ConfigureConnection = connection =>
|
var localUser = LocalUser;
|
||||||
{
|
return localUser != null && Room?.Host != null && localUser.Equals(Room.Host);
|
||||||
// this is kind of SILLY
|
|
||||||
// https://github.com/dotnet/aspnetcore/issues/15198
|
|
||||||
connection.On<MultiplayerRoomState>(nameof(IMultiplayerClient.RoomStateChanged), ((IMultiplayerClient)this).RoomStateChanged);
|
|
||||||
connection.On<MultiplayerRoomUser>(nameof(IMultiplayerClient.UserJoined), ((IMultiplayerClient)this).UserJoined);
|
|
||||||
connection.On<MultiplayerRoomUser>(nameof(IMultiplayerClient.UserLeft), ((IMultiplayerClient)this).UserLeft);
|
|
||||||
connection.On<int>(nameof(IMultiplayerClient.HostChanged), ((IMultiplayerClient)this).HostChanged);
|
|
||||||
connection.On<MultiplayerRoomSettings>(nameof(IMultiplayerClient.SettingsChanged), ((IMultiplayerClient)this).SettingsChanged);
|
|
||||||
connection.On<int, MultiplayerUserState>(nameof(IMultiplayerClient.UserStateChanged), ((IMultiplayerClient)this).UserStateChanged);
|
|
||||||
connection.On(nameof(IMultiplayerClient.LoadRequested), ((IMultiplayerClient)this).LoadRequested);
|
|
||||||
connection.On(nameof(IMultiplayerClient.MatchStarted), ((IMultiplayerClient)this).MatchStarted);
|
|
||||||
connection.On(nameof(IMultiplayerClient.ResultsReady), ((IMultiplayerClient)this).ResultsReady);
|
|
||||||
connection.On<int, IEnumerable<APIMod>>(nameof(IMultiplayerClient.UserModsChanged), ((IMultiplayerClient)this).UserModsChanged);
|
|
||||||
connection.On<int, BeatmapAvailability>(nameof(IMultiplayerClient.UserBeatmapAvailabilityChanged), ((IMultiplayerClient)this).UserBeatmapAvailabilityChanged);
|
|
||||||
};
|
|
||||||
|
|
||||||
IsConnected.BindTo(connector.IsConnected);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Task<MultiplayerRoom> JoinRoom(long roomId)
|
[Resolved]
|
||||||
{
|
protected IAPIProvider API { get; private set; } = null!;
|
||||||
if (!IsConnected.Value)
|
|
||||||
return Task.FromCanceled<MultiplayerRoom>(new CancellationToken(true));
|
|
||||||
|
|
||||||
return connection.InvokeAsync<MultiplayerRoom>(nameof(IMultiplayerServer.JoinRoom), roomId);
|
[Resolved]
|
||||||
|
protected RulesetStore Rulesets { get; private set; } = null!;
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private UserLookupCache userLookupCache { get; set; } = null!;
|
||||||
|
|
||||||
|
private Room? apiRoom;
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load()
|
||||||
|
{
|
||||||
|
IsConnected.BindValueChanged(connected =>
|
||||||
|
{
|
||||||
|
// clean up local room state on server disconnect.
|
||||||
|
if (!connected.NewValue && Room != null)
|
||||||
|
{
|
||||||
|
Logger.Log("Connection to multiplayer server was lost.", LoggingTarget.Runtime, LogLevel.Important);
|
||||||
|
LeaveRoom();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Task LeaveRoomInternal()
|
private readonly TaskChain joinOrLeaveTaskChain = new TaskChain();
|
||||||
{
|
private CancellationTokenSource? joinCancellationSource;
|
||||||
if (!IsConnected.Value)
|
|
||||||
return Task.FromCanceled(new CancellationToken(true));
|
|
||||||
|
|
||||||
return connection.InvokeAsync(nameof(IMultiplayerServer.LeaveRoom));
|
/// <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)
|
||||||
|
{
|
||||||
|
var cancellationSource = joinCancellationSource = new CancellationTokenSource();
|
||||||
|
|
||||||
|
await joinOrLeaveTaskChain.Add(async () =>
|
||||||
|
{
|
||||||
|
if (Room != null)
|
||||||
|
throw new InvalidOperationException("Cannot join a multiplayer room while already in one.");
|
||||||
|
|
||||||
|
Debug.Assert(room.RoomID.Value != null);
|
||||||
|
|
||||||
|
// Join the server-side room.
|
||||||
|
var joinedRoom = await JoinRoom(room.RoomID.Value.Value).ConfigureAwait(false);
|
||||||
|
Debug.Assert(joinedRoom != null);
|
||||||
|
|
||||||
|
// Populate users.
|
||||||
|
Debug.Assert(joinedRoom.Users != null);
|
||||||
|
await Task.WhenAll(joinedRoom.Users.Select(PopulateUser)).ConfigureAwait(false);
|
||||||
|
|
||||||
|
// Update the stored room (must be done on update thread for thread-safety).
|
||||||
|
await scheduleAsync(() =>
|
||||||
|
{
|
||||||
|
Room = joinedRoom;
|
||||||
|
apiRoom = room;
|
||||||
|
foreach (var user in joinedRoom.Users)
|
||||||
|
updateUserPlayingState(user.UserID, user.State);
|
||||||
|
}, cancellationSource.Token).ConfigureAwait(false);
|
||||||
|
|
||||||
|
// Update room settings.
|
||||||
|
await updateLocalRoomSettings(joinedRoom.Settings, cancellationSource.Token).ConfigureAwait(false);
|
||||||
|
}, cancellationSource.Token).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Task TransferHost(int userId)
|
/// <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 Task LeaveRoom()
|
||||||
{
|
{
|
||||||
if (!IsConnected.Value)
|
// The join may have not completed yet, so certain tasks that either update the room or reference the room should be cancelled.
|
||||||
|
// This includes the setting of Room itself along with the initial update of the room settings on join.
|
||||||
|
joinCancellationSource?.Cancel();
|
||||||
|
|
||||||
|
// Leaving rooms is expected to occur instantaneously whilst the operation is finalised in the background.
|
||||||
|
// However a few members need to be reset immediately to prevent other components from entering invalid states whilst the operation hasn't yet completed.
|
||||||
|
// For example, if a room was left and the user immediately pressed the "create room" button, then the user could be taken into the lobby if the value of Room is not reset in time.
|
||||||
|
var scheduledReset = scheduleAsync(() =>
|
||||||
|
{
|
||||||
|
apiRoom = null;
|
||||||
|
Room = null;
|
||||||
|
CurrentMatchPlayingUserIds.Clear();
|
||||||
|
|
||||||
|
RoomUpdated?.Invoke();
|
||||||
|
});
|
||||||
|
|
||||||
|
return joinOrLeaveTaskChain.Add(async () =>
|
||||||
|
{
|
||||||
|
await scheduledReset.ConfigureAwait(false);
|
||||||
|
await LeaveRoomInternal().ConfigureAwait(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract Task LeaveRoomInternal();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Change the current <see cref="MultiplayerRoom"/> settings.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// A room must be joined for this to have any effect.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="name">The new room name, if any.</param>
|
||||||
|
/// <param name="item">The new room playlist item, if any.</param>
|
||||||
|
public Task ChangeSettings(Optional<string> name = default, Optional<PlaylistItem> item = default)
|
||||||
|
{
|
||||||
|
if (Room == null)
|
||||||
|
throw new InvalidOperationException("Must be joined to a match to change settings.");
|
||||||
|
|
||||||
|
// A dummy playlist item filled with the current room settings (except mods).
|
||||||
|
var existingPlaylistItem = new PlaylistItem
|
||||||
|
{
|
||||||
|
Beatmap =
|
||||||
|
{
|
||||||
|
Value = new BeatmapInfo
|
||||||
|
{
|
||||||
|
OnlineBeatmapID = Room.Settings.BeatmapID,
|
||||||
|
MD5Hash = Room.Settings.BeatmapChecksum
|
||||||
|
}
|
||||||
|
},
|
||||||
|
RulesetID = Room.Settings.RulesetID
|
||||||
|
};
|
||||||
|
|
||||||
|
return ChangeSettings(new MultiplayerRoomSettings
|
||||||
|
{
|
||||||
|
Name = name.GetOr(Room.Settings.Name),
|
||||||
|
BeatmapID = item.GetOr(existingPlaylistItem).BeatmapID,
|
||||||
|
BeatmapChecksum = item.GetOr(existingPlaylistItem).Beatmap.Value.MD5Hash,
|
||||||
|
RulesetID = item.GetOr(existingPlaylistItem).RulesetID,
|
||||||
|
RequiredMods = item.HasValue ? item.Value.AsNonNull().RequiredMods.Select(m => new APIMod(m)).ToList() : Room.Settings.RequiredMods,
|
||||||
|
AllowedMods = item.HasValue ? item.Value.AsNonNull().AllowedMods.Select(m => new APIMod(m)).ToList() : Room.Settings.AllowedMods,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Toggles the <see cref="LocalUser"/>'s ready state.
|
||||||
|
/// </summary>
|
||||||
|
/// <exception cref="InvalidOperationException">If a toggle of ready state is not valid at this time.</exception>
|
||||||
|
public async Task ToggleReady()
|
||||||
|
{
|
||||||
|
var localUser = LocalUser;
|
||||||
|
|
||||||
|
if (localUser == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
switch (localUser.State)
|
||||||
|
{
|
||||||
|
case MultiplayerUserState.Idle:
|
||||||
|
await ChangeState(MultiplayerUserState.Ready).ConfigureAwait(false);
|
||||||
|
return;
|
||||||
|
|
||||||
|
case MultiplayerUserState.Ready:
|
||||||
|
await ChangeState(MultiplayerUserState.Idle).ConfigureAwait(false);
|
||||||
|
return;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new InvalidOperationException($"Cannot toggle ready when in {localUser.State}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Toggles the <see cref="LocalUser"/>'s spectating state.
|
||||||
|
/// </summary>
|
||||||
|
/// <exception cref="InvalidOperationException">If a toggle of the spectating state is not valid at this time.</exception>
|
||||||
|
public async Task ToggleSpectate()
|
||||||
|
{
|
||||||
|
var localUser = LocalUser;
|
||||||
|
|
||||||
|
if (localUser == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
switch (localUser.State)
|
||||||
|
{
|
||||||
|
case MultiplayerUserState.Idle:
|
||||||
|
case MultiplayerUserState.Ready:
|
||||||
|
await ChangeState(MultiplayerUserState.Spectating).ConfigureAwait(false);
|
||||||
|
return;
|
||||||
|
|
||||||
|
case MultiplayerUserState.Spectating:
|
||||||
|
await ChangeState(MultiplayerUserState.Idle).ConfigureAwait(false);
|
||||||
|
return;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new InvalidOperationException($"Cannot toggle spectate when in {localUser.State}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract Task TransferHost(int userId);
|
||||||
|
|
||||||
|
public abstract Task ChangeSettings(MultiplayerRoomSettings settings);
|
||||||
|
|
||||||
|
public abstract Task ChangeState(MultiplayerUserState newState);
|
||||||
|
|
||||||
|
public abstract Task ChangeBeatmapAvailability(BeatmapAvailability newBeatmapAvailability);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Change the local user's mods in the currently joined room.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="newMods">The proposed new mods, excluding any required by the room itself.</param>
|
||||||
|
public Task ChangeUserMods(IEnumerable<Mod> newMods) => ChangeUserMods(newMods.Select(m => new APIMod(m)).ToList());
|
||||||
|
|
||||||
|
public abstract Task ChangeUserMods(IEnumerable<APIMod> newMods);
|
||||||
|
|
||||||
|
public abstract Task StartMatch();
|
||||||
|
|
||||||
|
Task IMultiplayerClient.RoomStateChanged(MultiplayerRoomState state)
|
||||||
|
{
|
||||||
|
if (Room == null)
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
|
|
||||||
return connection.InvokeAsync(nameof(IMultiplayerServer.TransferHost), userId);
|
Scheduler.Add(() =>
|
||||||
|
{
|
||||||
|
if (Room == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Debug.Assert(apiRoom != null);
|
||||||
|
|
||||||
|
Room.State = state;
|
||||||
|
|
||||||
|
switch (state)
|
||||||
|
{
|
||||||
|
case MultiplayerRoomState.Open:
|
||||||
|
apiRoom.Status.Value = new RoomStatusOpen();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MultiplayerRoomState.Playing:
|
||||||
|
apiRoom.Status.Value = new RoomStatusPlaying();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MultiplayerRoomState.Closed:
|
||||||
|
apiRoom.Status.Value = new RoomStatusEnded();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
RoomUpdated?.Invoke();
|
||||||
|
}, false);
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Task ChangeSettings(MultiplayerRoomSettings settings)
|
async Task IMultiplayerClient.UserJoined(MultiplayerRoomUser user)
|
||||||
{
|
{
|
||||||
if (!IsConnected.Value)
|
if (Room == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
await PopulateUser(user).ConfigureAwait(false);
|
||||||
|
|
||||||
|
Scheduler.Add(() =>
|
||||||
|
{
|
||||||
|
if (Room == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// for sanity, ensure that there can be no duplicate users in the room user list.
|
||||||
|
if (Room.Users.Any(existing => existing.UserID == user.UserID))
|
||||||
|
return;
|
||||||
|
|
||||||
|
Room.Users.Add(user);
|
||||||
|
|
||||||
|
RoomUpdated?.Invoke();
|
||||||
|
}, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
Task IMultiplayerClient.UserLeft(MultiplayerRoomUser user)
|
||||||
|
{
|
||||||
|
if (Room == null)
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
|
|
||||||
return connection.InvokeAsync(nameof(IMultiplayerServer.ChangeSettings), settings);
|
Scheduler.Add(() =>
|
||||||
|
{
|
||||||
|
if (Room == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Room.Users.Remove(user);
|
||||||
|
CurrentMatchPlayingUserIds.Remove(user.UserID);
|
||||||
|
|
||||||
|
RoomUpdated?.Invoke();
|
||||||
|
}, false);
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Task ChangeState(MultiplayerUserState newState)
|
Task IMultiplayerClient.HostChanged(int userId)
|
||||||
{
|
{
|
||||||
if (!IsConnected.Value)
|
if (Room == null)
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
|
|
||||||
return connection.InvokeAsync(nameof(IMultiplayerServer.ChangeState), newState);
|
Scheduler.Add(() =>
|
||||||
|
{
|
||||||
|
if (Room == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Debug.Assert(apiRoom != null);
|
||||||
|
|
||||||
|
var user = Room.Users.FirstOrDefault(u => u.UserID == userId);
|
||||||
|
|
||||||
|
Room.Host = user;
|
||||||
|
apiRoom.Host.Value = user?.User;
|
||||||
|
|
||||||
|
RoomUpdated?.Invoke();
|
||||||
|
}, false);
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Task ChangeBeatmapAvailability(BeatmapAvailability newBeatmapAvailability)
|
Task IMultiplayerClient.SettingsChanged(MultiplayerRoomSettings newSettings)
|
||||||
{
|
{
|
||||||
if (!IsConnected.Value)
|
updateLocalRoomSettings(newSettings);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
Task IMultiplayerClient.UserStateChanged(int userId, MultiplayerUserState state)
|
||||||
|
{
|
||||||
|
if (Room == null)
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
|
|
||||||
return connection.InvokeAsync(nameof(IMultiplayerServer.ChangeBeatmapAvailability), newBeatmapAvailability);
|
Scheduler.Add(() =>
|
||||||
|
{
|
||||||
|
if (Room == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Room.Users.Single(u => u.UserID == userId).State = state;
|
||||||
|
|
||||||
|
updateUserPlayingState(userId, state);
|
||||||
|
|
||||||
|
RoomUpdated?.Invoke();
|
||||||
|
}, false);
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Task ChangeUserMods(IEnumerable<APIMod> newMods)
|
Task IMultiplayerClient.UserBeatmapAvailabilityChanged(int userId, BeatmapAvailability beatmapAvailability)
|
||||||
{
|
{
|
||||||
if (!IsConnected.Value)
|
if (Room == null)
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
|
|
||||||
return connection.InvokeAsync(nameof(IMultiplayerServer.ChangeUserMods), newMods);
|
Scheduler.Add(() =>
|
||||||
|
{
|
||||||
|
var user = Room?.Users.SingleOrDefault(u => u.UserID == userId);
|
||||||
|
|
||||||
|
// errors here are not critical - beatmap availability state is mostly for display.
|
||||||
|
if (user == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
user.BeatmapAvailability = beatmapAvailability;
|
||||||
|
|
||||||
|
RoomUpdated?.Invoke();
|
||||||
|
}, false);
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Task StartMatch()
|
public Task UserModsChanged(int userId, IEnumerable<APIMod> mods)
|
||||||
{
|
{
|
||||||
if (!IsConnected.Value)
|
if (Room == null)
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
|
|
||||||
return connection.InvokeAsync(nameof(IMultiplayerServer.StartMatch));
|
Scheduler.Add(() =>
|
||||||
|
{
|
||||||
|
var user = Room?.Users.SingleOrDefault(u => u.UserID == userId);
|
||||||
|
|
||||||
|
// errors here are not critical - user mods are mostly for display.
|
||||||
|
if (user == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
user.Mods = mods;
|
||||||
|
|
||||||
|
RoomUpdated?.Invoke();
|
||||||
|
}, false);
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Task<BeatmapSetInfo> GetOnlineBeatmapSet(int beatmapId, CancellationToken cancellationToken = default)
|
Task IMultiplayerClient.LoadRequested()
|
||||||
{
|
{
|
||||||
var tcs = new TaskCompletionSource<BeatmapSetInfo>();
|
if (Room == null)
|
||||||
var req = new GetBeatmapSetRequest(beatmapId, BeatmapSetLookupType.BeatmapId);
|
return Task.CompletedTask;
|
||||||
|
|
||||||
req.Success += res =>
|
Scheduler.Add(() =>
|
||||||
|
{
|
||||||
|
if (Room == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
LoadRequested?.Invoke();
|
||||||
|
}, false);
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
Task IMultiplayerClient.MatchStarted()
|
||||||
|
{
|
||||||
|
if (Room == null)
|
||||||
|
return Task.CompletedTask;
|
||||||
|
|
||||||
|
Scheduler.Add(() =>
|
||||||
|
{
|
||||||
|
if (Room == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
MatchStarted?.Invoke();
|
||||||
|
}, false);
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
Task IMultiplayerClient.ResultsReady()
|
||||||
|
{
|
||||||
|
if (Room == null)
|
||||||
|
return Task.CompletedTask;
|
||||||
|
|
||||||
|
Scheduler.Add(() =>
|
||||||
|
{
|
||||||
|
if (Room == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
ResultsReady?.Invoke();
|
||||||
|
}, false);
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Populates the <see cref="User"/> for a given <see cref="MultiplayerRoomUser"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="multiplayerUser">The <see cref="MultiplayerRoomUser"/> to populate.</param>
|
||||||
|
protected async Task PopulateUser(MultiplayerRoomUser multiplayerUser) => multiplayerUser.User ??= await userLookupCache.GetUserAsync(multiplayerUser.UserID).ConfigureAwait(false);
|
||||||
|
|
||||||
|
/// <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>
|
||||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to cancel the update.</param>
|
||||||
|
private Task updateLocalRoomSettings(MultiplayerRoomSettings settings, CancellationToken cancellationToken = default) => scheduleAsync(() =>
|
||||||
|
{
|
||||||
|
if (Room == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Debug.Assert(apiRoom != null);
|
||||||
|
|
||||||
|
// Update a few properties of the room instantaneously.
|
||||||
|
Room.Settings = settings;
|
||||||
|
apiRoom.Name.Value = Room.Settings.Name;
|
||||||
|
|
||||||
|
// The current item update is delayed until an online beatmap lookup (below) succeeds.
|
||||||
|
// In-order for the client to not display an outdated beatmap, the current item is forcefully cleared here.
|
||||||
|
CurrentMatchPlayingItem.Value = null;
|
||||||
|
|
||||||
|
RoomUpdated?.Invoke();
|
||||||
|
|
||||||
|
GetOnlineBeatmapSet(settings.BeatmapID, cancellationToken).ContinueWith(set => Schedule(() =>
|
||||||
|
{
|
||||||
|
if (cancellationToken.IsCancellationRequested)
|
||||||
|
return;
|
||||||
|
|
||||||
|
updatePlaylist(settings, set.Result);
|
||||||
|
}), TaskContinuationOptions.OnlyOnRanToCompletion);
|
||||||
|
}, cancellationToken);
|
||||||
|
|
||||||
|
private void updatePlaylist(MultiplayerRoomSettings settings, BeatmapSetInfo beatmapSet)
|
||||||
|
{
|
||||||
|
if (Room == null || !Room.Settings.Equals(settings))
|
||||||
|
return;
|
||||||
|
|
||||||
|
Debug.Assert(apiRoom != null);
|
||||||
|
|
||||||
|
var beatmap = beatmapSet.Beatmaps.Single(b => b.OnlineBeatmapID == settings.BeatmapID);
|
||||||
|
beatmap.MD5Hash = settings.BeatmapChecksum;
|
||||||
|
|
||||||
|
var ruleset = Rulesets.GetRuleset(settings.RulesetID).CreateInstance();
|
||||||
|
var mods = settings.RequiredMods.Select(m => m.ToMod(ruleset));
|
||||||
|
var allowedMods = settings.AllowedMods.Select(m => m.ToMod(ruleset));
|
||||||
|
|
||||||
|
// Try to retrieve the existing playlist item from the API room.
|
||||||
|
var playlistItem = apiRoom.Playlist.FirstOrDefault(i => i.ID == settings.PlaylistItemId);
|
||||||
|
|
||||||
|
if (playlistItem != null)
|
||||||
|
updateItem(playlistItem);
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// An existing playlist item does not exist, so append a new one.
|
||||||
|
updateItem(playlistItem = new PlaylistItem());
|
||||||
|
apiRoom.Playlist.Add(playlistItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
CurrentMatchPlayingItem.Value = playlistItem;
|
||||||
|
|
||||||
|
void updateItem(PlaylistItem item)
|
||||||
|
{
|
||||||
|
item.ID = settings.PlaylistItemId;
|
||||||
|
item.Beatmap.Value = beatmap;
|
||||||
|
item.Ruleset.Value = ruleset.RulesetInfo;
|
||||||
|
item.RequiredMods.Clear();
|
||||||
|
item.RequiredMods.AddRange(mods);
|
||||||
|
item.AllowedMods.Clear();
|
||||||
|
item.AllowedMods.AddRange(allowedMods);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves a <see cref="BeatmapSetInfo"/> from an online source.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="beatmapId">The beatmap set ID.</param>
|
||||||
|
/// <param name="cancellationToken">A token to cancel the request.</param>
|
||||||
|
/// <returns>The <see cref="BeatmapSetInfo"/> retrieval task.</returns>
|
||||||
|
protected abstract Task<BeatmapSetInfo> GetOnlineBeatmapSet(int beatmapId, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// For the provided user ID, update whether the user is included in <see cref="CurrentMatchPlayingUserIds"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userId">The user's ID.</param>
|
||||||
|
/// <param name="state">The new state of the user.</param>
|
||||||
|
private void updateUserPlayingState(int userId, MultiplayerUserState state)
|
||||||
|
{
|
||||||
|
bool wasPlaying = CurrentMatchPlayingUserIds.Contains(userId);
|
||||||
|
bool isPlaying = state >= MultiplayerUserState.WaitingForLoad && state <= MultiplayerUserState.FinishedPlay;
|
||||||
|
|
||||||
|
if (isPlaying == wasPlaying)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (isPlaying)
|
||||||
|
CurrentMatchPlayingUserIds.Add(userId);
|
||||||
|
else
|
||||||
|
CurrentMatchPlayingUserIds.Remove(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task scheduleAsync(Action action, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var tcs = new TaskCompletionSource<bool>();
|
||||||
|
|
||||||
|
Scheduler.Add(() =>
|
||||||
{
|
{
|
||||||
if (cancellationToken.IsCancellationRequested)
|
if (cancellationToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
@ -136,20 +625,18 @@ namespace osu.Game.Online.Multiplayer
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
tcs.SetResult(res.ToBeatmapSet(Rulesets));
|
try
|
||||||
};
|
{
|
||||||
|
action();
|
||||||
req.Failure += e => tcs.SetException(e);
|
tcs.SetResult(true);
|
||||||
|
}
|
||||||
API.Queue(req);
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
tcs.SetException(ex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return tcs.Task;
|
return tcs.Task;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Dispose(bool isDisposing)
|
|
||||||
{
|
|
||||||
base.Dispose(isDisposing);
|
|
||||||
connector?.Dispose();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
158
osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs
Normal file
158
osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
// 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.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.SignalR.Client;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Online.API;
|
||||||
|
using osu.Game.Online.API.Requests;
|
||||||
|
using osu.Game.Online.Rooms;
|
||||||
|
|
||||||
|
namespace osu.Game.Online.Multiplayer
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A <see cref="MultiplayerClient"/> with online connectivity.
|
||||||
|
/// </summary>
|
||||||
|
public class OnlineMultiplayerClient : MultiplayerClient
|
||||||
|
{
|
||||||
|
private readonly string endpoint;
|
||||||
|
|
||||||
|
private IHubClientConnector? connector;
|
||||||
|
|
||||||
|
public override IBindable<bool> IsConnected { get; } = new BindableBool();
|
||||||
|
|
||||||
|
private HubConnection? connection => connector?.CurrentConnection;
|
||||||
|
|
||||||
|
public OnlineMultiplayerClient(EndpointConfiguration endpoints)
|
||||||
|
{
|
||||||
|
endpoint = endpoints.MultiplayerEndpointUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load(IAPIProvider api)
|
||||||
|
{
|
||||||
|
connector = api.GetHubConnector(nameof(OnlineMultiplayerClient), endpoint);
|
||||||
|
|
||||||
|
if (connector != null)
|
||||||
|
{
|
||||||
|
connector.ConfigureConnection = connection =>
|
||||||
|
{
|
||||||
|
// this is kind of SILLY
|
||||||
|
// https://github.com/dotnet/aspnetcore/issues/15198
|
||||||
|
connection.On<MultiplayerRoomState>(nameof(IMultiplayerClient.RoomStateChanged), ((IMultiplayerClient)this).RoomStateChanged);
|
||||||
|
connection.On<MultiplayerRoomUser>(nameof(IMultiplayerClient.UserJoined), ((IMultiplayerClient)this).UserJoined);
|
||||||
|
connection.On<MultiplayerRoomUser>(nameof(IMultiplayerClient.UserLeft), ((IMultiplayerClient)this).UserLeft);
|
||||||
|
connection.On<int>(nameof(IMultiplayerClient.HostChanged), ((IMultiplayerClient)this).HostChanged);
|
||||||
|
connection.On<MultiplayerRoomSettings>(nameof(IMultiplayerClient.SettingsChanged), ((IMultiplayerClient)this).SettingsChanged);
|
||||||
|
connection.On<int, MultiplayerUserState>(nameof(IMultiplayerClient.UserStateChanged), ((IMultiplayerClient)this).UserStateChanged);
|
||||||
|
connection.On(nameof(IMultiplayerClient.LoadRequested), ((IMultiplayerClient)this).LoadRequested);
|
||||||
|
connection.On(nameof(IMultiplayerClient.MatchStarted), ((IMultiplayerClient)this).MatchStarted);
|
||||||
|
connection.On(nameof(IMultiplayerClient.ResultsReady), ((IMultiplayerClient)this).ResultsReady);
|
||||||
|
connection.On<int, IEnumerable<APIMod>>(nameof(IMultiplayerClient.UserModsChanged), ((IMultiplayerClient)this).UserModsChanged);
|
||||||
|
connection.On<int, BeatmapAvailability>(nameof(IMultiplayerClient.UserBeatmapAvailabilityChanged), ((IMultiplayerClient)this).UserBeatmapAvailabilityChanged);
|
||||||
|
};
|
||||||
|
|
||||||
|
IsConnected.BindTo(connector.IsConnected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Task<MultiplayerRoom> JoinRoom(long roomId)
|
||||||
|
{
|
||||||
|
if (!IsConnected.Value)
|
||||||
|
return Task.FromCanceled<MultiplayerRoom>(new CancellationToken(true));
|
||||||
|
|
||||||
|
return connection.InvokeAsync<MultiplayerRoom>(nameof(IMultiplayerServer.JoinRoom), roomId);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Task LeaveRoomInternal()
|
||||||
|
{
|
||||||
|
if (!IsConnected.Value)
|
||||||
|
return Task.FromCanceled(new CancellationToken(true));
|
||||||
|
|
||||||
|
return connection.InvokeAsync(nameof(IMultiplayerServer.LeaveRoom));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Task TransferHost(int userId)
|
||||||
|
{
|
||||||
|
if (!IsConnected.Value)
|
||||||
|
return Task.CompletedTask;
|
||||||
|
|
||||||
|
return connection.InvokeAsync(nameof(IMultiplayerServer.TransferHost), userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Task ChangeSettings(MultiplayerRoomSettings settings)
|
||||||
|
{
|
||||||
|
if (!IsConnected.Value)
|
||||||
|
return Task.CompletedTask;
|
||||||
|
|
||||||
|
return connection.InvokeAsync(nameof(IMultiplayerServer.ChangeSettings), settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Task ChangeState(MultiplayerUserState newState)
|
||||||
|
{
|
||||||
|
if (!IsConnected.Value)
|
||||||
|
return Task.CompletedTask;
|
||||||
|
|
||||||
|
return connection.InvokeAsync(nameof(IMultiplayerServer.ChangeState), newState);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Task ChangeBeatmapAvailability(BeatmapAvailability newBeatmapAvailability)
|
||||||
|
{
|
||||||
|
if (!IsConnected.Value)
|
||||||
|
return Task.CompletedTask;
|
||||||
|
|
||||||
|
return connection.InvokeAsync(nameof(IMultiplayerServer.ChangeBeatmapAvailability), newBeatmapAvailability);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Task ChangeUserMods(IEnumerable<APIMod> newMods)
|
||||||
|
{
|
||||||
|
if (!IsConnected.Value)
|
||||||
|
return Task.CompletedTask;
|
||||||
|
|
||||||
|
return connection.InvokeAsync(nameof(IMultiplayerServer.ChangeUserMods), newMods);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Task StartMatch()
|
||||||
|
{
|
||||||
|
if (!IsConnected.Value)
|
||||||
|
return Task.CompletedTask;
|
||||||
|
|
||||||
|
return connection.InvokeAsync(nameof(IMultiplayerServer.StartMatch));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Task<BeatmapSetInfo> GetOnlineBeatmapSet(int beatmapId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var tcs = new TaskCompletionSource<BeatmapSetInfo>();
|
||||||
|
var req = new GetBeatmapSetRequest(beatmapId, BeatmapSetLookupType.BeatmapId);
|
||||||
|
|
||||||
|
req.Success += res =>
|
||||||
|
{
|
||||||
|
if (cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
tcs.SetCanceled();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
tcs.SetResult(res.ToBeatmapSet(Rulesets));
|
||||||
|
};
|
||||||
|
|
||||||
|
req.Failure += e => tcs.SetException(e);
|
||||||
|
|
||||||
|
API.Queue(req);
|
||||||
|
|
||||||
|
return tcs.Task;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool isDisposing)
|
||||||
|
{
|
||||||
|
base.Dispose(isDisposing);
|
||||||
|
connector?.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,642 +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.
|
|
||||||
|
|
||||||
#nullable enable
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using osu.Framework.Allocation;
|
|
||||||
using osu.Framework.Bindables;
|
|
||||||
using osu.Framework.Extensions.ObjectExtensions;
|
|
||||||
using osu.Framework.Graphics;
|
|
||||||
using osu.Framework.Logging;
|
|
||||||
using osu.Game.Beatmaps;
|
|
||||||
using osu.Game.Database;
|
|
||||||
using osu.Game.Online.API;
|
|
||||||
using osu.Game.Online.Rooms;
|
|
||||||
using osu.Game.Online.Rooms.RoomStatuses;
|
|
||||||
using osu.Game.Rulesets;
|
|
||||||
using osu.Game.Rulesets.Mods;
|
|
||||||
using osu.Game.Users;
|
|
||||||
using osu.Game.Utils;
|
|
||||||
|
|
||||||
namespace osu.Game.Online.Multiplayer
|
|
||||||
{
|
|
||||||
public abstract class StatefulMultiplayerClient : Component, IMultiplayerClient, IMultiplayerRoomServer
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Invoked when any change occurs to the multiplayer room.
|
|
||||||
/// </summary>
|
|
||||||
public event Action? RoomUpdated;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Invoked when the multiplayer server requests the current beatmap to be loaded into play.
|
|
||||||
/// </summary>
|
|
||||||
public event Action? LoadRequested;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Invoked when the multiplayer server requests gameplay to be started.
|
|
||||||
/// </summary>
|
|
||||||
public event Action? MatchStarted;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Invoked when the multiplayer server has finished collating results.
|
|
||||||
/// </summary>
|
|
||||||
public event Action? ResultsReady;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Whether the <see cref="StatefulMultiplayerClient"/> is currently connected.
|
|
||||||
/// This is NOT thread safe and usage should be scheduled.
|
|
||||||
/// </summary>
|
|
||||||
public abstract IBindable<bool> IsConnected { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The joined <see cref="MultiplayerRoom"/>.
|
|
||||||
/// </summary>
|
|
||||||
public MultiplayerRoom? Room { get; private set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The users in the joined <see cref="Room"/> which are participating in the current gameplay loop.
|
|
||||||
/// </summary>
|
|
||||||
public readonly BindableList<int> CurrentMatchPlayingUserIds = new BindableList<int>();
|
|
||||||
|
|
||||||
public readonly Bindable<PlaylistItem?> CurrentMatchPlayingItem = new Bindable<PlaylistItem?>();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The <see cref="MultiplayerRoomUser"/> corresponding to the local player, if available.
|
|
||||||
/// </summary>
|
|
||||||
public MultiplayerRoomUser? LocalUser => Room?.Users.SingleOrDefault(u => u.User?.Id == API.LocalUser.Value.Id);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Whether the <see cref="LocalUser"/> is the host in <see cref="Room"/>.
|
|
||||||
/// </summary>
|
|
||||||
public bool IsHost
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
var localUser = LocalUser;
|
|
||||||
return localUser != null && Room?.Host != null && localUser.Equals(Room.Host);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Resolved]
|
|
||||||
protected IAPIProvider API { get; private set; } = null!;
|
|
||||||
|
|
||||||
[Resolved]
|
|
||||||
protected RulesetStore Rulesets { get; private set; } = null!;
|
|
||||||
|
|
||||||
[Resolved]
|
|
||||||
private UserLookupCache userLookupCache { get; set; } = null!;
|
|
||||||
|
|
||||||
private Room? apiRoom;
|
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
|
||||||
private void load()
|
|
||||||
{
|
|
||||||
IsConnected.BindValueChanged(connected =>
|
|
||||||
{
|
|
||||||
// clean up local room state on server disconnect.
|
|
||||||
if (!connected.NewValue && Room != null)
|
|
||||||
{
|
|
||||||
Logger.Log("Connection to multiplayer server was lost.", LoggingTarget.Runtime, LogLevel.Important);
|
|
||||||
LeaveRoom();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly TaskChain joinOrLeaveTaskChain = new TaskChain();
|
|
||||||
private CancellationTokenSource? joinCancellationSource;
|
|
||||||
|
|
||||||
/// <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)
|
|
||||||
{
|
|
||||||
var cancellationSource = joinCancellationSource = new CancellationTokenSource();
|
|
||||||
|
|
||||||
await joinOrLeaveTaskChain.Add(async () =>
|
|
||||||
{
|
|
||||||
if (Room != null)
|
|
||||||
throw new InvalidOperationException("Cannot join a multiplayer room while already in one.");
|
|
||||||
|
|
||||||
Debug.Assert(room.RoomID.Value != null);
|
|
||||||
|
|
||||||
// Join the server-side room.
|
|
||||||
var joinedRoom = await JoinRoom(room.RoomID.Value.Value).ConfigureAwait(false);
|
|
||||||
Debug.Assert(joinedRoom != null);
|
|
||||||
|
|
||||||
// Populate users.
|
|
||||||
Debug.Assert(joinedRoom.Users != null);
|
|
||||||
await Task.WhenAll(joinedRoom.Users.Select(PopulateUser)).ConfigureAwait(false);
|
|
||||||
|
|
||||||
// Update the stored room (must be done on update thread for thread-safety).
|
|
||||||
await scheduleAsync(() =>
|
|
||||||
{
|
|
||||||
Room = joinedRoom;
|
|
||||||
apiRoom = room;
|
|
||||||
foreach (var user in joinedRoom.Users)
|
|
||||||
updateUserPlayingState(user.UserID, user.State);
|
|
||||||
}, cancellationSource.Token).ConfigureAwait(false);
|
|
||||||
|
|
||||||
// Update room settings.
|
|
||||||
await updateLocalRoomSettings(joinedRoom.Settings, cancellationSource.Token).ConfigureAwait(false);
|
|
||||||
}, cancellationSource.Token).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <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 Task LeaveRoom()
|
|
||||||
{
|
|
||||||
// The join may have not completed yet, so certain tasks that either update the room or reference the room should be cancelled.
|
|
||||||
// This includes the setting of Room itself along with the initial update of the room settings on join.
|
|
||||||
joinCancellationSource?.Cancel();
|
|
||||||
|
|
||||||
// Leaving rooms is expected to occur instantaneously whilst the operation is finalised in the background.
|
|
||||||
// However a few members need to be reset immediately to prevent other components from entering invalid states whilst the operation hasn't yet completed.
|
|
||||||
// For example, if a room was left and the user immediately pressed the "create room" button, then the user could be taken into the lobby if the value of Room is not reset in time.
|
|
||||||
var scheduledReset = scheduleAsync(() =>
|
|
||||||
{
|
|
||||||
apiRoom = null;
|
|
||||||
Room = null;
|
|
||||||
CurrentMatchPlayingUserIds.Clear();
|
|
||||||
|
|
||||||
RoomUpdated?.Invoke();
|
|
||||||
});
|
|
||||||
|
|
||||||
return joinOrLeaveTaskChain.Add(async () =>
|
|
||||||
{
|
|
||||||
await scheduledReset.ConfigureAwait(false);
|
|
||||||
await LeaveRoomInternal().ConfigureAwait(false);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
protected abstract Task LeaveRoomInternal();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Change the current <see cref="MultiplayerRoom"/> settings.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// A room must be joined for this to have any effect.
|
|
||||||
/// </remarks>
|
|
||||||
/// <param name="name">The new room name, if any.</param>
|
|
||||||
/// <param name="item">The new room playlist item, if any.</param>
|
|
||||||
public Task ChangeSettings(Optional<string> name = default, Optional<PlaylistItem> item = default)
|
|
||||||
{
|
|
||||||
if (Room == null)
|
|
||||||
throw new InvalidOperationException("Must be joined to a match to change settings.");
|
|
||||||
|
|
||||||
// A dummy playlist item filled with the current room settings (except mods).
|
|
||||||
var existingPlaylistItem = new PlaylistItem
|
|
||||||
{
|
|
||||||
Beatmap =
|
|
||||||
{
|
|
||||||
Value = new BeatmapInfo
|
|
||||||
{
|
|
||||||
OnlineBeatmapID = Room.Settings.BeatmapID,
|
|
||||||
MD5Hash = Room.Settings.BeatmapChecksum
|
|
||||||
}
|
|
||||||
},
|
|
||||||
RulesetID = Room.Settings.RulesetID
|
|
||||||
};
|
|
||||||
|
|
||||||
return ChangeSettings(new MultiplayerRoomSettings
|
|
||||||
{
|
|
||||||
Name = name.GetOr(Room.Settings.Name),
|
|
||||||
BeatmapID = item.GetOr(existingPlaylistItem).BeatmapID,
|
|
||||||
BeatmapChecksum = item.GetOr(existingPlaylistItem).Beatmap.Value.MD5Hash,
|
|
||||||
RulesetID = item.GetOr(existingPlaylistItem).RulesetID,
|
|
||||||
RequiredMods = item.HasValue ? item.Value.AsNonNull().RequiredMods.Select(m => new APIMod(m)).ToList() : Room.Settings.RequiredMods,
|
|
||||||
AllowedMods = item.HasValue ? item.Value.AsNonNull().AllowedMods.Select(m => new APIMod(m)).ToList() : Room.Settings.AllowedMods,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Toggles the <see cref="LocalUser"/>'s ready state.
|
|
||||||
/// </summary>
|
|
||||||
/// <exception cref="InvalidOperationException">If a toggle of ready state is not valid at this time.</exception>
|
|
||||||
public async Task ToggleReady()
|
|
||||||
{
|
|
||||||
var localUser = LocalUser;
|
|
||||||
|
|
||||||
if (localUser == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
switch (localUser.State)
|
|
||||||
{
|
|
||||||
case MultiplayerUserState.Idle:
|
|
||||||
await ChangeState(MultiplayerUserState.Ready).ConfigureAwait(false);
|
|
||||||
return;
|
|
||||||
|
|
||||||
case MultiplayerUserState.Ready:
|
|
||||||
await ChangeState(MultiplayerUserState.Idle).ConfigureAwait(false);
|
|
||||||
return;
|
|
||||||
|
|
||||||
default:
|
|
||||||
throw new InvalidOperationException($"Cannot toggle ready when in {localUser.State}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Toggles the <see cref="LocalUser"/>'s spectating state.
|
|
||||||
/// </summary>
|
|
||||||
/// <exception cref="InvalidOperationException">If a toggle of the spectating state is not valid at this time.</exception>
|
|
||||||
public async Task ToggleSpectate()
|
|
||||||
{
|
|
||||||
var localUser = LocalUser;
|
|
||||||
|
|
||||||
if (localUser == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
switch (localUser.State)
|
|
||||||
{
|
|
||||||
case MultiplayerUserState.Idle:
|
|
||||||
case MultiplayerUserState.Ready:
|
|
||||||
await ChangeState(MultiplayerUserState.Spectating).ConfigureAwait(false);
|
|
||||||
return;
|
|
||||||
|
|
||||||
case MultiplayerUserState.Spectating:
|
|
||||||
await ChangeState(MultiplayerUserState.Idle).ConfigureAwait(false);
|
|
||||||
return;
|
|
||||||
|
|
||||||
default:
|
|
||||||
throw new InvalidOperationException($"Cannot toggle spectate when in {localUser.State}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public abstract Task TransferHost(int userId);
|
|
||||||
|
|
||||||
public abstract Task ChangeSettings(MultiplayerRoomSettings settings);
|
|
||||||
|
|
||||||
public abstract Task ChangeState(MultiplayerUserState newState);
|
|
||||||
|
|
||||||
public abstract Task ChangeBeatmapAvailability(BeatmapAvailability newBeatmapAvailability);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Change the local user's mods in the currently joined room.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="newMods">The proposed new mods, excluding any required by the room itself.</param>
|
|
||||||
public Task ChangeUserMods(IEnumerable<Mod> newMods) => ChangeUserMods(newMods.Select(m => new APIMod(m)).ToList());
|
|
||||||
|
|
||||||
public abstract Task ChangeUserMods(IEnumerable<APIMod> newMods);
|
|
||||||
|
|
||||||
public abstract Task StartMatch();
|
|
||||||
|
|
||||||
Task IMultiplayerClient.RoomStateChanged(MultiplayerRoomState state)
|
|
||||||
{
|
|
||||||
if (Room == null)
|
|
||||||
return Task.CompletedTask;
|
|
||||||
|
|
||||||
Scheduler.Add(() =>
|
|
||||||
{
|
|
||||||
if (Room == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
Debug.Assert(apiRoom != null);
|
|
||||||
|
|
||||||
Room.State = state;
|
|
||||||
|
|
||||||
switch (state)
|
|
||||||
{
|
|
||||||
case MultiplayerRoomState.Open:
|
|
||||||
apiRoom.Status.Value = new RoomStatusOpen();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case MultiplayerRoomState.Playing:
|
|
||||||
apiRoom.Status.Value = new RoomStatusPlaying();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case MultiplayerRoomState.Closed:
|
|
||||||
apiRoom.Status.Value = new RoomStatusEnded();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
RoomUpdated?.Invoke();
|
|
||||||
}, false);
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
async Task IMultiplayerClient.UserJoined(MultiplayerRoomUser user)
|
|
||||||
{
|
|
||||||
if (Room == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
await PopulateUser(user).ConfigureAwait(false);
|
|
||||||
|
|
||||||
Scheduler.Add(() =>
|
|
||||||
{
|
|
||||||
if (Room == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// for sanity, ensure that there can be no duplicate users in the room user list.
|
|
||||||
if (Room.Users.Any(existing => existing.UserID == user.UserID))
|
|
||||||
return;
|
|
||||||
|
|
||||||
Room.Users.Add(user);
|
|
||||||
|
|
||||||
RoomUpdated?.Invoke();
|
|
||||||
}, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
Task IMultiplayerClient.UserLeft(MultiplayerRoomUser user)
|
|
||||||
{
|
|
||||||
if (Room == null)
|
|
||||||
return Task.CompletedTask;
|
|
||||||
|
|
||||||
Scheduler.Add(() =>
|
|
||||||
{
|
|
||||||
if (Room == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
Room.Users.Remove(user);
|
|
||||||
CurrentMatchPlayingUserIds.Remove(user.UserID);
|
|
||||||
|
|
||||||
RoomUpdated?.Invoke();
|
|
||||||
}, false);
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
Task IMultiplayerClient.HostChanged(int userId)
|
|
||||||
{
|
|
||||||
if (Room == null)
|
|
||||||
return Task.CompletedTask;
|
|
||||||
|
|
||||||
Scheduler.Add(() =>
|
|
||||||
{
|
|
||||||
if (Room == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
Debug.Assert(apiRoom != null);
|
|
||||||
|
|
||||||
var user = Room.Users.FirstOrDefault(u => u.UserID == userId);
|
|
||||||
|
|
||||||
Room.Host = user;
|
|
||||||
apiRoom.Host.Value = user?.User;
|
|
||||||
|
|
||||||
RoomUpdated?.Invoke();
|
|
||||||
}, false);
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
Task IMultiplayerClient.SettingsChanged(MultiplayerRoomSettings newSettings)
|
|
||||||
{
|
|
||||||
updateLocalRoomSettings(newSettings);
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
Task IMultiplayerClient.UserStateChanged(int userId, MultiplayerUserState state)
|
|
||||||
{
|
|
||||||
if (Room == null)
|
|
||||||
return Task.CompletedTask;
|
|
||||||
|
|
||||||
Scheduler.Add(() =>
|
|
||||||
{
|
|
||||||
if (Room == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
Room.Users.Single(u => u.UserID == userId).State = state;
|
|
||||||
|
|
||||||
updateUserPlayingState(userId, state);
|
|
||||||
|
|
||||||
RoomUpdated?.Invoke();
|
|
||||||
}, false);
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
Task IMultiplayerClient.UserBeatmapAvailabilityChanged(int userId, BeatmapAvailability beatmapAvailability)
|
|
||||||
{
|
|
||||||
if (Room == null)
|
|
||||||
return Task.CompletedTask;
|
|
||||||
|
|
||||||
Scheduler.Add(() =>
|
|
||||||
{
|
|
||||||
var user = Room?.Users.SingleOrDefault(u => u.UserID == userId);
|
|
||||||
|
|
||||||
// errors here are not critical - beatmap availability state is mostly for display.
|
|
||||||
if (user == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
user.BeatmapAvailability = beatmapAvailability;
|
|
||||||
|
|
||||||
RoomUpdated?.Invoke();
|
|
||||||
}, false);
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task UserModsChanged(int userId, IEnumerable<APIMod> mods)
|
|
||||||
{
|
|
||||||
if (Room == null)
|
|
||||||
return Task.CompletedTask;
|
|
||||||
|
|
||||||
Scheduler.Add(() =>
|
|
||||||
{
|
|
||||||
var user = Room?.Users.SingleOrDefault(u => u.UserID == userId);
|
|
||||||
|
|
||||||
// errors here are not critical - user mods are mostly for display.
|
|
||||||
if (user == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
user.Mods = mods;
|
|
||||||
|
|
||||||
RoomUpdated?.Invoke();
|
|
||||||
}, false);
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
Task IMultiplayerClient.LoadRequested()
|
|
||||||
{
|
|
||||||
if (Room == null)
|
|
||||||
return Task.CompletedTask;
|
|
||||||
|
|
||||||
Scheduler.Add(() =>
|
|
||||||
{
|
|
||||||
if (Room == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
LoadRequested?.Invoke();
|
|
||||||
}, false);
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
Task IMultiplayerClient.MatchStarted()
|
|
||||||
{
|
|
||||||
if (Room == null)
|
|
||||||
return Task.CompletedTask;
|
|
||||||
|
|
||||||
Scheduler.Add(() =>
|
|
||||||
{
|
|
||||||
if (Room == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
MatchStarted?.Invoke();
|
|
||||||
}, false);
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
Task IMultiplayerClient.ResultsReady()
|
|
||||||
{
|
|
||||||
if (Room == null)
|
|
||||||
return Task.CompletedTask;
|
|
||||||
|
|
||||||
Scheduler.Add(() =>
|
|
||||||
{
|
|
||||||
if (Room == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
ResultsReady?.Invoke();
|
|
||||||
}, false);
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Populates the <see cref="User"/> for a given <see cref="MultiplayerRoomUser"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="multiplayerUser">The <see cref="MultiplayerRoomUser"/> to populate.</param>
|
|
||||||
protected async Task PopulateUser(MultiplayerRoomUser multiplayerUser) => multiplayerUser.User ??= await userLookupCache.GetUserAsync(multiplayerUser.UserID).ConfigureAwait(false);
|
|
||||||
|
|
||||||
/// <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>
|
|
||||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to cancel the update.</param>
|
|
||||||
private Task updateLocalRoomSettings(MultiplayerRoomSettings settings, CancellationToken cancellationToken = default) => scheduleAsync(() =>
|
|
||||||
{
|
|
||||||
if (Room == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
Debug.Assert(apiRoom != null);
|
|
||||||
|
|
||||||
// Update a few properties of the room instantaneously.
|
|
||||||
Room.Settings = settings;
|
|
||||||
apiRoom.Name.Value = Room.Settings.Name;
|
|
||||||
|
|
||||||
// The current item update is delayed until an online beatmap lookup (below) succeeds.
|
|
||||||
// In-order for the client to not display an outdated beatmap, the current item is forcefully cleared here.
|
|
||||||
CurrentMatchPlayingItem.Value = null;
|
|
||||||
|
|
||||||
RoomUpdated?.Invoke();
|
|
||||||
|
|
||||||
GetOnlineBeatmapSet(settings.BeatmapID, cancellationToken).ContinueWith(set => Schedule(() =>
|
|
||||||
{
|
|
||||||
if (cancellationToken.IsCancellationRequested)
|
|
||||||
return;
|
|
||||||
|
|
||||||
updatePlaylist(settings, set.Result);
|
|
||||||
}), TaskContinuationOptions.OnlyOnRanToCompletion);
|
|
||||||
}, cancellationToken);
|
|
||||||
|
|
||||||
private void updatePlaylist(MultiplayerRoomSettings settings, BeatmapSetInfo beatmapSet)
|
|
||||||
{
|
|
||||||
if (Room == null || !Room.Settings.Equals(settings))
|
|
||||||
return;
|
|
||||||
|
|
||||||
Debug.Assert(apiRoom != null);
|
|
||||||
|
|
||||||
var beatmap = beatmapSet.Beatmaps.Single(b => b.OnlineBeatmapID == settings.BeatmapID);
|
|
||||||
beatmap.MD5Hash = settings.BeatmapChecksum;
|
|
||||||
|
|
||||||
var ruleset = Rulesets.GetRuleset(settings.RulesetID).CreateInstance();
|
|
||||||
var mods = settings.RequiredMods.Select(m => m.ToMod(ruleset));
|
|
||||||
var allowedMods = settings.AllowedMods.Select(m => m.ToMod(ruleset));
|
|
||||||
|
|
||||||
// Try to retrieve the existing playlist item from the API room.
|
|
||||||
var playlistItem = apiRoom.Playlist.FirstOrDefault(i => i.ID == settings.PlaylistItemId);
|
|
||||||
|
|
||||||
if (playlistItem != null)
|
|
||||||
updateItem(playlistItem);
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// An existing playlist item does not exist, so append a new one.
|
|
||||||
updateItem(playlistItem = new PlaylistItem());
|
|
||||||
apiRoom.Playlist.Add(playlistItem);
|
|
||||||
}
|
|
||||||
|
|
||||||
CurrentMatchPlayingItem.Value = playlistItem;
|
|
||||||
|
|
||||||
void updateItem(PlaylistItem item)
|
|
||||||
{
|
|
||||||
item.ID = settings.PlaylistItemId;
|
|
||||||
item.Beatmap.Value = beatmap;
|
|
||||||
item.Ruleset.Value = ruleset.RulesetInfo;
|
|
||||||
item.RequiredMods.Clear();
|
|
||||||
item.RequiredMods.AddRange(mods);
|
|
||||||
item.AllowedMods.Clear();
|
|
||||||
item.AllowedMods.AddRange(allowedMods);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Retrieves a <see cref="BeatmapSetInfo"/> from an online source.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="beatmapId">The beatmap set ID.</param>
|
|
||||||
/// <param name="cancellationToken">A token to cancel the request.</param>
|
|
||||||
/// <returns>The <see cref="BeatmapSetInfo"/> retrieval task.</returns>
|
|
||||||
protected abstract Task<BeatmapSetInfo> GetOnlineBeatmapSet(int beatmapId, CancellationToken cancellationToken = default);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// For the provided user ID, update whether the user is included in <see cref="CurrentMatchPlayingUserIds"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="userId">The user's ID.</param>
|
|
||||||
/// <param name="state">The new state of the user.</param>
|
|
||||||
private void updateUserPlayingState(int userId, MultiplayerUserState state)
|
|
||||||
{
|
|
||||||
bool wasPlaying = CurrentMatchPlayingUserIds.Contains(userId);
|
|
||||||
bool isPlaying = state >= MultiplayerUserState.WaitingForLoad && state <= MultiplayerUserState.FinishedPlay;
|
|
||||||
|
|
||||||
if (isPlaying == wasPlaying)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (isPlaying)
|
|
||||||
CurrentMatchPlayingUserIds.Add(userId);
|
|
||||||
else
|
|
||||||
CurrentMatchPlayingUserIds.Remove(userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Task scheduleAsync(Action action, CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
var tcs = new TaskCompletionSource<bool>();
|
|
||||||
|
|
||||||
Scheduler.Add(() =>
|
|
||||||
{
|
|
||||||
if (cancellationToken.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
tcs.SetCanceled();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
action();
|
|
||||||
tcs.SetResult(true);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
tcs.SetException(ex);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return tcs.Task;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
89
osu.Game/Online/Spectator/OnlineSpectatorClient.cs
Normal file
89
osu.Game/Online/Spectator/OnlineSpectatorClient.cs
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
// 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.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.SignalR.Client;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Game.Online.API;
|
||||||
|
|
||||||
|
namespace osu.Game.Online.Spectator
|
||||||
|
{
|
||||||
|
public class OnlineSpectatorClient : SpectatorClient
|
||||||
|
{
|
||||||
|
private readonly string endpoint;
|
||||||
|
|
||||||
|
private IHubClientConnector? connector;
|
||||||
|
|
||||||
|
public override IBindable<bool> IsConnected { get; } = new BindableBool();
|
||||||
|
|
||||||
|
private HubConnection? connection => connector?.CurrentConnection;
|
||||||
|
|
||||||
|
public OnlineSpectatorClient(EndpointConfiguration endpoints)
|
||||||
|
{
|
||||||
|
endpoint = endpoints.SpectatorEndpointUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load(IAPIProvider api)
|
||||||
|
{
|
||||||
|
connector = api.GetHubConnector(nameof(SpectatorClient), endpoint);
|
||||||
|
|
||||||
|
if (connector != null)
|
||||||
|
{
|
||||||
|
connector.ConfigureConnection = connection =>
|
||||||
|
{
|
||||||
|
// until strong typed client support is added, each method must be manually bound
|
||||||
|
// (see https://github.com/dotnet/aspnetcore/issues/15198)
|
||||||
|
connection.On<int, SpectatorState>(nameof(ISpectatorClient.UserBeganPlaying), ((ISpectatorClient)this).UserBeganPlaying);
|
||||||
|
connection.On<int, FrameDataBundle>(nameof(ISpectatorClient.UserSentFrames), ((ISpectatorClient)this).UserSentFrames);
|
||||||
|
connection.On<int, SpectatorState>(nameof(ISpectatorClient.UserFinishedPlaying), ((ISpectatorClient)this).UserFinishedPlaying);
|
||||||
|
};
|
||||||
|
|
||||||
|
IsConnected.BindTo(connector.IsConnected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Task BeginPlayingInternal(SpectatorState state)
|
||||||
|
{
|
||||||
|
if (!IsConnected.Value)
|
||||||
|
return Task.CompletedTask;
|
||||||
|
|
||||||
|
return connection.SendAsync(nameof(ISpectatorServer.BeginPlaySession), state);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Task SendFramesInternal(FrameDataBundle data)
|
||||||
|
{
|
||||||
|
if (!IsConnected.Value)
|
||||||
|
return Task.CompletedTask;
|
||||||
|
|
||||||
|
return connection.SendAsync(nameof(ISpectatorServer.SendFrameData), data);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Task EndPlayingInternal(SpectatorState state)
|
||||||
|
{
|
||||||
|
if (!IsConnected.Value)
|
||||||
|
return Task.CompletedTask;
|
||||||
|
|
||||||
|
return connection.SendAsync(nameof(ISpectatorServer.EndPlaySession), state);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Task WatchUserInternal(int userId)
|
||||||
|
{
|
||||||
|
if (!IsConnected.Value)
|
||||||
|
return Task.CompletedTask;
|
||||||
|
|
||||||
|
return connection.SendAsync(nameof(ISpectatorServer.StartWatchingUser), userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Task StopWatchingUserInternal(int userId)
|
||||||
|
{
|
||||||
|
if (!IsConnected.Value)
|
||||||
|
return Task.CompletedTask;
|
||||||
|
|
||||||
|
return connection.SendAsync(nameof(ISpectatorServer.EndWatchingUser), userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,15 +1,16 @@
|
|||||||
// 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.
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using JetBrains.Annotations;
|
|
||||||
using Microsoft.AspNetCore.SignalR.Client;
|
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Development;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Online.API;
|
using osu.Game.Online.API;
|
||||||
@ -23,121 +24,89 @@ using osu.Game.Screens.Play;
|
|||||||
|
|
||||||
namespace osu.Game.Online.Spectator
|
namespace osu.Game.Online.Spectator
|
||||||
{
|
{
|
||||||
public class SpectatorStreamingClient : Component, ISpectatorClient
|
public abstract class SpectatorClient : Component, ISpectatorClient
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The maximum milliseconds between frame bundle sends.
|
/// The maximum milliseconds between frame bundle sends.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const double TIME_BETWEEN_SENDS = 200;
|
public const double TIME_BETWEEN_SENDS = 200;
|
||||||
|
|
||||||
private readonly string endpoint;
|
/// <summary>
|
||||||
|
/// Whether the <see cref="SpectatorClient"/> is currently connected.
|
||||||
[CanBeNull]
|
/// This is NOT thread safe and usage should be scheduled.
|
||||||
private IHubClientConnector connector;
|
/// </summary>
|
||||||
|
public abstract IBindable<bool> IsConnected { get; }
|
||||||
private readonly IBindable<bool> isConnected = new BindableBool();
|
|
||||||
|
|
||||||
private HubConnection connection => connector?.CurrentConnection;
|
|
||||||
|
|
||||||
private readonly List<int> watchingUsers = new List<int>();
|
private readonly List<int> watchingUsers = new List<int>();
|
||||||
|
|
||||||
private readonly object userLock = new object();
|
|
||||||
|
|
||||||
public IBindableList<int> PlayingUsers => playingUsers;
|
public IBindableList<int> PlayingUsers => playingUsers;
|
||||||
|
|
||||||
private readonly BindableList<int> playingUsers = new BindableList<int>();
|
private readonly BindableList<int> playingUsers = new BindableList<int>();
|
||||||
|
|
||||||
private readonly Dictionary<int, SpectatorState> playingUserStates = new Dictionary<int, SpectatorState>();
|
private readonly Dictionary<int, SpectatorState> playingUserStates = new Dictionary<int, SpectatorState>();
|
||||||
|
|
||||||
[CanBeNull]
|
private IBeatmap? currentBeatmap;
|
||||||
private IBeatmap currentBeatmap;
|
|
||||||
|
|
||||||
[CanBeNull]
|
private Score? currentScore;
|
||||||
private Score currentScore;
|
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private IBindable<RulesetInfo> currentRuleset { get; set; }
|
private IBindable<RulesetInfo> currentRuleset { get; set; } = null!;
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private IBindable<IReadOnlyList<Mod>> currentMods { get; set; }
|
private IBindable<IReadOnlyList<Mod>> currentMods { get; set; } = null!;
|
||||||
|
|
||||||
private readonly SpectatorState currentState = new SpectatorState();
|
private readonly SpectatorState currentState = new SpectatorState();
|
||||||
|
|
||||||
private bool isPlaying;
|
/// <summary>
|
||||||
|
/// Whether the local user is playing.
|
||||||
|
/// </summary>
|
||||||
|
protected bool IsPlaying { get; private set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Called whenever new frames arrive from the server.
|
/// Called whenever new frames arrive from the server.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public event Action<int, FrameDataBundle> OnNewFrames;
|
public event Action<int, FrameDataBundle>? OnNewFrames;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Called whenever a user starts a play session, or immediately if the user is being watched and currently in a play session.
|
/// Called whenever a user starts a play session, or immediately if the user is being watched and currently in a play session.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public event Action<int, SpectatorState> OnUserBeganPlaying;
|
public event Action<int, SpectatorState>? OnUserBeganPlaying;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Called whenever a user finishes a play session.
|
/// Called whenever a user finishes a play session.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public event Action<int, SpectatorState> OnUserFinishedPlaying;
|
public event Action<int, SpectatorState>? OnUserFinishedPlaying;
|
||||||
|
|
||||||
public SpectatorStreamingClient(EndpointConfiguration endpoints)
|
|
||||||
{
|
|
||||||
endpoint = endpoints.SpectatorEndpointUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(IAPIProvider api)
|
private void load()
|
||||||
{
|
{
|
||||||
connector = api.GetHubConnector(nameof(SpectatorStreamingClient), endpoint);
|
IsConnected.BindValueChanged(connected => Schedule(() =>
|
||||||
|
|
||||||
if (connector != null)
|
|
||||||
{
|
{
|
||||||
connector.ConfigureConnection = connection =>
|
if (connected.NewValue)
|
||||||
{
|
{
|
||||||
// until strong typed client support is added, each method must be manually bound
|
// get all the users that were previously being watched
|
||||||
// (see https://github.com/dotnet/aspnetcore/issues/15198)
|
int[] users = watchingUsers.ToArray();
|
||||||
connection.On<int, SpectatorState>(nameof(ISpectatorClient.UserBeganPlaying), ((ISpectatorClient)this).UserBeganPlaying);
|
watchingUsers.Clear();
|
||||||
connection.On<int, FrameDataBundle>(nameof(ISpectatorClient.UserSentFrames), ((ISpectatorClient)this).UserSentFrames);
|
|
||||||
connection.On<int, SpectatorState>(nameof(ISpectatorClient.UserFinishedPlaying), ((ISpectatorClient)this).UserFinishedPlaying);
|
|
||||||
};
|
|
||||||
|
|
||||||
isConnected.BindTo(connector.IsConnected);
|
// resubscribe to watched users.
|
||||||
isConnected.BindValueChanged(connected =>
|
foreach (var userId in users)
|
||||||
|
WatchUser(userId);
|
||||||
|
|
||||||
|
// re-send state in case it wasn't received
|
||||||
|
if (IsPlaying)
|
||||||
|
BeginPlayingInternal(currentState);
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
if (connected.NewValue)
|
playingUsers.Clear();
|
||||||
{
|
playingUserStates.Clear();
|
||||||
// get all the users that were previously being watched
|
}
|
||||||
int[] users;
|
}), true);
|
||||||
|
|
||||||
lock (userLock)
|
|
||||||
{
|
|
||||||
users = watchingUsers.ToArray();
|
|
||||||
watchingUsers.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
// resubscribe to watched users.
|
|
||||||
foreach (var userId in users)
|
|
||||||
WatchUser(userId);
|
|
||||||
|
|
||||||
// re-send state in case it wasn't received
|
|
||||||
if (isPlaying)
|
|
||||||
beginPlaying();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
lock (userLock)
|
|
||||||
{
|
|
||||||
playingUsers.Clear();
|
|
||||||
playingUserStates.Clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Task ISpectatorClient.UserBeganPlaying(int userId, SpectatorState state)
|
Task ISpectatorClient.UserBeganPlaying(int userId, SpectatorState state)
|
||||||
{
|
{
|
||||||
lock (userLock)
|
Schedule(() =>
|
||||||
{
|
{
|
||||||
if (!playingUsers.Contains(userId))
|
if (!playingUsers.Contains(userId))
|
||||||
playingUsers.Add(userId);
|
playingUsers.Add(userId);
|
||||||
@ -147,39 +116,41 @@ namespace osu.Game.Online.Spectator
|
|||||||
// We don't want the user states to update unless the player is being watched, otherwise calling BindUserBeganPlaying() can lead to double invocations.
|
// We don't want the user states to update unless the player is being watched, otherwise calling BindUserBeganPlaying() can lead to double invocations.
|
||||||
if (watchingUsers.Contains(userId))
|
if (watchingUsers.Contains(userId))
|
||||||
playingUserStates[userId] = state;
|
playingUserStates[userId] = state;
|
||||||
}
|
|
||||||
|
|
||||||
OnUserBeganPlaying?.Invoke(userId, state);
|
OnUserBeganPlaying?.Invoke(userId, state);
|
||||||
|
});
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
Task ISpectatorClient.UserFinishedPlaying(int userId, SpectatorState state)
|
Task ISpectatorClient.UserFinishedPlaying(int userId, SpectatorState state)
|
||||||
{
|
{
|
||||||
lock (userLock)
|
Schedule(() =>
|
||||||
{
|
{
|
||||||
playingUsers.Remove(userId);
|
playingUsers.Remove(userId);
|
||||||
playingUserStates.Remove(userId);
|
playingUserStates.Remove(userId);
|
||||||
}
|
|
||||||
|
|
||||||
OnUserFinishedPlaying?.Invoke(userId, state);
|
OnUserFinishedPlaying?.Invoke(userId, state);
|
||||||
|
});
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
Task ISpectatorClient.UserSentFrames(int userId, FrameDataBundle data)
|
Task ISpectatorClient.UserSentFrames(int userId, FrameDataBundle data)
|
||||||
{
|
{
|
||||||
OnNewFrames?.Invoke(userId, data);
|
Schedule(() => OnNewFrames?.Invoke(userId, data));
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void BeginPlaying(GameplayBeatmap beatmap, Score score)
|
public void BeginPlaying(GameplayBeatmap beatmap, Score score)
|
||||||
{
|
{
|
||||||
if (isPlaying)
|
Debug.Assert(ThreadSafety.IsUpdateThread);
|
||||||
|
|
||||||
|
if (IsPlaying)
|
||||||
throw new InvalidOperationException($"Cannot invoke {nameof(BeginPlaying)} when already playing");
|
throw new InvalidOperationException($"Cannot invoke {nameof(BeginPlaying)} when already playing");
|
||||||
|
|
||||||
isPlaying = true;
|
IsPlaying = true;
|
||||||
|
|
||||||
// transfer state at point of beginning play
|
// transfer state at point of beginning play
|
||||||
currentState.BeatmapID = beatmap.BeatmapInfo.OnlineBeatmapID;
|
currentState.BeatmapID = beatmap.BeatmapInfo.OnlineBeatmapID;
|
||||||
@ -189,69 +160,65 @@ namespace osu.Game.Online.Spectator
|
|||||||
currentBeatmap = beatmap.PlayableBeatmap;
|
currentBeatmap = beatmap.PlayableBeatmap;
|
||||||
currentScore = score;
|
currentScore = score;
|
||||||
|
|
||||||
beginPlaying();
|
BeginPlayingInternal(currentState);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void beginPlaying()
|
public void SendFrames(FrameDataBundle data) => lastSend = SendFramesInternal(data);
|
||||||
{
|
|
||||||
Debug.Assert(isPlaying);
|
|
||||||
|
|
||||||
if (!isConnected.Value) return;
|
|
||||||
|
|
||||||
connection.SendAsync(nameof(ISpectatorServer.BeginPlaySession), currentState);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SendFrames(FrameDataBundle data)
|
|
||||||
{
|
|
||||||
if (!isConnected.Value) return;
|
|
||||||
|
|
||||||
lastSend = connection.SendAsync(nameof(ISpectatorServer.SendFrameData), data);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void EndPlaying()
|
public void EndPlaying()
|
||||||
{
|
{
|
||||||
isPlaying = false;
|
// This method is most commonly called via Dispose(), which is asynchronous.
|
||||||
currentBeatmap = null;
|
// Todo: This should not be a thing, but requires framework changes.
|
||||||
|
Schedule(() =>
|
||||||
if (!isConnected.Value) return;
|
|
||||||
|
|
||||||
connection.SendAsync(nameof(ISpectatorServer.EndPlaySession), currentState);
|
|
||||||
}
|
|
||||||
|
|
||||||
public virtual void WatchUser(int userId)
|
|
||||||
{
|
|
||||||
lock (userLock)
|
|
||||||
{
|
{
|
||||||
if (watchingUsers.Contains(userId))
|
if (!IsPlaying)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
watchingUsers.Add(userId);
|
IsPlaying = false;
|
||||||
|
currentBeatmap = null;
|
||||||
|
|
||||||
if (!isConnected.Value)
|
EndPlayingInternal(currentState);
|
||||||
return;
|
});
|
||||||
}
|
|
||||||
|
|
||||||
connection.SendAsync(nameof(ISpectatorServer.StartWatchingUser), userId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual void StopWatchingUser(int userId)
|
public void WatchUser(int userId)
|
||||||
{
|
{
|
||||||
lock (userLock)
|
Debug.Assert(ThreadSafety.IsUpdateThread);
|
||||||
|
|
||||||
|
if (watchingUsers.Contains(userId))
|
||||||
|
return;
|
||||||
|
|
||||||
|
watchingUsers.Add(userId);
|
||||||
|
|
||||||
|
WatchUserInternal(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void StopWatchingUser(int userId)
|
||||||
|
{
|
||||||
|
// This method is most commonly called via Dispose(), which is asynchronous.
|
||||||
|
// Todo: This should not be a thing, but requires framework changes.
|
||||||
|
Schedule(() =>
|
||||||
{
|
{
|
||||||
watchingUsers.Remove(userId);
|
watchingUsers.Remove(userId);
|
||||||
|
StopWatchingUserInternal(userId);
|
||||||
if (!isConnected.Value)
|
});
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
connection.SendAsync(nameof(ISpectatorServer.EndWatchingUser), userId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected abstract Task BeginPlayingInternal(SpectatorState state);
|
||||||
|
|
||||||
|
protected abstract Task SendFramesInternal(FrameDataBundle data);
|
||||||
|
|
||||||
|
protected abstract Task EndPlayingInternal(SpectatorState state);
|
||||||
|
|
||||||
|
protected abstract Task WatchUserInternal(int userId);
|
||||||
|
|
||||||
|
protected abstract Task StopWatchingUserInternal(int userId);
|
||||||
|
|
||||||
private readonly Queue<LegacyReplayFrame> pendingFrames = new Queue<LegacyReplayFrame>();
|
private readonly Queue<LegacyReplayFrame> pendingFrames = new Queue<LegacyReplayFrame>();
|
||||||
|
|
||||||
private double lastSendTime;
|
private double lastSendTime;
|
||||||
|
|
||||||
private Task lastSend;
|
private Task? lastSend;
|
||||||
|
|
||||||
private const int max_pending_frames = 30;
|
private const int max_pending_frames = 30;
|
||||||
|
|
||||||
@ -265,6 +232,8 @@ namespace osu.Game.Online.Spectator
|
|||||||
|
|
||||||
public void HandleFrame(ReplayFrame frame)
|
public void HandleFrame(ReplayFrame frame)
|
||||||
{
|
{
|
||||||
|
Debug.Assert(ThreadSafety.IsUpdateThread);
|
||||||
|
|
||||||
if (frame is IConvertibleReplayFrame convertible)
|
if (frame is IConvertibleReplayFrame convertible)
|
||||||
pendingFrames.Enqueue(convertible.ToLegacy(currentBeatmap));
|
pendingFrames.Enqueue(convertible.ToLegacy(currentBeatmap));
|
||||||
|
|
||||||
@ -296,8 +265,7 @@ namespace osu.Game.Online.Spectator
|
|||||||
/// <returns><c>true</c> if successful (the user is playing), <c>false</c> otherwise.</returns>
|
/// <returns><c>true</c> if successful (the user is playing), <c>false</c> otherwise.</returns>
|
||||||
public bool TryGetPlayingUserState(int userId, out SpectatorState state)
|
public bool TryGetPlayingUserState(int userId, out SpectatorState state)
|
||||||
{
|
{
|
||||||
lock (userLock)
|
return playingUserStates.TryGetValue(userId, out state);
|
||||||
return playingUserStates.TryGetValue(userId, out state);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -308,16 +276,13 @@ namespace osu.Game.Online.Spectator
|
|||||||
public void BindUserBeganPlaying(Action<int, SpectatorState> callback, bool runOnceImmediately = false)
|
public void BindUserBeganPlaying(Action<int, SpectatorState> callback, bool runOnceImmediately = false)
|
||||||
{
|
{
|
||||||
// The lock is taken before the event is subscribed to to prevent doubling of events.
|
// The lock is taken before the event is subscribed to to prevent doubling of events.
|
||||||
lock (userLock)
|
OnUserBeganPlaying += callback;
|
||||||
{
|
|
||||||
OnUserBeganPlaying += callback;
|
|
||||||
|
|
||||||
if (!runOnceImmediately)
|
if (!runOnceImmediately)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
foreach (var (userId, state) in playingUserStates)
|
foreach (var (userId, state) in playingUserStates)
|
||||||
callback(userId, state);
|
callback(userId, state);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -85,8 +85,8 @@ namespace osu.Game
|
|||||||
|
|
||||||
protected IAPIProvider API;
|
protected IAPIProvider API;
|
||||||
|
|
||||||
private SpectatorStreamingClient spectatorStreaming;
|
private SpectatorClient spectatorClient;
|
||||||
private StatefulMultiplayerClient multiplayerClient;
|
private MultiplayerClient multiplayerClient;
|
||||||
|
|
||||||
protected MenuCursorContainer MenuCursorContainer;
|
protected MenuCursorContainer MenuCursorContainer;
|
||||||
|
|
||||||
@ -240,8 +240,8 @@ namespace osu.Game
|
|||||||
|
|
||||||
dependencies.CacheAs(API ??= new APIAccess(LocalConfig, endpoints, VersionHash));
|
dependencies.CacheAs(API ??= new APIAccess(LocalConfig, endpoints, VersionHash));
|
||||||
|
|
||||||
dependencies.CacheAs(spectatorStreaming = new SpectatorStreamingClient(endpoints));
|
dependencies.CacheAs(spectatorClient = new OnlineSpectatorClient(endpoints));
|
||||||
dependencies.CacheAs(multiplayerClient = new MultiplayerClient(endpoints));
|
dependencies.CacheAs(multiplayerClient = new OnlineMultiplayerClient(endpoints));
|
||||||
|
|
||||||
var defaultBeatmap = new DummyWorkingBeatmap(Audio, Textures);
|
var defaultBeatmap = new DummyWorkingBeatmap(Audio, Textures);
|
||||||
|
|
||||||
@ -313,7 +313,7 @@ namespace osu.Game
|
|||||||
// add api components to hierarchy.
|
// add api components to hierarchy.
|
||||||
if (API is APIAccess apiAccess)
|
if (API is APIAccess apiAccess)
|
||||||
AddInternal(apiAccess);
|
AddInternal(apiAccess);
|
||||||
AddInternal(spectatorStreaming);
|
AddInternal(spectatorClient);
|
||||||
AddInternal(multiplayerClient);
|
AddInternal(multiplayerClient);
|
||||||
|
|
||||||
AddInternal(RulesetConfigCache);
|
AddInternal(RulesetConfigCache);
|
||||||
|
@ -23,14 +23,17 @@ namespace osu.Game.Overlays.AccountCreation
|
|||||||
private OsuTextFlowContainer multiAccountExplanationText;
|
private OsuTextFlowContainer multiAccountExplanationText;
|
||||||
private LinkFlowContainer furtherAssistance;
|
private LinkFlowContainer furtherAssistance;
|
||||||
|
|
||||||
[Resolved(CanBeNull = true)]
|
[Resolved(canBeNull: true)]
|
||||||
private IAPIProvider api { get; set; }
|
private IAPIProvider api { get; set; }
|
||||||
|
|
||||||
|
[Resolved(canBeNull: true)]
|
||||||
|
private OsuGame game { get; set; }
|
||||||
|
|
||||||
private const string help_centre_url = "/help/wiki/Help_Centre#login";
|
private const string help_centre_url = "/help/wiki/Help_Centre#login";
|
||||||
|
|
||||||
public override void OnEntering(IScreen last)
|
public override void OnEntering(IScreen last)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(api?.ProvidedUsername))
|
if (string.IsNullOrEmpty(api?.ProvidedUsername) || game?.UseDevelopmentServer == true)
|
||||||
{
|
{
|
||||||
this.FadeOut();
|
this.FadeOut();
|
||||||
this.Push(new ScreenEntry());
|
this.Push(new ScreenEntry());
|
||||||
@ -41,7 +44,7 @@ namespace osu.Game.Overlays.AccountCreation
|
|||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader(true)]
|
[BackgroundDependencyLoader(true)]
|
||||||
private void load(OsuColour colours, OsuGame game, TextureStore textures)
|
private void load(OsuColour colours, TextureStore textures)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(api?.ProvidedUsername))
|
if (string.IsNullOrEmpty(api?.ProvidedUsername))
|
||||||
return;
|
return;
|
||||||
|
@ -26,7 +26,10 @@ namespace osu.Game.Overlays
|
|||||||
AccentColour = colourProvider.Light2;
|
AccentColour = colourProvider.Light2;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override TabItem<string> CreateTabItem(string value) => new ControlTabItem(value);
|
protected override TabItem<string> CreateTabItem(string value) => new ControlTabItem(value)
|
||||||
|
{
|
||||||
|
AccentColour = AccentColour,
|
||||||
|
};
|
||||||
|
|
||||||
private class ControlTabItem : BreadcrumbTabItem
|
private class ControlTabItem : BreadcrumbTabItem
|
||||||
{
|
{
|
||||||
|
@ -25,7 +25,7 @@ namespace osu.Game.Overlays.Dashboard
|
|||||||
private FillFlowContainer<PlayingUserPanel> userFlow;
|
private FillFlowContainer<PlayingUserPanel> userFlow;
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private SpectatorStreamingClient spectatorStreaming { get; set; }
|
private SpectatorClient spectatorClient { get; set; }
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load()
|
private void load()
|
||||||
@ -52,7 +52,7 @@ namespace osu.Game.Overlays.Dashboard
|
|||||||
{
|
{
|
||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
|
|
||||||
playingUsers.BindTo(spectatorStreaming.PlayingUsers);
|
playingUsers.BindTo(spectatorClient.PlayingUsers);
|
||||||
playingUsers.BindCollectionChanged(onUsersChanged, true);
|
playingUsers.BindCollectionChanged(onUsersChanged, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
@ -9,12 +10,18 @@ using osu.Framework.Graphics.Containers;
|
|||||||
using osu.Game.Graphics.UserInterface;
|
using osu.Game.Graphics.UserInterface;
|
||||||
using osu.Game.Online.API;
|
using osu.Game.Online.API;
|
||||||
using osu.Game.Online.API.Requests;
|
using osu.Game.Online.API.Requests;
|
||||||
|
using osu.Game.Online.API.Requests.Responses;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Overlays.News.Displays
|
namespace osu.Game.Overlays.News.Displays
|
||||||
{
|
{
|
||||||
public class FrontPageDisplay : CompositeDrawable
|
/// <summary>
|
||||||
|
/// Lists articles in a vertical flow for a specified year.
|
||||||
|
/// </summary>
|
||||||
|
public class ArticleListing : CompositeDrawable
|
||||||
{
|
{
|
||||||
|
public Action<APINewsSidebar> SidebarMetadataUpdated;
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private IAPIProvider api { get; set; }
|
private IAPIProvider api { get; set; }
|
||||||
|
|
||||||
@ -24,6 +31,17 @@ namespace osu.Game.Overlays.News.Displays
|
|||||||
private GetNewsRequest request;
|
private GetNewsRequest request;
|
||||||
private Cursor lastCursor;
|
private Cursor lastCursor;
|
||||||
|
|
||||||
|
private readonly int? year;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Instantiate a listing for the specified year.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="year">The year to load articles from. If null, will show the most recent articles.</param>
|
||||||
|
public ArticleListing(int? year = null)
|
||||||
|
{
|
||||||
|
this.year = year;
|
||||||
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load()
|
private void load()
|
||||||
{
|
{
|
||||||
@ -74,7 +92,7 @@ namespace osu.Game.Overlays.News.Displays
|
|||||||
{
|
{
|
||||||
request?.Cancel();
|
request?.Cancel();
|
||||||
|
|
||||||
request = new GetNewsRequest(lastCursor);
|
request = new GetNewsRequest(year, lastCursor);
|
||||||
request.Success += response => Schedule(() => onSuccess(response));
|
request.Success += response => Schedule(() => onSuccess(response));
|
||||||
api.PerformAsync(request);
|
api.PerformAsync(request);
|
||||||
}
|
}
|
||||||
@ -85,22 +103,19 @@ namespace osu.Game.Overlays.News.Displays
|
|||||||
{
|
{
|
||||||
cancellationToken?.Cancel();
|
cancellationToken?.Cancel();
|
||||||
|
|
||||||
|
// only needs to be updated on the initial load, as the content won't change during pagination.
|
||||||
|
if (lastCursor == null)
|
||||||
|
SidebarMetadataUpdated?.Invoke(response.SidebarMetadata);
|
||||||
|
|
||||||
|
// store cursor for next pagination request.
|
||||||
lastCursor = response.Cursor;
|
lastCursor = response.Cursor;
|
||||||
|
|
||||||
var flow = new FillFlowContainer<NewsCard>
|
LoadComponentsAsync(response.NewsPosts.Select(p => new NewsCard(p)).ToList(), loaded =>
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.X,
|
content.AddRange(loaded);
|
||||||
AutoSizeAxes = Axes.Y,
|
|
||||||
Direction = FillDirection.Vertical,
|
|
||||||
Spacing = new Vector2(0, 10),
|
|
||||||
Children = response.NewsPosts.Select(p => new NewsCard(p)).ToList()
|
|
||||||
};
|
|
||||||
|
|
||||||
LoadComponentAsync(flow, loaded =>
|
|
||||||
{
|
|
||||||
content.Add(loaded);
|
|
||||||
showMore.IsLoading = false;
|
showMore.IsLoading = false;
|
||||||
showMore.Alpha = lastCursor == null ? 0 : 1;
|
showMore.Alpha = response.Cursor != null ? 1 : 0;
|
||||||
}, (cancellationToken = new CancellationTokenSource()).Token);
|
}, (cancellationToken = new CancellationTokenSource()).Token);
|
||||||
}
|
}
|
||||||
|
|
@ -19,13 +19,18 @@ namespace osu.Game.Overlays.News
|
|||||||
{
|
{
|
||||||
TabControl.AddItem(front_page_string);
|
TabControl.AddItem(front_page_string);
|
||||||
|
|
||||||
|
article.BindValueChanged(onArticleChanged, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
Current.BindValueChanged(e =>
|
Current.BindValueChanged(e =>
|
||||||
{
|
{
|
||||||
if (e.NewValue == front_page_string)
|
if (e.NewValue == front_page_string)
|
||||||
ShowFrontPage?.Invoke();
|
ShowFrontPage?.Invoke();
|
||||||
});
|
});
|
||||||
|
|
||||||
article.BindValueChanged(onArticleChanged, true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetFrontPage() => article.Value = null;
|
public void SetFrontPage() => article.Value = null;
|
||||||
|
@ -9,6 +9,7 @@ using osu.Game.Online.API.Requests.Responses;
|
|||||||
using osu.Framework.Graphics.Shapes;
|
using osu.Framework.Graphics.Shapes;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using osu.Game.Graphics.Containers;
|
||||||
|
|
||||||
namespace osu.Game.Overlays.News.Sidebar
|
namespace osu.Game.Overlays.News.Sidebar
|
||||||
{
|
{
|
||||||
@ -31,30 +32,55 @@ namespace osu.Game.Overlays.News.Sidebar
|
|||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Colour = colourProvider.Background4
|
Colour = colourProvider.Background4
|
||||||
},
|
},
|
||||||
|
new Box
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Y,
|
||||||
|
Width = OsuScrollContainer.SCROLL_BAR_HEIGHT,
|
||||||
|
Anchor = Anchor.TopRight,
|
||||||
|
Origin = Anchor.TopRight,
|
||||||
|
Colour = colourProvider.Background3,
|
||||||
|
Alpha = 0.5f
|
||||||
|
},
|
||||||
new Container
|
new Container
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Padding = new MarginPadding
|
Padding = new MarginPadding { Right = -3 }, // Compensate for scrollbar margin
|
||||||
|
Child = new OsuScrollContainer
|
||||||
{
|
{
|
||||||
Vertical = 20,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Left = 50,
|
Child = new Container
|
||||||
Right = 30
|
|
||||||
},
|
|
||||||
Child = new FillFlowContainer
|
|
||||||
{
|
|
||||||
Direction = FillDirection.Vertical,
|
|
||||||
RelativeSizeAxes = Axes.X,
|
|
||||||
AutoSizeAxes = Axes.Y,
|
|
||||||
Spacing = new Vector2(0, 20),
|
|
||||||
Children = new Drawable[]
|
|
||||||
{
|
{
|
||||||
new YearsPanel(),
|
RelativeSizeAxes = Axes.X,
|
||||||
monthsFlow = new FillFlowContainer<MonthSection>
|
AutoSizeAxes = Axes.Y,
|
||||||
|
Padding = new MarginPadding { Right = 3 }, // Addeded 3px back
|
||||||
|
Child = new Container
|
||||||
{
|
{
|
||||||
AutoSizeAxes = Axes.Y,
|
|
||||||
RelativeSizeAxes = Axes.X,
|
RelativeSizeAxes = Axes.X,
|
||||||
Direction = FillDirection.Vertical,
|
AutoSizeAxes = Axes.Y,
|
||||||
Spacing = new Vector2(0, 10)
|
Padding = new MarginPadding
|
||||||
|
{
|
||||||
|
Vertical = 20,
|
||||||
|
Left = 50,
|
||||||
|
Right = 30
|
||||||
|
},
|
||||||
|
Child = new FillFlowContainer
|
||||||
|
{
|
||||||
|
Direction = FillDirection.Vertical,
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
AutoSizeAxes = Axes.Y,
|
||||||
|
Spacing = new Vector2(0, 20),
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
new YearsPanel(),
|
||||||
|
monthsFlow = new FillFlowContainer<MonthSection>
|
||||||
|
{
|
||||||
|
AutoSizeAxes = Axes.Y,
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
Direction = FillDirection.Vertical,
|
||||||
|
Spacing = new Vector2(0, 10)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -81,6 +81,9 @@ namespace osu.Game.Overlays.News.Sidebar
|
|||||||
{
|
{
|
||||||
public int Year { get; }
|
public int Year { get; }
|
||||||
|
|
||||||
|
[Resolved(canBeNull: true)]
|
||||||
|
private NewsOverlay overlay { get; set; }
|
||||||
|
|
||||||
private readonly bool isCurrent;
|
private readonly bool isCurrent;
|
||||||
|
|
||||||
public YearButton(int year, bool isCurrent)
|
public YearButton(int year, bool isCurrent)
|
||||||
@ -106,7 +109,11 @@ namespace osu.Game.Overlays.News.Sidebar
|
|||||||
{
|
{
|
||||||
IdleColour = isCurrent ? Color4.White : colourProvider.Light2;
|
IdleColour = isCurrent ? Color4.White : colourProvider.Light2;
|
||||||
HoverColour = isCurrent ? Color4.White : colourProvider.Light1;
|
HoverColour = isCurrent ? Color4.White : colourProvider.Light1;
|
||||||
Action = () => { }; // Avoid button being disabled since there's no proper action assigned.
|
Action = () =>
|
||||||
|
{
|
||||||
|
if (!isCurrent)
|
||||||
|
overlay?.ShowYear(Year);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
// 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.Threading;
|
using System.Threading;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Game.Overlays.News;
|
using osu.Game.Overlays.News;
|
||||||
using osu.Game.Overlays.News.Displays;
|
using osu.Game.Overlays.News.Displays;
|
||||||
|
using osu.Game.Overlays.News.Sidebar;
|
||||||
|
|
||||||
namespace osu.Game.Overlays
|
namespace osu.Game.Overlays
|
||||||
{
|
{
|
||||||
@ -13,9 +16,48 @@ namespace osu.Game.Overlays
|
|||||||
{
|
{
|
||||||
private readonly Bindable<string> article = new Bindable<string>(null);
|
private readonly Bindable<string> article = new Bindable<string>(null);
|
||||||
|
|
||||||
|
private readonly Container sidebarContainer;
|
||||||
|
private readonly NewsSidebar sidebar;
|
||||||
|
|
||||||
|
private readonly Container content;
|
||||||
|
|
||||||
|
private CancellationTokenSource cancellationToken;
|
||||||
|
|
||||||
|
private bool displayUpdateRequired = true;
|
||||||
|
|
||||||
public NewsOverlay()
|
public NewsOverlay()
|
||||||
: base(OverlayColourScheme.Purple, false)
|
: base(OverlayColourScheme.Purple, false)
|
||||||
{
|
{
|
||||||
|
Child = new GridContainer
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
AutoSizeAxes = Axes.Y,
|
||||||
|
RowDimensions = new[]
|
||||||
|
{
|
||||||
|
new Dimension(GridSizeMode.AutoSize)
|
||||||
|
},
|
||||||
|
ColumnDimensions = new[]
|
||||||
|
{
|
||||||
|
new Dimension(GridSizeMode.AutoSize),
|
||||||
|
new Dimension()
|
||||||
|
},
|
||||||
|
Content = new[]
|
||||||
|
{
|
||||||
|
new Drawable[]
|
||||||
|
{
|
||||||
|
sidebarContainer = new Container
|
||||||
|
{
|
||||||
|
AutoSizeAxes = Axes.X,
|
||||||
|
Child = sidebar = new NewsSidebar()
|
||||||
|
},
|
||||||
|
content = new Container
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
AutoSizeAxes = Axes.Y
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
@ -26,12 +68,7 @@ namespace osu.Game.Overlays
|
|||||||
article.BindValueChanged(onArticleChanged);
|
article.BindValueChanged(onArticleChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override NewsHeader CreateHeader() => new NewsHeader
|
protected override NewsHeader CreateHeader() => new NewsHeader { ShowFrontPage = ShowFrontPage };
|
||||||
{
|
|
||||||
ShowFrontPage = ShowFrontPage
|
|
||||||
};
|
|
||||||
|
|
||||||
private bool displayUpdateRequired = true;
|
|
||||||
|
|
||||||
protected override void PopIn()
|
protected override void PopIn()
|
||||||
{
|
{
|
||||||
@ -56,38 +93,69 @@ namespace osu.Game.Overlays
|
|||||||
Show();
|
Show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void ShowYear(int year)
|
||||||
|
{
|
||||||
|
loadFrontPage(year);
|
||||||
|
Show();
|
||||||
|
}
|
||||||
|
|
||||||
public void ShowArticle(string slug)
|
public void ShowArticle(string slug)
|
||||||
{
|
{
|
||||||
article.Value = slug;
|
article.Value = slug;
|
||||||
Show();
|
Show();
|
||||||
}
|
}
|
||||||
|
|
||||||
private CancellationTokenSource cancellationToken;
|
|
||||||
|
|
||||||
private void onArticleChanged(ValueChangedEvent<string> e)
|
|
||||||
{
|
|
||||||
cancellationToken?.Cancel();
|
|
||||||
Loading.Show();
|
|
||||||
|
|
||||||
if (e.NewValue == null)
|
|
||||||
{
|
|
||||||
Header.SetFrontPage();
|
|
||||||
LoadDisplay(new FrontPageDisplay());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Header.SetArticle(e.NewValue);
|
|
||||||
LoadDisplay(Empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void LoadDisplay(Drawable display)
|
protected void LoadDisplay(Drawable display)
|
||||||
{
|
{
|
||||||
ScrollFlow.ScrollToStart();
|
ScrollFlow.ScrollToStart();
|
||||||
LoadComponentAsync(display, loaded =>
|
LoadComponentAsync(display, loaded => content.Child = loaded, (cancellationToken = new CancellationTokenSource()).Token);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void UpdateAfterChildren()
|
||||||
|
{
|
||||||
|
base.UpdateAfterChildren();
|
||||||
|
sidebarContainer.Height = DrawHeight;
|
||||||
|
sidebarContainer.Y = Math.Clamp(ScrollFlow.Current - Header.DrawHeight, 0, Math.Max(ScrollFlow.ScrollContent.DrawHeight - DrawHeight - Header.DrawHeight, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onArticleChanged(ValueChangedEvent<string> article)
|
||||||
|
{
|
||||||
|
if (article.NewValue == null)
|
||||||
|
loadFrontPage();
|
||||||
|
else
|
||||||
|
loadArticle(article.NewValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadFrontPage(int? year = null)
|
||||||
|
{
|
||||||
|
beginLoading();
|
||||||
|
|
||||||
|
Header.SetFrontPage();
|
||||||
|
|
||||||
|
var page = new ArticleListing(year);
|
||||||
|
page.SidebarMetadataUpdated += metadata => Schedule(() =>
|
||||||
{
|
{
|
||||||
Child = loaded;
|
sidebar.Metadata.Value = metadata;
|
||||||
Loading.Hide();
|
Loading.Hide();
|
||||||
}, (cancellationToken = new CancellationTokenSource()).Token);
|
});
|
||||||
|
LoadDisplay(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadArticle(string article)
|
||||||
|
{
|
||||||
|
beginLoading();
|
||||||
|
|
||||||
|
Header.SetArticle(article);
|
||||||
|
|
||||||
|
// Temporary, should be handled by ArticleDisplay later
|
||||||
|
LoadDisplay(Empty());
|
||||||
|
Loading.Hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void beginLoading()
|
||||||
|
{
|
||||||
|
cancellationToken?.Cancel();
|
||||||
|
Loading.Show();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Dispose(bool isDisposing)
|
protected override void Dispose(bool isDisposing)
|
||||||
|
@ -73,11 +73,6 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay
|
|||||||
LabelText = "Always play first combo break sound",
|
LabelText = "Always play first combo break sound",
|
||||||
Current = config.GetBindable<bool>(OsuSetting.AlwaysPlayFirstComboBreak)
|
Current = config.GetBindable<bool>(OsuSetting.AlwaysPlayFirstComboBreak)
|
||||||
},
|
},
|
||||||
new SettingsEnumDropdown<ScoreMeterType>
|
|
||||||
{
|
|
||||||
LabelText = "Score meter type",
|
|
||||||
Current = config.GetBindable<ScoreMeterType>(OsuSetting.ScoreMeter)
|
|
||||||
},
|
|
||||||
new SettingsEnumDropdown<ScoringMode>
|
new SettingsEnumDropdown<ScoringMode>
|
||||||
{
|
{
|
||||||
LabelText = "Score display mode",
|
LabelText = "Score display mode",
|
||||||
|
@ -36,7 +36,8 @@ namespace osu.Game.Rulesets.Edit
|
|||||||
[Resolved(canBeNull: true)]
|
[Resolved(canBeNull: true)]
|
||||||
protected EditorClock EditorClock { get; private set; }
|
protected EditorClock EditorClock { get; private set; }
|
||||||
|
|
||||||
private readonly IBindable<WorkingBeatmap> beatmap = new Bindable<WorkingBeatmap>();
|
[Resolved]
|
||||||
|
private EditorBeatmap beatmap { get; set; }
|
||||||
|
|
||||||
private Bindable<double> startTimeBindable;
|
private Bindable<double> startTimeBindable;
|
||||||
|
|
||||||
@ -58,10 +59,8 @@ namespace osu.Game.Rulesets.Edit
|
|||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(IBindable<WorkingBeatmap> beatmap)
|
private void load()
|
||||||
{
|
{
|
||||||
this.beatmap.BindTo(beatmap);
|
|
||||||
|
|
||||||
startTimeBindable = HitObject.StartTimeBindable.GetBoundCopy();
|
startTimeBindable = HitObject.StartTimeBindable.GetBoundCopy();
|
||||||
startTimeBindable.BindValueChanged(_ => ApplyDefaultsToHitObject(), true);
|
startTimeBindable.BindValueChanged(_ => ApplyDefaultsToHitObject(), true);
|
||||||
}
|
}
|
||||||
@ -113,7 +112,7 @@ namespace osu.Game.Rulesets.Edit
|
|||||||
/// Invokes <see cref="Objects.HitObject.ApplyDefaults(ControlPointInfo,BeatmapDifficulty, CancellationToken)"/>,
|
/// Invokes <see cref="Objects.HitObject.ApplyDefaults(ControlPointInfo,BeatmapDifficulty, CancellationToken)"/>,
|
||||||
/// refreshing <see cref="Objects.HitObject.NestedHitObjects"/> and parameters for the <see cref="HitObject"/>.
|
/// refreshing <see cref="Objects.HitObject.NestedHitObjects"/> and parameters for the <see cref="HitObject"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected void ApplyDefaultsToHitObject() => HitObject.ApplyDefaults(beatmap.Value.Beatmap.ControlPointInfo, beatmap.Value.Beatmap.BeatmapInfo.BaseDifficulty);
|
protected void ApplyDefaultsToHitObject() => HitObject.ApplyDefaults(beatmap.ControlPointInfo, beatmap.BeatmapInfo.BaseDifficulty);
|
||||||
|
|
||||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Parent?.ReceivePositionalInputAt(screenSpacePos) ?? false;
|
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Parent?.ReceivePositionalInputAt(screenSpacePos) ?? false;
|
||||||
|
|
||||||
|
@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.UI
|
|||||||
public int RecordFrameRate = 60;
|
public int RecordFrameRate = 60;
|
||||||
|
|
||||||
[Resolved(canBeNull: true)]
|
[Resolved(canBeNull: true)]
|
||||||
private SpectatorStreamingClient spectatorStreaming { get; set; }
|
private SpectatorClient spectatorClient { get; set; }
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private GameplayBeatmap gameplayBeatmap { get; set; }
|
private GameplayBeatmap gameplayBeatmap { get; set; }
|
||||||
@ -49,13 +49,13 @@ namespace osu.Game.Rulesets.UI
|
|||||||
|
|
||||||
inputManager = GetContainingInputManager();
|
inputManager = GetContainingInputManager();
|
||||||
|
|
||||||
spectatorStreaming?.BeginPlaying(gameplayBeatmap, target);
|
spectatorClient?.BeginPlaying(gameplayBeatmap, target);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Dispose(bool isDisposing)
|
protected override void Dispose(bool isDisposing)
|
||||||
{
|
{
|
||||||
base.Dispose(isDisposing);
|
base.Dispose(isDisposing);
|
||||||
spectatorStreaming?.EndPlaying();
|
spectatorClient?.EndPlaying();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Update()
|
protected override void Update()
|
||||||
@ -98,7 +98,7 @@ namespace osu.Game.Rulesets.UI
|
|||||||
{
|
{
|
||||||
target.Replay.Frames.Add(frame);
|
target.Replay.Frames.Add(frame);
|
||||||
|
|
||||||
spectatorStreaming?.HandleFrame(frame);
|
spectatorClient?.HandleFrame(frame);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -168,13 +168,13 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
|||||||
{
|
{
|
||||||
if (SelectedBlueprints.All(b => b.Item is IHasComboInformation))
|
if (SelectedBlueprints.All(b => b.Item is IHasComboInformation))
|
||||||
{
|
{
|
||||||
yield return new TernaryStateMenuItem("New combo") { State = { BindTarget = SelectionNewComboState } };
|
yield return new TernaryStateToggleMenuItem("New combo") { State = { BindTarget = SelectionNewComboState } };
|
||||||
}
|
}
|
||||||
|
|
||||||
yield return new OsuMenuItem("Sound")
|
yield return new OsuMenuItem("Sound")
|
||||||
{
|
{
|
||||||
Items = SelectionSampleStates.Select(kvp =>
|
Items = SelectionSampleStates.Select(kvp =>
|
||||||
new TernaryStateMenuItem(kvp.Value.Description) { State = { BindTarget = kvp.Value } }).ToArray()
|
new TernaryStateToggleMenuItem(kvp.Value.Description) { State = { BindTarget = kvp.Value } }).ToArray()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
|||||||
private IBindable<bool> operationInProgress;
|
private IBindable<bool> operationInProgress;
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private StatefulMultiplayerClient multiplayerClient { get; set; }
|
private MultiplayerClient multiplayerClient { get; set; }
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private OngoingOperationTracker ongoingOperationTracker { get; set; }
|
private OngoingOperationTracker ongoingOperationTracker { get; set; }
|
||||||
|
@ -59,7 +59,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
|||||||
private IRoomManager manager { get; set; }
|
private IRoomManager manager { get; set; }
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private StatefulMultiplayerClient client { get; set; }
|
private MultiplayerClient client { get; set; }
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private Bindable<Room> currentRoom { get; set; }
|
private Bindable<Room> currentRoom { get; set; }
|
||||||
|
@ -15,7 +15,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
|||||||
public class Multiplayer : OnlinePlayScreen
|
public class Multiplayer : OnlinePlayScreen
|
||||||
{
|
{
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private StatefulMultiplayerClient client { get; set; }
|
private MultiplayerClient client { get; set; }
|
||||||
|
|
||||||
public override void OnResuming(IScreen last)
|
public override void OnResuming(IScreen last)
|
||||||
{
|
{
|
||||||
|
@ -18,7 +18,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
|||||||
protected override RoomSubScreen CreateRoomSubScreen(Room room) => new MultiplayerMatchSubScreen(room);
|
protected override RoomSubScreen CreateRoomSubScreen(Room room) => new MultiplayerMatchSubScreen(room);
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private StatefulMultiplayerClient client { get; set; }
|
private MultiplayerClient client { get; set; }
|
||||||
|
|
||||||
public override void Open(Room room)
|
public override void Open(Room room)
|
||||||
{
|
{
|
||||||
|
@ -17,7 +17,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
|||||||
public class MultiplayerMatchSongSelect : OnlinePlaySongSelect
|
public class MultiplayerMatchSongSelect : OnlinePlaySongSelect
|
||||||
{
|
{
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private StatefulMultiplayerClient client { get; set; }
|
private MultiplayerClient client { get; set; }
|
||||||
|
|
||||||
private LoadingLayer loadingLayer;
|
private LoadingLayer loadingLayer;
|
||||||
|
|
||||||
|
@ -43,7 +43,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
|||||||
public override string ShortTitle => "room";
|
public override string ShortTitle => "room";
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private StatefulMultiplayerClient client { get; set; }
|
private MultiplayerClient client { get; set; }
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private OngoingOperationTracker ongoingOperationTracker { get; set; }
|
private OngoingOperationTracker ongoingOperationTracker { get; set; }
|
||||||
|
@ -26,7 +26,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
|||||||
protected override bool CheckModsAllowFailure() => false;
|
protected override bool CheckModsAllowFailure() => false;
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private StatefulMultiplayerClient client { get; set; }
|
private MultiplayerClient client { get; set; }
|
||||||
|
|
||||||
private IBindable<bool> isConnected;
|
private IBindable<bool> isConnected;
|
||||||
|
|
||||||
|
@ -13,7 +13,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
|||||||
protected MultiplayerRoom Room => Client.Room;
|
protected MultiplayerRoom Room => Client.Room;
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
protected StatefulMultiplayerClient Client { get; private set; }
|
protected MultiplayerClient Client { get; private set; }
|
||||||
|
|
||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
{
|
{
|
||||||
|
@ -19,7 +19,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
|||||||
public class MultiplayerRoomManager : RoomManager
|
public class MultiplayerRoomManager : RoomManager
|
||||||
{
|
{
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private StatefulMultiplayerClient multiplayerClient { get; set; }
|
private MultiplayerClient multiplayerClient { get; set; }
|
||||||
|
|
||||||
public readonly Bindable<double> TimeBetweenListingPolls = new Bindable<double>();
|
public readonly Bindable<double> TimeBetweenListingPolls = new Bindable<double>();
|
||||||
public readonly Bindable<double> TimeBetweenSelectionPolls = new Bindable<double>();
|
public readonly Bindable<double> TimeBetweenSelectionPolls = new Bindable<double>();
|
||||||
|
@ -10,7 +10,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
|
|||||||
public class ParticipantsListHeader : OverlinedHeader
|
public class ParticipantsListHeader : OverlinedHeader
|
||||||
{
|
{
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private StatefulMultiplayerClient client { get; set; }
|
private MultiplayerClient client { get; set; }
|
||||||
|
|
||||||
public ParticipantsListHeader()
|
public ParticipantsListHeader()
|
||||||
: base("Participants")
|
: base("Participants")
|
||||||
|
@ -28,10 +28,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
|||||||
public bool AllPlayersLoaded => instances.All(p => p?.PlayerLoaded == true);
|
public bool AllPlayersLoaded => instances.All(p => p?.PlayerLoaded == true);
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private SpectatorStreamingClient spectatorClient { get; set; }
|
private SpectatorClient spectatorClient { get; set; }
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private StatefulMultiplayerClient multiplayerClient { get; set; }
|
private MultiplayerClient multiplayerClient { get; set; }
|
||||||
|
|
||||||
private readonly PlayerArea[] instances;
|
private readonly PlayerArea[] instances;
|
||||||
private MasterGameplayClockContainer masterClockContainer;
|
private MasterGameplayClockContainer masterClockContainer;
|
||||||
|
@ -1,127 +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.Allocation;
|
|
||||||
using osu.Framework.Bindables;
|
|
||||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
|
||||||
using osu.Framework.Graphics;
|
|
||||||
using osu.Framework.Graphics.Containers;
|
|
||||||
using osu.Game.Configuration;
|
|
||||||
using osu.Game.Rulesets.Scoring;
|
|
||||||
using osu.Game.Screens.Play.HUD.HitErrorMeters;
|
|
||||||
|
|
||||||
namespace osu.Game.Screens.Play.HUD
|
|
||||||
{
|
|
||||||
public class HitErrorDisplay : Container<HitErrorMeter>
|
|
||||||
{
|
|
||||||
private const int fade_duration = 200;
|
|
||||||
private const int margin = 10;
|
|
||||||
|
|
||||||
private readonly Bindable<ScoreMeterType> type = new Bindable<ScoreMeterType>();
|
|
||||||
|
|
||||||
private readonly HitWindows hitWindows;
|
|
||||||
|
|
||||||
public HitErrorDisplay(HitWindows hitWindows)
|
|
||||||
{
|
|
||||||
this.hitWindows = hitWindows;
|
|
||||||
|
|
||||||
RelativeSizeAxes = Axes.Both;
|
|
||||||
}
|
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
|
||||||
private void load(OsuConfigManager config)
|
|
||||||
{
|
|
||||||
config.BindWith(OsuSetting.ScoreMeter, type);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void LoadComplete()
|
|
||||||
{
|
|
||||||
base.LoadComplete();
|
|
||||||
type.BindValueChanged(typeChanged, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void typeChanged(ValueChangedEvent<ScoreMeterType> type)
|
|
||||||
{
|
|
||||||
Children.ForEach(c => c.FadeOut(fade_duration, Easing.OutQuint));
|
|
||||||
|
|
||||||
if (hitWindows == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
switch (type.NewValue)
|
|
||||||
{
|
|
||||||
case ScoreMeterType.HitErrorBoth:
|
|
||||||
createBar(Anchor.CentreLeft);
|
|
||||||
createBar(Anchor.CentreRight);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case ScoreMeterType.HitErrorLeft:
|
|
||||||
createBar(Anchor.CentreLeft);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case ScoreMeterType.HitErrorRight:
|
|
||||||
createBar(Anchor.CentreRight);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case ScoreMeterType.HitErrorBottom:
|
|
||||||
createBar(Anchor.BottomCentre);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case ScoreMeterType.ColourBoth:
|
|
||||||
createColour(Anchor.CentreLeft);
|
|
||||||
createColour(Anchor.CentreRight);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case ScoreMeterType.ColourLeft:
|
|
||||||
createColour(Anchor.CentreLeft);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case ScoreMeterType.ColourRight:
|
|
||||||
createColour(Anchor.CentreRight);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case ScoreMeterType.ColourBottom:
|
|
||||||
createColour(Anchor.BottomCentre);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void createBar(Anchor anchor)
|
|
||||||
{
|
|
||||||
bool rightAligned = (anchor & Anchor.x2) > 0;
|
|
||||||
bool bottomAligned = (anchor & Anchor.y2) > 0;
|
|
||||||
|
|
||||||
var display = new BarHitErrorMeter(hitWindows, rightAligned)
|
|
||||||
{
|
|
||||||
Margin = new MarginPadding(margin),
|
|
||||||
Anchor = anchor,
|
|
||||||
Origin = bottomAligned ? Anchor.CentreLeft : anchor,
|
|
||||||
Alpha = 0,
|
|
||||||
Rotation = bottomAligned ? 270 : 0
|
|
||||||
};
|
|
||||||
|
|
||||||
completeDisplayLoading(display);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void createColour(Anchor anchor)
|
|
||||||
{
|
|
||||||
bool bottomAligned = (anchor & Anchor.y2) > 0;
|
|
||||||
|
|
||||||
var display = new ColourHitErrorMeter(hitWindows)
|
|
||||||
{
|
|
||||||
Margin = new MarginPadding(margin),
|
|
||||||
Anchor = anchor,
|
|
||||||
Origin = bottomAligned ? Anchor.CentreLeft : anchor,
|
|
||||||
Alpha = 0,
|
|
||||||
Rotation = bottomAligned ? 270 : 0
|
|
||||||
};
|
|
||||||
|
|
||||||
completeDisplayLoading(display);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void completeDisplayLoading(HitErrorMeter display)
|
|
||||||
{
|
|
||||||
Add(display);
|
|
||||||
display.FadeInFromZero(fade_duration, Easing.OutQuint);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -20,8 +20,6 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
|
|||||||
{
|
{
|
||||||
public class BarHitErrorMeter : HitErrorMeter
|
public class BarHitErrorMeter : HitErrorMeter
|
||||||
{
|
{
|
||||||
private readonly Anchor alignment;
|
|
||||||
|
|
||||||
private const int arrow_move_duration = 400;
|
private const int arrow_move_duration = 400;
|
||||||
|
|
||||||
private const int judgement_line_width = 6;
|
private const int judgement_line_width = 6;
|
||||||
@ -43,11 +41,8 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
|
|||||||
|
|
||||||
private double maxHitWindow;
|
private double maxHitWindow;
|
||||||
|
|
||||||
public BarHitErrorMeter(HitWindows hitWindows, bool rightAligned = false)
|
public BarHitErrorMeter()
|
||||||
: base(hitWindows)
|
|
||||||
{
|
{
|
||||||
alignment = rightAligned ? Anchor.x0 : Anchor.x2;
|
|
||||||
|
|
||||||
AutoSizeAxes = Axes.Both;
|
AutoSizeAxes = Axes.Both;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,33 +58,42 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
|
|||||||
Margin = new MarginPadding(2),
|
Margin = new MarginPadding(2),
|
||||||
Children = new Drawable[]
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
judgementsContainer = new Container
|
new Container
|
||||||
{
|
{
|
||||||
Anchor = Anchor.y1 | alignment,
|
Anchor = Anchor.CentreLeft,
|
||||||
Origin = Anchor.y1 | alignment,
|
Origin = Anchor.CentreLeft,
|
||||||
Width = judgement_line_width,
|
Width = chevron_size,
|
||||||
RelativeSizeAxes = Axes.Y,
|
RelativeSizeAxes = Axes.Y,
|
||||||
|
Child = arrow = new SpriteIcon
|
||||||
|
{
|
||||||
|
Anchor = Anchor.TopCentre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
RelativePositionAxes = Axes.Y,
|
||||||
|
Y = 0.5f,
|
||||||
|
Icon = FontAwesome.Solid.ChevronRight,
|
||||||
|
Size = new Vector2(chevron_size),
|
||||||
|
}
|
||||||
},
|
},
|
||||||
colourBars = new Container
|
colourBars = new Container
|
||||||
{
|
{
|
||||||
Width = bar_width,
|
Width = bar_width,
|
||||||
RelativeSizeAxes = Axes.Y,
|
RelativeSizeAxes = Axes.Y,
|
||||||
Anchor = Anchor.y1 | alignment,
|
Anchor = Anchor.CentreLeft,
|
||||||
Origin = Anchor.y1 | alignment,
|
Origin = Anchor.CentreLeft,
|
||||||
Children = new Drawable[]
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
colourBarsEarly = new Container
|
colourBarsEarly = new Container
|
||||||
{
|
{
|
||||||
Anchor = Anchor.y1 | alignment,
|
Anchor = Anchor.CentreLeft,
|
||||||
Origin = alignment,
|
Origin = Anchor.TopRight,
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Height = 0.5f,
|
Height = 0.5f,
|
||||||
Scale = new Vector2(1, -1),
|
Scale = new Vector2(1, -1),
|
||||||
},
|
},
|
||||||
colourBarsLate = new Container
|
colourBarsLate = new Container
|
||||||
{
|
{
|
||||||
Anchor = Anchor.y1 | alignment,
|
Anchor = Anchor.CentreLeft,
|
||||||
Origin = alignment,
|
Origin = Anchor.TopRight,
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Height = 0.5f,
|
Height = 0.5f,
|
||||||
},
|
},
|
||||||
@ -115,21 +119,12 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
new Container
|
judgementsContainer = new Container
|
||||||
{
|
{
|
||||||
Anchor = Anchor.y1 | alignment,
|
Anchor = Anchor.CentreLeft,
|
||||||
Origin = Anchor.y1 | alignment,
|
Origin = Anchor.CentreLeft,
|
||||||
Width = chevron_size,
|
Width = judgement_line_width,
|
||||||
RelativeSizeAxes = Axes.Y,
|
RelativeSizeAxes = Axes.Y,
|
||||||
Child = arrow = new SpriteIcon
|
|
||||||
{
|
|
||||||
Anchor = Anchor.TopCentre,
|
|
||||||
Origin = Anchor.Centre,
|
|
||||||
RelativePositionAxes = Axes.Y,
|
|
||||||
Y = 0.5f,
|
|
||||||
Icon = alignment == Anchor.x2 ? FontAwesome.Solid.ChevronRight : FontAwesome.Solid.ChevronLeft,
|
|
||||||
Size = new Vector2(chevron_size),
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -152,19 +147,22 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
|
|||||||
{
|
{
|
||||||
var windows = HitWindows.GetAllAvailableWindows().ToArray();
|
var windows = HitWindows.GetAllAvailableWindows().ToArray();
|
||||||
|
|
||||||
maxHitWindow = windows.First().length;
|
// max to avoid div-by-zero.
|
||||||
|
maxHitWindow = Math.Max(1, windows.First().length);
|
||||||
|
|
||||||
for (var i = 0; i < windows.Length; i++)
|
for (var i = 0; i < windows.Length; i++)
|
||||||
{
|
{
|
||||||
var (result, length) = windows[i];
|
var (result, length) = windows[i];
|
||||||
|
|
||||||
colourBarsEarly.Add(createColourBar(result, (float)(length / maxHitWindow), i == 0));
|
var hitWindow = (float)(length / maxHitWindow);
|
||||||
colourBarsLate.Add(createColourBar(result, (float)(length / maxHitWindow), i == 0));
|
|
||||||
|
colourBarsEarly.Add(createColourBar(result, hitWindow, i == 0));
|
||||||
|
colourBarsLate.Add(createColourBar(result, hitWindow, i == 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
// a little nub to mark the centre point.
|
// a little nub to mark the centre point.
|
||||||
var centre = createColourBar(windows.Last().result, 0.01f);
|
var centre = createColourBar(windows.Last().result, 0.01f);
|
||||||
centre.Anchor = centre.Origin = Anchor.y1 | (alignment == Anchor.x2 ? Anchor.x0 : Anchor.x2);
|
centre.Anchor = centre.Origin = Anchor.CentreLeft;
|
||||||
centre.Width = 2.5f;
|
centre.Width = 2.5f;
|
||||||
colourBars.Add(centre);
|
colourBars.Add(centre);
|
||||||
|
|
||||||
@ -236,8 +234,7 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
|
|||||||
judgementsContainer.Add(new JudgementLine
|
judgementsContainer.Add(new JudgementLine
|
||||||
{
|
{
|
||||||
Y = getRelativeJudgementPosition(judgement.TimeOffset),
|
Y = getRelativeJudgementPosition(judgement.TimeOffset),
|
||||||
Anchor = alignment == Anchor.x2 ? Anchor.x0 : Anchor.x2,
|
Origin = Anchor.CentreLeft,
|
||||||
Origin = Anchor.y1 | (alignment == Anchor.x2 ? Anchor.x0 : Anchor.x2),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
arrow.MoveToY(
|
arrow.MoveToY(
|
||||||
|
@ -7,7 +7,6 @@ using osu.Framework.Graphics;
|
|||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Shapes;
|
using osu.Framework.Graphics.Shapes;
|
||||||
using osu.Game.Rulesets.Judgements;
|
using osu.Game.Rulesets.Judgements;
|
||||||
using osu.Game.Rulesets.Scoring;
|
|
||||||
using osuTK;
|
using osuTK;
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
|
|
||||||
@ -19,8 +18,7 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
|
|||||||
|
|
||||||
private readonly JudgementFlow judgementsFlow;
|
private readonly JudgementFlow judgementsFlow;
|
||||||
|
|
||||||
public ColourHitErrorMeter(HitWindows hitWindows)
|
public ColourHitErrorMeter()
|
||||||
: base(hitWindows)
|
|
||||||
{
|
{
|
||||||
AutoSizeAxes = Axes.Both;
|
AutoSizeAxes = Axes.Both;
|
||||||
InternalChild = judgementsFlow = new JudgementFlow();
|
InternalChild = judgementsFlow = new JudgementFlow();
|
||||||
|
@ -6,13 +6,15 @@ using osu.Framework.Graphics.Containers;
|
|||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Rulesets.Judgements;
|
using osu.Game.Rulesets.Judgements;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
using osu.Game.Rulesets.UI;
|
||||||
|
using osu.Game.Skinning;
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
|
|
||||||
namespace osu.Game.Screens.Play.HUD.HitErrorMeters
|
namespace osu.Game.Screens.Play.HUD.HitErrorMeters
|
||||||
{
|
{
|
||||||
public abstract class HitErrorMeter : CompositeDrawable
|
public abstract class HitErrorMeter : CompositeDrawable, ISkinnableDrawable
|
||||||
{
|
{
|
||||||
protected readonly HitWindows HitWindows;
|
protected HitWindows HitWindows { get; private set; }
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private ScoreProcessor processor { get; set; }
|
private ScoreProcessor processor { get; set; }
|
||||||
@ -20,9 +22,10 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
|
|||||||
[Resolved]
|
[Resolved]
|
||||||
private OsuColour colours { get; set; }
|
private OsuColour colours { get; set; }
|
||||||
|
|
||||||
protected HitErrorMeter(HitWindows hitWindows)
|
[BackgroundDependencyLoader(true)]
|
||||||
|
private void load(DrawableRuleset drawableRuleset)
|
||||||
{
|
{
|
||||||
HitWindows = hitWindows;
|
HitWindows = drawableRuleset?.FirstAvailableHitWindows ?? HitWindows.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
|
@ -22,10 +22,10 @@ namespace osu.Game.Screens.Play.HUD
|
|||||||
protected readonly Dictionary<int, TrackedUserData> UserScores = new Dictionary<int, TrackedUserData>();
|
protected readonly Dictionary<int, TrackedUserData> UserScores = new Dictionary<int, TrackedUserData>();
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private SpectatorStreamingClient streamingClient { get; set; }
|
private SpectatorClient spectatorClient { get; set; }
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private StatefulMultiplayerClient multiplayerClient { get; set; }
|
private MultiplayerClient multiplayerClient { get; set; }
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private UserLookupCache userLookupCache { get; set; }
|
private UserLookupCache userLookupCache { get; set; }
|
||||||
@ -55,8 +55,6 @@ namespace osu.Game.Screens.Play.HUD
|
|||||||
|
|
||||||
foreach (var userId in playingUsers)
|
foreach (var userId in playingUsers)
|
||||||
{
|
{
|
||||||
streamingClient.WatchUser(userId);
|
|
||||||
|
|
||||||
// probably won't be required in the final implementation.
|
// probably won't be required in the final implementation.
|
||||||
var resolvedUser = userLookupCache.GetUserAsync(userId).Result;
|
var resolvedUser = userLookupCache.GetUserAsync(userId).Result;
|
||||||
|
|
||||||
@ -80,6 +78,8 @@ namespace osu.Game.Screens.Play.HUD
|
|||||||
// BindableList handles binding in a really bad way (Clear then AddRange) so we need to do this manually..
|
// BindableList handles binding in a really bad way (Clear then AddRange) so we need to do this manually..
|
||||||
foreach (int userId in playingUsers)
|
foreach (int userId in playingUsers)
|
||||||
{
|
{
|
||||||
|
spectatorClient.WatchUser(userId);
|
||||||
|
|
||||||
if (!multiplayerClient.CurrentMatchPlayingUserIds.Contains(userId))
|
if (!multiplayerClient.CurrentMatchPlayingUserIds.Contains(userId))
|
||||||
usersChanged(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, new[] { userId }));
|
usersChanged(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, new[] { userId }));
|
||||||
}
|
}
|
||||||
@ -88,7 +88,7 @@ namespace osu.Game.Screens.Play.HUD
|
|||||||
playingUsers.BindCollectionChanged(usersChanged);
|
playingUsers.BindCollectionChanged(usersChanged);
|
||||||
|
|
||||||
// this leaderboard should be guaranteed to be completely loaded before the gameplay starts (is a prerequisite in MultiplayerPlayer).
|
// this leaderboard should be guaranteed to be completely loaded before the gameplay starts (is a prerequisite in MultiplayerPlayer).
|
||||||
streamingClient.OnNewFrames += handleIncomingFrames;
|
spectatorClient.OnNewFrames += handleIncomingFrames;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void usersChanged(object sender, NotifyCollectionChangedEventArgs e)
|
private void usersChanged(object sender, NotifyCollectionChangedEventArgs e)
|
||||||
@ -98,7 +98,7 @@ namespace osu.Game.Screens.Play.HUD
|
|||||||
case NotifyCollectionChangedAction.Remove:
|
case NotifyCollectionChangedAction.Remove:
|
||||||
foreach (var userId in e.OldItems.OfType<int>())
|
foreach (var userId in e.OldItems.OfType<int>())
|
||||||
{
|
{
|
||||||
streamingClient.StopWatchingUser(userId);
|
spectatorClient.StopWatchingUser(userId);
|
||||||
|
|
||||||
if (UserScores.TryGetValue(userId, out var trackedData))
|
if (UserScores.TryGetValue(userId, out var trackedData))
|
||||||
trackedData.MarkUserQuit();
|
trackedData.MarkUserQuit();
|
||||||
@ -123,14 +123,14 @@ namespace osu.Game.Screens.Play.HUD
|
|||||||
{
|
{
|
||||||
base.Dispose(isDisposing);
|
base.Dispose(isDisposing);
|
||||||
|
|
||||||
if (streamingClient != null)
|
if (spectatorClient != null)
|
||||||
{
|
{
|
||||||
foreach (var user in playingUsers)
|
foreach (var user in playingUsers)
|
||||||
{
|
{
|
||||||
streamingClient.StopWatchingUser(user);
|
spectatorClient.StopWatchingUser(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
streamingClient.OnNewFrames -= handleIncomingFrames;
|
spectatorClient.OnNewFrames -= handleIncomingFrames;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,22 +87,10 @@ namespace osu.Game.Screens.Play
|
|||||||
visibilityContainer = new Container
|
visibilityContainer = new Container
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Children = new Drawable[]
|
Child = mainComponents = new SkinnableTargetContainer(SkinnableTarget.MainHUDComponents)
|
||||||
{
|
{
|
||||||
mainComponents = new SkinnableTargetContainer(SkinnableTarget.MainHUDComponents)
|
RelativeSizeAxes = Axes.Both,
|
||||||
{
|
},
|
||||||
RelativeSizeAxes = Axes.Both,
|
|
||||||
},
|
|
||||||
new Container
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.Both,
|
|
||||||
Children = new Drawable[]
|
|
||||||
{
|
|
||||||
// still need to be migrated; a bit more involved.
|
|
||||||
new HitErrorDisplay(this.drawableRuleset?.FirstAvailableHitWindows),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
topRightElements = new FillFlowContainer
|
topRightElements = new FillFlowContainer
|
||||||
{
|
{
|
||||||
|
@ -22,6 +22,7 @@ using osu.Game.Configuration;
|
|||||||
using osu.Game.Graphics.Containers;
|
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.Online.Spectator;
|
||||||
using osu.Game.Overlays;
|
using osu.Game.Overlays;
|
||||||
using osu.Game.Replays;
|
using osu.Game.Replays;
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
@ -93,6 +94,9 @@ namespace osu.Game.Screens.Play
|
|||||||
[Resolved]
|
[Resolved]
|
||||||
private MusicController musicController { get; set; }
|
private MusicController musicController { get; set; }
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private SpectatorClient spectatorClient { get; set; }
|
||||||
|
|
||||||
private Sample sampleRestart;
|
private Sample sampleRestart;
|
||||||
|
|
||||||
public BreakOverlay BreakOverlay;
|
public BreakOverlay BreakOverlay;
|
||||||
@ -882,6 +886,11 @@ namespace osu.Game.Screens.Play
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EndPlaying() is typically called from ReplayRecorder.Dispose(). Disposal is currently asynchronous.
|
||||||
|
// To resolve test failures, forcefully end playing synchronously when this screen exits.
|
||||||
|
// Todo: Replace this with a more permanent solution once osu-framework has a synchronous cleanup method.
|
||||||
|
spectatorClient.EndPlaying();
|
||||||
|
|
||||||
// GameplayClockContainer performs seeks / start / stop operations on the beatmap's track.
|
// GameplayClockContainer performs seeks / start / stop operations on the beatmap's track.
|
||||||
// as we are no longer the current screen, we cannot guarantee the track is still usable.
|
// as we are no longer the current screen, we cannot guarantee the track is still usable.
|
||||||
(GameplayClockContainer as MasterGameplayClockContainer)?.StopUsingBeatmapClock();
|
(GameplayClockContainer as MasterGameplayClockContainer)?.StopUsingBeatmapClock();
|
||||||
|
@ -20,10 +20,14 @@ namespace osu.Game.Screens.Play
|
|||||||
{
|
{
|
||||||
public class SongProgress : OverlayContainer, ISkinnableDrawable
|
public class SongProgress : OverlayContainer, ISkinnableDrawable
|
||||||
{
|
{
|
||||||
private const int info_height = 20;
|
public const float MAX_HEIGHT = info_height + bottom_bar_height + graph_height + handle_height;
|
||||||
private const int bottom_bar_height = 5;
|
|
||||||
|
private const float info_height = 20;
|
||||||
|
private const float bottom_bar_height = 5;
|
||||||
private const float graph_height = SquareGraph.Column.WIDTH * 6;
|
private const float graph_height = SquareGraph.Column.WIDTH * 6;
|
||||||
private static readonly Vector2 handle_size = new Vector2(10, 18);
|
private const float handle_height = 18;
|
||||||
|
|
||||||
|
private static readonly Vector2 handle_size = new Vector2(10, handle_height);
|
||||||
|
|
||||||
private const float transition_duration = 200;
|
private const float transition_duration = 200;
|
||||||
|
|
||||||
|
@ -31,12 +31,12 @@ namespace osu.Game.Screens.Play
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private SpectatorStreamingClient spectatorStreaming { get; set; }
|
private SpectatorClient spectatorClient { get; set; }
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load()
|
private void load()
|
||||||
{
|
{
|
||||||
spectatorStreaming.OnUserBeganPlaying += userBeganPlaying;
|
spectatorClient.OnUserBeganPlaying += userBeganPlaying;
|
||||||
|
|
||||||
AddInternal(new OsuSpriteText
|
AddInternal(new OsuSpriteText
|
||||||
{
|
{
|
||||||
@ -66,7 +66,7 @@ namespace osu.Game.Screens.Play
|
|||||||
|
|
||||||
public override bool OnExiting(IScreen next)
|
public override bool OnExiting(IScreen next)
|
||||||
{
|
{
|
||||||
spectatorStreaming.OnUserBeganPlaying -= userBeganPlaying;
|
spectatorClient.OnUserBeganPlaying -= userBeganPlaying;
|
||||||
return base.OnExiting(next);
|
return base.OnExiting(next);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,8 +84,8 @@ namespace osu.Game.Screens.Play
|
|||||||
{
|
{
|
||||||
base.Dispose(isDisposing);
|
base.Dispose(isDisposing);
|
||||||
|
|
||||||
if (spectatorStreaming != null)
|
if (spectatorClient != null)
|
||||||
spectatorStreaming.OnUserBeganPlaying -= userBeganPlaying;
|
spectatorClient.OnUserBeganPlaying -= userBeganPlaying;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,12 +17,12 @@ namespace osu.Game.Screens.Play
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private SpectatorStreamingClient spectatorStreaming { get; set; }
|
private SpectatorClient spectatorClient { get; set; }
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load()
|
private void load()
|
||||||
{
|
{
|
||||||
spectatorStreaming.OnUserBeganPlaying += userBeganPlaying;
|
spectatorClient.OnUserBeganPlaying += userBeganPlaying;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void userBeganPlaying(int userId, SpectatorState state)
|
private void userBeganPlaying(int userId, SpectatorState state)
|
||||||
@ -40,8 +40,8 @@ namespace osu.Game.Screens.Play
|
|||||||
{
|
{
|
||||||
base.Dispose(isDisposing);
|
base.Dispose(isDisposing);
|
||||||
|
|
||||||
if (spectatorStreaming != null)
|
if (spectatorClient != null)
|
||||||
spectatorStreaming.OnUserBeganPlaying -= userBeganPlaying;
|
spectatorClient.OnUserBeganPlaying -= userBeganPlaying;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -250,7 +250,7 @@ namespace osu.Game.Screens.Select.Carousel
|
|||||||
else
|
else
|
||||||
state = TernaryState.False;
|
state = TernaryState.False;
|
||||||
|
|
||||||
return new TernaryStateMenuItem(collection.Name.Value, MenuItemType.Standard, s =>
|
return new TernaryStateToggleMenuItem(collection.Name.Value, MenuItemType.Standard, s =>
|
||||||
{
|
{
|
||||||
foreach (var b in beatmapSet.Beatmaps)
|
foreach (var b in beatmapSet.Beatmaps)
|
||||||
{
|
{
|
||||||
|
@ -37,14 +37,11 @@ namespace osu.Game.Screens.Spectate
|
|||||||
private RulesetStore rulesets { get; set; }
|
private RulesetStore rulesets { get; set; }
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private SpectatorStreamingClient spectatorClient { get; set; }
|
private SpectatorClient spectatorClient { get; set; }
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private UserLookupCache userLookupCache { get; set; }
|
private UserLookupCache userLookupCache { get; set; }
|
||||||
|
|
||||||
// A lock is used to synchronise access to spectator/gameplay states, since this class is a screen which may become non-current and stop receiving updates at any point.
|
|
||||||
private readonly object stateLock = new object();
|
|
||||||
|
|
||||||
private readonly Dictionary<int, User> userMap = new Dictionary<int, User>();
|
private readonly Dictionary<int, User> userMap = new Dictionary<int, User>();
|
||||||
private readonly Dictionary<int, GameplayState> gameplayStates = new Dictionary<int, GameplayState>();
|
private readonly Dictionary<int, GameplayState> gameplayStates = new Dictionary<int, GameplayState>();
|
||||||
|
|
||||||
@ -63,8 +60,11 @@ namespace osu.Game.Screens.Spectate
|
|||||||
{
|
{
|
||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
|
|
||||||
populateAllUsers().ContinueWith(_ => Schedule(() =>
|
getAllUsers().ContinueWith(users => Schedule(() =>
|
||||||
{
|
{
|
||||||
|
foreach (var u in users.Result)
|
||||||
|
userMap[u.Id] = u;
|
||||||
|
|
||||||
spectatorClient.BindUserBeganPlaying(userBeganPlaying, true);
|
spectatorClient.BindUserBeganPlaying(userBeganPlaying, true);
|
||||||
spectatorClient.OnUserFinishedPlaying += userFinishedPlaying;
|
spectatorClient.OnUserFinishedPlaying += userFinishedPlaying;
|
||||||
spectatorClient.OnNewFrames += userSentFrames;
|
spectatorClient.OnNewFrames += userSentFrames;
|
||||||
@ -72,27 +72,23 @@ namespace osu.Game.Screens.Spectate
|
|||||||
managerUpdated = beatmaps.ItemUpdated.GetBoundCopy();
|
managerUpdated = beatmaps.ItemUpdated.GetBoundCopy();
|
||||||
managerUpdated.BindValueChanged(beatmapUpdated);
|
managerUpdated.BindValueChanged(beatmapUpdated);
|
||||||
|
|
||||||
lock (stateLock)
|
foreach (var (id, _) in userMap)
|
||||||
{
|
spectatorClient.WatchUser(id);
|
||||||
foreach (var (id, _) in userMap)
|
|
||||||
spectatorClient.WatchUser(id);
|
|
||||||
}
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task populateAllUsers()
|
private Task<User[]> getAllUsers()
|
||||||
{
|
{
|
||||||
var userLookupTasks = new List<Task>();
|
var userLookupTasks = new List<Task<User>>();
|
||||||
|
|
||||||
foreach (var u in userIds)
|
foreach (var u in userIds)
|
||||||
{
|
{
|
||||||
userLookupTasks.Add(userLookupCache.GetUserAsync(u).ContinueWith(task =>
|
userLookupTasks.Add(userLookupCache.GetUserAsync(u).ContinueWith(task =>
|
||||||
{
|
{
|
||||||
if (!task.IsCompletedSuccessfully)
|
if (!task.IsCompletedSuccessfully)
|
||||||
return;
|
return null;
|
||||||
|
|
||||||
lock (stateLock)
|
return task.Result;
|
||||||
userMap[u] = task.Result;
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -104,16 +100,13 @@ namespace osu.Game.Screens.Spectate
|
|||||||
if (!e.NewValue.TryGetTarget(out var beatmapSet))
|
if (!e.NewValue.TryGetTarget(out var beatmapSet))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
lock (stateLock)
|
foreach (var (userId, _) in userMap)
|
||||||
{
|
{
|
||||||
foreach (var (userId, _) in userMap)
|
if (!spectatorClient.TryGetPlayingUserState(userId, out var userState))
|
||||||
{
|
continue;
|
||||||
if (!spectatorClient.TryGetPlayingUserState(userId, out var userState))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID == userState.BeatmapID))
|
if (beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID == userState.BeatmapID))
|
||||||
updateGameplayState(userId);
|
updateGameplayState(userId);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -122,101 +115,89 @@ namespace osu.Game.Screens.Spectate
|
|||||||
if (state.RulesetID == null || state.BeatmapID == null)
|
if (state.RulesetID == null || state.BeatmapID == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
lock (stateLock)
|
if (!userMap.ContainsKey(userId))
|
||||||
{
|
return;
|
||||||
if (!userMap.ContainsKey(userId))
|
|
||||||
return;
|
|
||||||
|
|
||||||
// The user may have stopped playing.
|
// The user may have stopped playing.
|
||||||
if (!spectatorClient.TryGetPlayingUserState(userId, out _))
|
if (!spectatorClient.TryGetPlayingUserState(userId, out _))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
Schedule(() => OnUserStateChanged(userId, state));
|
Schedule(() => OnUserStateChanged(userId, state));
|
||||||
|
|
||||||
updateGameplayState(userId);
|
updateGameplayState(userId);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateGameplayState(int userId)
|
private void updateGameplayState(int userId)
|
||||||
{
|
{
|
||||||
lock (stateLock)
|
Debug.Assert(userMap.ContainsKey(userId));
|
||||||
|
|
||||||
|
// The user may have stopped playing.
|
||||||
|
if (!spectatorClient.TryGetPlayingUserState(userId, out var spectatorState))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var user = userMap[userId];
|
||||||
|
|
||||||
|
var resolvedRuleset = rulesets.AvailableRulesets.FirstOrDefault(r => r.ID == spectatorState.RulesetID)?.CreateInstance();
|
||||||
|
if (resolvedRuleset == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var resolvedBeatmap = beatmaps.QueryBeatmap(b => b.OnlineBeatmapID == spectatorState.BeatmapID);
|
||||||
|
if (resolvedBeatmap == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var score = new Score
|
||||||
{
|
{
|
||||||
Debug.Assert(userMap.ContainsKey(userId));
|
ScoreInfo = new ScoreInfo
|
||||||
|
|
||||||
// The user may have stopped playing.
|
|
||||||
if (!spectatorClient.TryGetPlayingUserState(userId, out var spectatorState))
|
|
||||||
return;
|
|
||||||
|
|
||||||
var user = userMap[userId];
|
|
||||||
|
|
||||||
var resolvedRuleset = rulesets.AvailableRulesets.FirstOrDefault(r => r.ID == spectatorState.RulesetID)?.CreateInstance();
|
|
||||||
if (resolvedRuleset == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var resolvedBeatmap = beatmaps.QueryBeatmap(b => b.OnlineBeatmapID == spectatorState.BeatmapID);
|
|
||||||
if (resolvedBeatmap == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var score = new Score
|
|
||||||
{
|
{
|
||||||
ScoreInfo = new ScoreInfo
|
Beatmap = resolvedBeatmap,
|
||||||
{
|
User = user,
|
||||||
Beatmap = resolvedBeatmap,
|
Mods = spectatorState.Mods.Select(m => m.ToMod(resolvedRuleset)).ToArray(),
|
||||||
User = user,
|
Ruleset = resolvedRuleset.RulesetInfo,
|
||||||
Mods = spectatorState.Mods.Select(m => m.ToMod(resolvedRuleset)).ToArray(),
|
},
|
||||||
Ruleset = resolvedRuleset.RulesetInfo,
|
Replay = new Replay { HasReceivedAllFrames = false },
|
||||||
},
|
};
|
||||||
Replay = new Replay { HasReceivedAllFrames = false },
|
|
||||||
};
|
|
||||||
|
|
||||||
var gameplayState = new GameplayState(score, resolvedRuleset, beatmaps.GetWorkingBeatmap(resolvedBeatmap));
|
var gameplayState = new GameplayState(score, resolvedRuleset, beatmaps.GetWorkingBeatmap(resolvedBeatmap));
|
||||||
|
|
||||||
gameplayStates[userId] = gameplayState;
|
gameplayStates[userId] = gameplayState;
|
||||||
Schedule(() => StartGameplay(userId, gameplayState));
|
Schedule(() => StartGameplay(userId, gameplayState));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void userSentFrames(int userId, FrameDataBundle bundle)
|
private void userSentFrames(int userId, FrameDataBundle bundle)
|
||||||
{
|
{
|
||||||
lock (stateLock)
|
if (!userMap.ContainsKey(userId))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!gameplayStates.TryGetValue(userId, out var gameplayState))
|
||||||
|
return;
|
||||||
|
|
||||||
|
// The ruleset instance should be guaranteed to be in sync with the score via ScoreLock.
|
||||||
|
Debug.Assert(gameplayState.Ruleset != null && gameplayState.Ruleset.RulesetInfo.Equals(gameplayState.Score.ScoreInfo.Ruleset));
|
||||||
|
|
||||||
|
foreach (var frame in bundle.Frames)
|
||||||
{
|
{
|
||||||
if (!userMap.ContainsKey(userId))
|
IConvertibleReplayFrame convertibleFrame = gameplayState.Ruleset.CreateConvertibleReplayFrame();
|
||||||
return;
|
convertibleFrame.FromLegacy(frame, gameplayState.Beatmap.Beatmap);
|
||||||
|
|
||||||
if (!gameplayStates.TryGetValue(userId, out var gameplayState))
|
var convertedFrame = (ReplayFrame)convertibleFrame;
|
||||||
return;
|
convertedFrame.Time = frame.Time;
|
||||||
|
|
||||||
// The ruleset instance should be guaranteed to be in sync with the score via ScoreLock.
|
gameplayState.Score.Replay.Frames.Add(convertedFrame);
|
||||||
Debug.Assert(gameplayState.Ruleset != null && gameplayState.Ruleset.RulesetInfo.Equals(gameplayState.Score.ScoreInfo.Ruleset));
|
|
||||||
|
|
||||||
foreach (var frame in bundle.Frames)
|
|
||||||
{
|
|
||||||
IConvertibleReplayFrame convertibleFrame = gameplayState.Ruleset.CreateConvertibleReplayFrame();
|
|
||||||
convertibleFrame.FromLegacy(frame, gameplayState.Beatmap.Beatmap);
|
|
||||||
|
|
||||||
var convertedFrame = (ReplayFrame)convertibleFrame;
|
|
||||||
convertedFrame.Time = frame.Time;
|
|
||||||
|
|
||||||
gameplayState.Score.Replay.Frames.Add(convertedFrame);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void userFinishedPlaying(int userId, SpectatorState state)
|
private void userFinishedPlaying(int userId, SpectatorState state)
|
||||||
{
|
{
|
||||||
lock (stateLock)
|
if (!userMap.ContainsKey(userId))
|
||||||
{
|
return;
|
||||||
if (!userMap.ContainsKey(userId))
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (!gameplayStates.TryGetValue(userId, out var gameplayState))
|
if (!gameplayStates.TryGetValue(userId, out var gameplayState))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
gameplayState.Score.Replay.HasReceivedAllFrames = true;
|
gameplayState.Score.Replay.HasReceivedAllFrames = true;
|
||||||
|
|
||||||
gameplayStates.Remove(userId);
|
gameplayStates.Remove(userId);
|
||||||
Schedule(() => EndGameplay(userId));
|
Schedule(() => EndGameplay(userId));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -245,15 +226,12 @@ namespace osu.Game.Screens.Spectate
|
|||||||
/// <param name="userId">The user to stop spectating.</param>
|
/// <param name="userId">The user to stop spectating.</param>
|
||||||
protected void RemoveUser(int userId)
|
protected void RemoveUser(int userId)
|
||||||
{
|
{
|
||||||
lock (stateLock)
|
userFinishedPlaying(userId, null);
|
||||||
{
|
|
||||||
userFinishedPlaying(userId, null);
|
|
||||||
|
|
||||||
userIds.Remove(userId);
|
userIds.Remove(userId);
|
||||||
userMap.Remove(userId);
|
userMap.Remove(userId);
|
||||||
|
|
||||||
spectatorClient.StopWatchingUser(userId);
|
spectatorClient.StopWatchingUser(userId);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Dispose(bool isDisposing)
|
protected override void Dispose(bool isDisposing)
|
||||||
@ -266,11 +244,8 @@ namespace osu.Game.Screens.Spectate
|
|||||||
spectatorClient.OnUserFinishedPlaying -= userFinishedPlaying;
|
spectatorClient.OnUserFinishedPlaying -= userFinishedPlaying;
|
||||||
spectatorClient.OnNewFrames -= userSentFrames;
|
spectatorClient.OnNewFrames -= userSentFrames;
|
||||||
|
|
||||||
lock (stateLock)
|
foreach (var (userId, _) in userMap)
|
||||||
{
|
spectatorClient.StopWatchingUser(userId);
|
||||||
foreach (var (userId, _) in userMap)
|
|
||||||
spectatorClient.StopWatchingUser(userId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
managerUpdated?.UnbindAll();
|
managerUpdated?.UnbindAll();
|
||||||
|
@ -14,6 +14,7 @@ using osu.Game.Extensions;
|
|||||||
using osu.Game.IO;
|
using osu.Game.IO;
|
||||||
using osu.Game.Screens.Play;
|
using osu.Game.Screens.Play;
|
||||||
using osu.Game.Screens.Play.HUD;
|
using osu.Game.Screens.Play.HUD;
|
||||||
|
using osu.Game.Screens.Play.HUD.HitErrorMeters;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
|
|
||||||
@ -78,6 +79,24 @@ namespace osu.Game.Skinning
|
|||||||
combo.Position = new Vector2(accuracy.ScreenSpaceDeltaToParentSpace(score.ScreenSpaceDrawQuad.Size).X / 2 + horizontal_padding, vertical_offset + 5);
|
combo.Position = new Vector2(accuracy.ScreenSpaceDeltaToParentSpace(score.ScreenSpaceDrawQuad.Size).X / 2 + horizontal_padding, vertical_offset + 5);
|
||||||
combo.Anchor = Anchor.TopCentre;
|
combo.Anchor = Anchor.TopCentre;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var hitError = container.OfType<HitErrorMeter>().FirstOrDefault();
|
||||||
|
|
||||||
|
if (hitError != null)
|
||||||
|
{
|
||||||
|
hitError.Anchor = Anchor.CentreLeft;
|
||||||
|
hitError.Origin = Anchor.CentreLeft;
|
||||||
|
}
|
||||||
|
|
||||||
|
var hitError2 = container.OfType<HitErrorMeter>().LastOrDefault();
|
||||||
|
|
||||||
|
if (hitError2 != null)
|
||||||
|
{
|
||||||
|
hitError2.Anchor = Anchor.CentreRight;
|
||||||
|
hitError2.Scale = new Vector2(-1, 1);
|
||||||
|
// origin flipped to match scale above.
|
||||||
|
hitError2.Origin = Anchor.CentreLeft;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
{
|
{
|
||||||
@ -88,6 +107,8 @@ namespace osu.Game.Skinning
|
|||||||
GetDrawableComponent(new HUDSkinComponent(HUDSkinComponents.AccuracyCounter)),
|
GetDrawableComponent(new HUDSkinComponent(HUDSkinComponents.AccuracyCounter)),
|
||||||
GetDrawableComponent(new HUDSkinComponent(HUDSkinComponents.HealthDisplay)),
|
GetDrawableComponent(new HUDSkinComponent(HUDSkinComponents.HealthDisplay)),
|
||||||
GetDrawableComponent(new HUDSkinComponent(HUDSkinComponents.SongProgress)),
|
GetDrawableComponent(new HUDSkinComponent(HUDSkinComponents.SongProgress)),
|
||||||
|
GetDrawableComponent(new HUDSkinComponent(HUDSkinComponents.BarHitErrorMeter)),
|
||||||
|
GetDrawableComponent(new HUDSkinComponent(HUDSkinComponents.BarHitErrorMeter)),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -114,6 +135,12 @@ namespace osu.Game.Skinning
|
|||||||
|
|
||||||
case HUDSkinComponents.SongProgress:
|
case HUDSkinComponents.SongProgress:
|
||||||
return new SongProgress();
|
return new SongProgress();
|
||||||
|
|
||||||
|
case HUDSkinComponents.BarHitErrorMeter:
|
||||||
|
return new BarHitErrorMeter();
|
||||||
|
|
||||||
|
case HUDSkinComponents.ColourHitErrorMeter:
|
||||||
|
return new ColourHitErrorMeter();
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
@ -19,7 +19,7 @@ using osuTK;
|
|||||||
namespace osu.Game.Skinning.Editor
|
namespace osu.Game.Skinning.Editor
|
||||||
{
|
{
|
||||||
[Cached(typeof(SkinEditor))]
|
[Cached(typeof(SkinEditor))]
|
||||||
public class SkinEditor : FocusedOverlayContainer
|
public class SkinEditor : VisibilityContainer
|
||||||
{
|
{
|
||||||
public const double TRANSITION_DURATION = 500;
|
public const double TRANSITION_DURATION = 500;
|
||||||
|
|
||||||
|
@ -65,8 +65,6 @@ namespace osu.Game.Skinning.Editor
|
|||||||
if (visibility.NewValue == Visibility.Visible)
|
if (visibility.NewValue == Visibility.Visible)
|
||||||
{
|
{
|
||||||
target.Masking = true;
|
target.Masking = true;
|
||||||
target.BorderThickness = 5;
|
|
||||||
target.BorderColour = colours.Yellow;
|
|
||||||
target.AllowScaling = false;
|
target.AllowScaling = false;
|
||||||
target.RelativePositionAxes = Axes.Both;
|
target.RelativePositionAxes = Axes.Both;
|
||||||
|
|
||||||
@ -75,7 +73,6 @@ namespace osu.Game.Skinning.Editor
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
target.BorderThickness = 0;
|
|
||||||
target.AllowScaling = true;
|
target.AllowScaling = true;
|
||||||
|
|
||||||
target.ScaleTo(1, SkinEditor.TRANSITION_DURATION, Easing.OutQuint).OnComplete(_ => target.Masking = false);
|
target.ScaleTo(1, SkinEditor.TRANSITION_DURATION, Easing.OutQuint).OnComplete(_ => target.Masking = false);
|
||||||
|
@ -182,7 +182,7 @@ namespace osu.Game.Skinning.Editor
|
|||||||
foreach (var item in base.GetContextMenuItemsForSelection(selection))
|
foreach (var item in base.GetContextMenuItemsForSelection(selection))
|
||||||
yield return item;
|
yield return item;
|
||||||
|
|
||||||
IEnumerable<AnchorMenuItem> createAnchorItems(Func<Drawable, Anchor> checkFunction, Action<Anchor> applyFunction)
|
IEnumerable<TernaryStateMenuItem> createAnchorItems(Func<Drawable, Anchor> checkFunction, Action<Anchor> applyFunction)
|
||||||
{
|
{
|
||||||
var displayableAnchors = new[]
|
var displayableAnchors = new[]
|
||||||
{
|
{
|
||||||
@ -199,7 +199,7 @@ namespace osu.Game.Skinning.Editor
|
|||||||
|
|
||||||
return displayableAnchors.Select(a =>
|
return displayableAnchors.Select(a =>
|
||||||
{
|
{
|
||||||
return new AnchorMenuItem(a, selection, _ => applyFunction(a))
|
return new TernaryStateRadioMenuItem(a.ToString(), MenuItemType.Standard, _ => applyFunction(a))
|
||||||
{
|
{
|
||||||
State = { Value = GetStateFromSelection(selection, c => checkFunction((Drawable)c.Item) == a) }
|
State = { Value = GetStateFromSelection(selection, c => checkFunction((Drawable)c.Item) == a) }
|
||||||
};
|
};
|
||||||
@ -248,15 +248,5 @@ namespace osu.Game.Skinning.Editor
|
|||||||
if ((reference & Anchor.x0) > 0) scale.X = -scale.X;
|
if ((reference & Anchor.x0) > 0) scale.X = -scale.X;
|
||||||
if ((reference & Anchor.y0) > 0) scale.Y = -scale.Y;
|
if ((reference & Anchor.y0) > 0) scale.Y = -scale.Y;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class AnchorMenuItem : TernaryStateMenuItem
|
|
||||||
{
|
|
||||||
public AnchorMenuItem(Anchor anchor, IEnumerable<SelectionBlueprint<ISkinnableDrawable>> selection, Action<TernaryState> action)
|
|
||||||
: base(anchor.ToString(), getNextState, MenuItemType.Standard, action)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
private static TernaryState getNextState(TernaryState state) => TernaryState.True;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,5 +10,7 @@ namespace osu.Game.Skinning
|
|||||||
AccuracyCounter,
|
AccuracyCounter,
|
||||||
HealthDisplay,
|
HealthDisplay,
|
||||||
SongProgress,
|
SongProgress,
|
||||||
|
BarHitErrorMeter,
|
||||||
|
ColourHitErrorMeter,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,7 @@ using osu.Game.IO;
|
|||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Screens.Play;
|
using osu.Game.Screens.Play;
|
||||||
using osu.Game.Screens.Play.HUD;
|
using osu.Game.Screens.Play.HUD;
|
||||||
|
using osu.Game.Screens.Play.HUD.HitErrorMeters;
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
|
|
||||||
namespace osu.Game.Skinning
|
namespace osu.Game.Skinning
|
||||||
@ -342,6 +343,20 @@ namespace osu.Game.Skinning
|
|||||||
{
|
{
|
||||||
accuracy.Y = container.ToLocalSpace(score.ScreenSpaceDrawQuad.BottomRight).Y;
|
accuracy.Y = container.ToLocalSpace(score.ScreenSpaceDrawQuad.BottomRight).Y;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var songProgress = container.OfType<SongProgress>().FirstOrDefault();
|
||||||
|
|
||||||
|
var hitError = container.OfType<HitErrorMeter>().FirstOrDefault();
|
||||||
|
|
||||||
|
if (hitError != null)
|
||||||
|
{
|
||||||
|
hitError.Anchor = Anchor.BottomCentre;
|
||||||
|
hitError.Origin = Anchor.CentreLeft;
|
||||||
|
hitError.Rotation = -90;
|
||||||
|
|
||||||
|
if (songProgress != null)
|
||||||
|
hitError.Y -= SongProgress.MAX_HEIGHT;
|
||||||
|
}
|
||||||
})
|
})
|
||||||
{
|
{
|
||||||
Children = new[]
|
Children = new[]
|
||||||
@ -352,6 +367,7 @@ namespace osu.Game.Skinning
|
|||||||
GetDrawableComponent(new HUDSkinComponent(HUDSkinComponents.AccuracyCounter)) ?? new DefaultAccuracyCounter(),
|
GetDrawableComponent(new HUDSkinComponent(HUDSkinComponents.AccuracyCounter)) ?? new DefaultAccuracyCounter(),
|
||||||
GetDrawableComponent(new HUDSkinComponent(HUDSkinComponents.HealthDisplay)) ?? new DefaultHealthDisplay(),
|
GetDrawableComponent(new HUDSkinComponent(HUDSkinComponents.HealthDisplay)) ?? new DefaultHealthDisplay(),
|
||||||
GetDrawableComponent(new HUDSkinComponent(HUDSkinComponents.SongProgress)) ?? new SongProgress(),
|
GetDrawableComponent(new HUDSkinComponent(HUDSkinComponents.SongProgress)) ?? new SongProgress(),
|
||||||
|
GetDrawableComponent(new HUDSkinComponent(HUDSkinComponents.BarHitErrorMeter)) ?? new BarHitErrorMeter(),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
public const int PLAYER_1_ID = 55;
|
public const int PLAYER_1_ID = 55;
|
||||||
public const int PLAYER_2_ID = 56;
|
public const int PLAYER_2_ID = 56;
|
||||||
|
|
||||||
[Cached(typeof(StatefulMultiplayerClient))]
|
[Cached(typeof(MultiplayerClient))]
|
||||||
public TestMultiplayerClient Client { get; }
|
public TestMultiplayerClient Client { get; }
|
||||||
|
|
||||||
[Cached(typeof(IRoomManager))]
|
[Cached(typeof(IRoomManager))]
|
||||||
|
@ -20,7 +20,7 @@ using osu.Game.Users;
|
|||||||
|
|
||||||
namespace osu.Game.Tests.Visual.Multiplayer
|
namespace osu.Game.Tests.Visual.Multiplayer
|
||||||
{
|
{
|
||||||
public class TestMultiplayerClient : StatefulMultiplayerClient
|
public class TestMultiplayerClient : MultiplayerClient
|
||||||
{
|
{
|
||||||
public override IBindable<bool> IsConnected => isConnected;
|
public override IBindable<bool> IsConnected => isConnected;
|
||||||
private readonly Bindable<bool> isConnected = new Bindable<bool>(true);
|
private readonly Bindable<bool> isConnected = new Bindable<bool>(true);
|
||||||
|
@ -16,7 +16,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
protected override Container<Drawable> Content => content;
|
protected override Container<Drawable> Content => content;
|
||||||
private readonly Container content;
|
private readonly Container content;
|
||||||
|
|
||||||
[Cached(typeof(StatefulMultiplayerClient))]
|
[Cached(typeof(MultiplayerClient))]
|
||||||
public readonly TestMultiplayerClient Client;
|
public readonly TestMultiplayerClient Client;
|
||||||
|
|
||||||
[Cached(typeof(IRoomManager))]
|
[Cached(typeof(IRoomManager))]
|
||||||
|
@ -33,8 +33,12 @@ namespace osu.Game.Tests.Visual
|
|||||||
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
|
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
|
||||||
{
|
{
|
||||||
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
|
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
|
||||||
|
|
||||||
dependencies.CacheAs(new EditorClock());
|
dependencies.CacheAs(new EditorClock());
|
||||||
|
|
||||||
|
var playable = Beatmap.Value.GetPlayableBeatmap(Beatmap.Value.BeatmapInfo.Ruleset);
|
||||||
|
dependencies.CacheAs(new EditorBeatmap(playable));
|
||||||
|
|
||||||
return dependencies;
|
return dependencies;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
110
osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs
Normal file
110
osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
// 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.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Utils;
|
||||||
|
using osu.Game.Online.API;
|
||||||
|
using osu.Game.Online.Spectator;
|
||||||
|
using osu.Game.Replays.Legacy;
|
||||||
|
using osu.Game.Scoring;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Visual.Spectator
|
||||||
|
{
|
||||||
|
public class TestSpectatorClient : SpectatorClient
|
||||||
|
{
|
||||||
|
public override IBindable<bool> IsConnected { get; } = new Bindable<bool>(true);
|
||||||
|
|
||||||
|
private readonly Dictionary<int, int> userBeatmapDictionary = new Dictionary<int, int>();
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private IAPIProvider api { get; set; } = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Starts play for an arbitrary user.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userId">The user to start play for.</param>
|
||||||
|
/// <param name="beatmapId">The playing beatmap id.</param>
|
||||||
|
public void StartPlay(int userId, int beatmapId)
|
||||||
|
{
|
||||||
|
userBeatmapDictionary[userId] = beatmapId;
|
||||||
|
sendPlayingState(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ends play for an arbitrary user.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userId">The user to end play for.</param>
|
||||||
|
public void EndPlay(int userId)
|
||||||
|
{
|
||||||
|
if (!PlayingUsers.Contains(userId))
|
||||||
|
return;
|
||||||
|
|
||||||
|
((ISpectatorClient)this).UserFinishedPlaying(userId, new SpectatorState
|
||||||
|
{
|
||||||
|
BeatmapID = userBeatmapDictionary[userId],
|
||||||
|
RulesetID = 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sends frames for an arbitrary user.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userId">The user to send frames for.</param>
|
||||||
|
/// <param name="index">The frame index.</param>
|
||||||
|
/// <param name="count">The number of frames to send.</param>
|
||||||
|
public void SendFrames(int userId, int index, int count)
|
||||||
|
{
|
||||||
|
var frames = new List<LegacyReplayFrame>();
|
||||||
|
|
||||||
|
for (int i = index; i < index + count; i++)
|
||||||
|
{
|
||||||
|
var buttonState = i == index + count - 1 ? ReplayButtonState.None : ReplayButtonState.Left1;
|
||||||
|
|
||||||
|
frames.Add(new LegacyReplayFrame(i * 100, RNG.Next(0, 512), RNG.Next(0, 512), buttonState));
|
||||||
|
}
|
||||||
|
|
||||||
|
var bundle = new FrameDataBundle(new ScoreInfo { Combo = index + count }, frames);
|
||||||
|
((ISpectatorClient)this).UserSentFrames(userId, bundle);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Task BeginPlayingInternal(SpectatorState state)
|
||||||
|
{
|
||||||
|
// Track the local user's playing beatmap ID.
|
||||||
|
Debug.Assert(state.BeatmapID != null);
|
||||||
|
userBeatmapDictionary[api.LocalUser.Value.Id] = state.BeatmapID.Value;
|
||||||
|
|
||||||
|
return ((ISpectatorClient)this).UserBeganPlaying(api.LocalUser.Value.Id, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Task SendFramesInternal(FrameDataBundle data) => ((ISpectatorClient)this).UserSentFrames(api.LocalUser.Value.Id, data);
|
||||||
|
|
||||||
|
protected override Task EndPlayingInternal(SpectatorState state) => ((ISpectatorClient)this).UserFinishedPlaying(api.LocalUser.Value.Id, state);
|
||||||
|
|
||||||
|
protected override Task WatchUserInternal(int userId)
|
||||||
|
{
|
||||||
|
// When newly watching a user, the server sends the playing state immediately.
|
||||||
|
if (PlayingUsers.Contains(userId))
|
||||||
|
sendPlayingState(userId);
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Task StopWatchingUserInternal(int userId) => Task.CompletedTask;
|
||||||
|
|
||||||
|
private void sendPlayingState(int userId)
|
||||||
|
{
|
||||||
|
((ISpectatorClient)this).UserBeganPlaying(userId, new SpectatorState
|
||||||
|
{
|
||||||
|
BeatmapID = userBeatmapDictionary[userId],
|
||||||
|
RulesetID = 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,90 +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.Collections.Concurrent;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using osu.Framework.Bindables;
|
|
||||||
using osu.Framework.Utils;
|
|
||||||
using osu.Game.Online;
|
|
||||||
using osu.Game.Online.Spectator;
|
|
||||||
using osu.Game.Replays.Legacy;
|
|
||||||
using osu.Game.Scoring;
|
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.Spectator
|
|
||||||
{
|
|
||||||
public class TestSpectatorStreamingClient : SpectatorStreamingClient
|
|
||||||
{
|
|
||||||
public new BindableList<int> PlayingUsers => (BindableList<int>)base.PlayingUsers;
|
|
||||||
private readonly ConcurrentDictionary<int, byte> watchingUsers = new ConcurrentDictionary<int, byte>();
|
|
||||||
|
|
||||||
private readonly Dictionary<int, int> userBeatmapDictionary = new Dictionary<int, int>();
|
|
||||||
private readonly Dictionary<int, bool> userSentStateDictionary = new Dictionary<int, bool>();
|
|
||||||
|
|
||||||
public TestSpectatorStreamingClient()
|
|
||||||
: base(new DevelopmentEndpointConfiguration())
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public void StartPlay(int userId, int beatmapId)
|
|
||||||
{
|
|
||||||
userBeatmapDictionary[userId] = beatmapId;
|
|
||||||
sendState(userId, beatmapId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void EndPlay(int userId, int beatmapId)
|
|
||||||
{
|
|
||||||
((ISpectatorClient)this).UserFinishedPlaying(userId, new SpectatorState
|
|
||||||
{
|
|
||||||
BeatmapID = beatmapId,
|
|
||||||
RulesetID = 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
userBeatmapDictionary.Remove(userId);
|
|
||||||
userSentStateDictionary.Remove(userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SendFrames(int userId, int index, int count)
|
|
||||||
{
|
|
||||||
var frames = new List<LegacyReplayFrame>();
|
|
||||||
|
|
||||||
for (int i = index; i < index + count; i++)
|
|
||||||
{
|
|
||||||
var buttonState = i == index + count - 1 ? ReplayButtonState.None : ReplayButtonState.Left1;
|
|
||||||
|
|
||||||
frames.Add(new LegacyReplayFrame(i * 100, RNG.Next(0, 512), RNG.Next(0, 512), buttonState));
|
|
||||||
}
|
|
||||||
|
|
||||||
var bundle = new FrameDataBundle(new ScoreInfo { Combo = index + count }, frames);
|
|
||||||
((ISpectatorClient)this).UserSentFrames(userId, bundle);
|
|
||||||
|
|
||||||
if (!userSentStateDictionary[userId])
|
|
||||||
sendState(userId, userBeatmapDictionary[userId]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void WatchUser(int userId)
|
|
||||||
{
|
|
||||||
base.WatchUser(userId);
|
|
||||||
|
|
||||||
// When newly watching a user, the server sends the playing state immediately.
|
|
||||||
if (watchingUsers.TryAdd(userId, 0) && PlayingUsers.Contains(userId))
|
|
||||||
sendState(userId, userBeatmapDictionary[userId]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void StopWatchingUser(int userId)
|
|
||||||
{
|
|
||||||
base.StopWatchingUser(userId);
|
|
||||||
watchingUsers.TryRemove(userId, out _);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void sendState(int userId, int beatmapId)
|
|
||||||
{
|
|
||||||
((ISpectatorClient)this).UserBeganPlaying(userId, new SpectatorState
|
|
||||||
{
|
|
||||||
BeatmapID = beatmapId,
|
|
||||||
RulesetID = 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
userSentStateDictionary[userId] = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user