1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-11 09:27:29 +08:00

Merge branch 'master' into catcher-area-catcher

# Conflicts:
#	osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs
This commit is contained in:
ekrctb 2021-07-21 16:45:28 +09:00
commit 179ba3c9a8
115 changed files with 2080 additions and 464 deletions

View File

@ -3,6 +3,7 @@ M:System.Object.Equals(System.Object)~System.Boolean;Don't use object.Equals. Us
M:System.ValueType.Equals(System.Object)~System.Boolean;Don't use object.Equals(Fallbacks to ValueType). Use IEquatable<T> or EqualityComparer<T>.Default instead.
M:System.Nullable`1.Equals(System.Object)~System.Boolean;Use == instead.
T:System.IComparable;Don't use non-generic IComparable. Use generic version instead.
T:SixLabors.ImageSharp.IDeepCloneable`1;Use osu.Game.Utils.IDeepCloneable<T> instead.
M:osu.Framework.Graphics.Sprites.SpriteText.#ctor;Use OsuSpriteText.
M:osu.Framework.Bindables.IBindableList`1.GetBoundCopy();Fails on iOS. Use manual ctor + BindTo instead. (see https://github.com/mono/mono/issues/19900)
T:Microsoft.EntityFrameworkCore.Internal.EnumerableExtensions;Don't use internal extension methods.

View File

@ -0,0 +1,288 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Utils;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osuTK;
namespace osu.Game.Rulesets.Catch.Tests
{
[TestFixture]
public class JuiceStreamPathTest
{
[TestCase(1e3, true, false)]
// When the coordinates are large, the slope invariant fails within the specified absolute allowance due to the floating-number precision.
[TestCase(1e9, false, false)]
// Using discrete values sometimes discover more edge cases.
[TestCase(10, true, true)]
public void TestRandomInsertSetPosition(double scale, bool checkSlope, bool integralValues)
{
var rng = new Random(1);
var path = new JuiceStreamPath();
for (int iteration = 0; iteration < 100000; iteration++)
{
if (rng.Next(10) == 0)
path.Clear();
int vertexCount = path.Vertices.Count;
switch (rng.Next(2))
{
case 0:
{
double distance = rng.NextDouble() * scale * 2 - scale;
if (integralValues)
distance = Math.Round(distance);
float oldX = path.PositionAtDistance(distance);
int index = path.InsertVertex(distance);
Assert.That(path.Vertices.Count, Is.EqualTo(vertexCount + 1));
Assert.That(path.Vertices[index].Distance, Is.EqualTo(distance));
Assert.That(path.Vertices[index].X, Is.EqualTo(oldX));
break;
}
case 1:
{
int index = rng.Next(path.Vertices.Count);
double distance = path.Vertices[index].Distance;
float newX = (float)(rng.NextDouble() * scale * 2 - scale);
if (integralValues)
newX = MathF.Round(newX);
path.SetVertexPosition(index, newX);
Assert.That(path.Vertices.Count, Is.EqualTo(vertexCount));
Assert.That(path.Vertices[index].Distance, Is.EqualTo(distance));
Assert.That(path.Vertices[index].X, Is.EqualTo(newX));
break;
}
}
assertInvariants(path.Vertices, checkSlope);
}
}
[Test]
public void TestRemoveVertices()
{
var path = new JuiceStreamPath();
path.Add(10, 5);
path.Add(20, -5);
int removeCount = path.RemoveVertices((v, i) => v.Distance == 10 && i == 1);
Assert.That(removeCount, Is.EqualTo(1));
Assert.That(path.Vertices, Is.EqualTo(new[]
{
new JuiceStreamPathVertex(0, 0),
new JuiceStreamPathVertex(20, -5)
}));
removeCount = path.RemoveVertices((_, i) => i == 0);
Assert.That(removeCount, Is.EqualTo(1));
Assert.That(path.Vertices, Is.EqualTo(new[]
{
new JuiceStreamPathVertex(20, -5)
}));
removeCount = path.RemoveVertices((_, i) => true);
Assert.That(removeCount, Is.EqualTo(1));
Assert.That(path.Vertices, Is.EqualTo(new[]
{
new JuiceStreamPathVertex()
}));
}
[Test]
public void TestResampleVertices()
{
var path = new JuiceStreamPath();
path.Add(-100, -10);
path.Add(100, 50);
path.ResampleVertices(new double[]
{
-50,
0,
70,
120
});
Assert.That(path.Vertices, Is.EqualTo(new[]
{
new JuiceStreamPathVertex(-100, -10),
new JuiceStreamPathVertex(-50, -5),
new JuiceStreamPathVertex(0, 0),
new JuiceStreamPathVertex(70, 35),
new JuiceStreamPathVertex(100, 50),
new JuiceStreamPathVertex(100, 50),
}));
path.Clear();
path.SetVertexPosition(0, 10);
path.ResampleVertices(Array.Empty<double>());
Assert.That(path.Vertices, Is.EqualTo(new[]
{
new JuiceStreamPathVertex(0, 10)
}));
}
[Test]
public void TestRandomConvertFromSliderPath()
{
var rng = new Random(1);
var path = new JuiceStreamPath();
var sliderPath = new SliderPath();
for (int iteration = 0; iteration < 10000; iteration++)
{
sliderPath.ControlPoints.Clear();
do
{
int start = sliderPath.ControlPoints.Count;
do
{
float x = (float)(rng.NextDouble() * 1e3);
float y = (float)(rng.NextDouble() * 1e3);
sliderPath.ControlPoints.Add(new PathControlPoint(new Vector2(x, y)));
} while (rng.Next(2) != 0);
int length = sliderPath.ControlPoints.Count - start + 1;
sliderPath.ControlPoints[start].Type.Value = length <= 2 ? PathType.Linear : length == 3 ? PathType.PerfectCurve : PathType.Bezier;
} while (rng.Next(3) != 0);
if (rng.Next(5) == 0)
sliderPath.ExpectedDistance.Value = rng.NextDouble() * 3e3;
else
sliderPath.ExpectedDistance.Value = null;
path.ConvertFromSliderPath(sliderPath);
Assert.That(path.Vertices[0].Distance, Is.EqualTo(0));
Assert.That(path.Distance, Is.EqualTo(sliderPath.Distance).Within(1e-3));
assertInvariants(path.Vertices, true);
double[] sampleDistances = Enumerable.Range(0, 10)
.Select(_ => rng.NextDouble() * sliderPath.Distance)
.ToArray();
foreach (double distance in sampleDistances)
{
float expected = sliderPath.PositionAt(distance / sliderPath.Distance).X;
Assert.That(path.PositionAtDistance(distance), Is.EqualTo(expected).Within(1e-3));
}
path.ResampleVertices(sampleDistances);
assertInvariants(path.Vertices, true);
foreach (double distance in sampleDistances)
{
float expected = sliderPath.PositionAt(distance / sliderPath.Distance).X;
Assert.That(path.PositionAtDistance(distance), Is.EqualTo(expected).Within(1e-3));
}
}
}
[Test]
public void TestRandomConvertToSliderPath()
{
var rng = new Random(1);
var path = new JuiceStreamPath();
var sliderPath = new SliderPath();
for (int iteration = 0; iteration < 10000; iteration++)
{
path.Clear();
do
{
double distance = rng.NextDouble() * 1e3;
float x = (float)(rng.NextDouble() * 1e3);
path.Add(distance, x);
} while (rng.Next(5) != 0);
float sliderStartY = (float)(rng.NextDouble() * JuiceStreamPath.OSU_PLAYFIELD_HEIGHT);
path.ConvertToSliderPath(sliderPath, sliderStartY);
Assert.That(sliderPath.Distance, Is.EqualTo(path.Distance).Within(1e-3));
Assert.That(sliderPath.ControlPoints[0].Position.Value.X, Is.EqualTo(path.Vertices[0].X));
assertInvariants(path.Vertices, true);
foreach (var point in sliderPath.ControlPoints)
{
Assert.That(point.Type.Value, Is.EqualTo(PathType.Linear).Or.Null);
Assert.That(sliderStartY + point.Position.Value.Y, Is.InRange(0, JuiceStreamPath.OSU_PLAYFIELD_HEIGHT));
}
for (int i = 0; i < 10; i++)
{
double distance = rng.NextDouble() * path.Distance;
float expected = path.PositionAtDistance(distance);
Assert.That(sliderPath.PositionAt(distance / sliderPath.Distance).X, Is.EqualTo(expected).Within(1e-3));
}
}
}
[Test]
public void TestInvalidation()
{
var path = new JuiceStreamPath();
Assert.That(path.InvalidationID, Is.EqualTo(1));
int previousId = path.InvalidationID;
path.InsertVertex(10);
checkNewId();
path.SetVertexPosition(1, 5);
checkNewId();
path.Add(20, 0);
checkNewId();
path.RemoveVertices((v, _) => v.Distance == 20);
checkNewId();
path.ResampleVertices(new double[] { 5, 10, 15 });
checkNewId();
path.Clear();
checkNewId();
path.ConvertFromSliderPath(new SliderPath());
checkNewId();
void checkNewId()
{
Assert.That(path.InvalidationID, Is.Not.EqualTo(previousId));
previousId = path.InvalidationID;
}
}
private void assertInvariants(IReadOnlyList<JuiceStreamPathVertex> vertices, bool checkSlope)
{
Assert.That(vertices, Is.Not.Empty);
for (int i = 0; i < vertices.Count; i++)
{
Assert.That(double.IsFinite(vertices[i].Distance));
Assert.That(float.IsFinite(vertices[i].X));
}
for (int i = 1; i < vertices.Count; i++)
{
Assert.That(vertices[i].Distance, Is.GreaterThanOrEqualTo(vertices[i - 1].Distance));
if (!checkSlope) continue;
float xDiff = Math.Abs(vertices[i].X - vertices[i - 1].X);
double distanceDiff = vertices[i].Distance - vertices[i - 1].Distance;
Assert.That(xDiff, Is.LessThanOrEqualTo(distanceDiff).Within(Precision.FLOAT_EPSILON));
}
}
}
}

View File

@ -9,6 +9,7 @@ using osu.Game.Audio;
using osu.Game.Rulesets.Catch.Judgements;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Skinning;
using osu.Game.Utils;
using osuTK.Graphics;
@ -31,7 +32,7 @@ namespace osu.Game.Rulesets.Catch.Objects
}
// override any external colour changes with banananana
Color4 IHasComboInformation.GetComboColour(IReadOnlyList<Color4> comboColours) => getBananaColour();
Color4 IHasComboInformation.GetComboColour(ISkin skin) => getBananaColour();
private Color4 getBananaColour()
{

View File

@ -0,0 +1,340 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Utils;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osuTK;
#nullable enable
namespace osu.Game.Rulesets.Catch.Objects
{
/// <summary>
/// Represents the path of a juice stream.
/// <para>
/// A <see cref="JuiceStream"/> holds a legacy <see cref="SliderPath"/> as the representation of the path.
/// However, the <see cref="SliderPath"/> representation is difficult to work with.
/// This <see cref="JuiceStreamPath"/> represents the path in a more convenient way, a polyline connecting list of <see cref="JuiceStreamPathVertex"/>s.
/// </para>
/// <para>
/// The path can be regarded as a function from the closed interval <c>[Vertices[0].Distance, Vertices[^1].Distance]</c> to the x position, given by <see cref="PositionAtDistance"/>.
/// To ensure the path is convertible to a <see cref="SliderPath"/>, the slope of the function must not be more than <c>1</c> everywhere,
/// and this slope condition is always maintained as an invariant.
/// </para>
/// </summary>
public class JuiceStreamPath
{
/// <summary>
/// The height of legacy osu!standard playfield.
/// The sliders converted by <see cref="ConvertToSliderPath"/> are vertically contained in this height.
/// </summary>
internal const float OSU_PLAYFIELD_HEIGHT = 384;
/// <summary>
/// The list of vertices of the path, which is represented as a polyline connecting the vertices.
/// </summary>
public IReadOnlyList<JuiceStreamPathVertex> Vertices => vertices;
/// <summary>
/// The current version number.
/// This starts from <c>1</c> and incremented whenever this <see cref="JuiceStreamPath"/> is modified.
/// </summary>
public int InvalidationID { get; private set; } = 1;
/// <summary>
/// The difference between first vertex's <see cref="JuiceStreamPathVertex.Distance"/> and last vertex's <see cref="JuiceStreamPathVertex.Distance"/>.
/// </summary>
public double Distance => vertices[^1].Distance - vertices[0].Distance;
/// <remarks>
/// This list should always be non-empty.
/// </remarks>
private readonly List<JuiceStreamPathVertex> vertices = new List<JuiceStreamPathVertex>
{
new JuiceStreamPathVertex()
};
/// <summary>
/// Compute the x-position of the path at the given <paramref name="distance"/>.
/// </summary>
/// <remarks>
/// When the given distance is outside of the path, the x position at the corresponding endpoint is returned,
/// </remarks>
public float PositionAtDistance(double distance)
{
int index = vertexIndexAtDistance(distance);
return positionAtDistance(distance, index);
}
/// <summary>
/// Remove all vertices of this path, then add a new vertex <c>(0, 0)</c>.
/// </summary>
public void Clear()
{
vertices.Clear();
vertices.Add(new JuiceStreamPathVertex());
invalidate();
}
/// <summary>
/// Insert a vertex at given <paramref name="distance"/>.
/// The <see cref="PositionAtDistance"/> is used as the position of the new vertex.
/// Thus, the set of points of the path is not changed (up to floating-point precision).
/// </summary>
/// <returns>The index of the new vertex.</returns>
public int InsertVertex(double distance)
{
if (!double.IsFinite(distance))
throw new ArgumentOutOfRangeException(nameof(distance));
int index = vertexIndexAtDistance(distance);
float x = positionAtDistance(distance, index);
vertices.Insert(index, new JuiceStreamPathVertex(distance, x));
invalidate();
return index;
}
/// <summary>
/// Move the vertex of given <paramref name="index"/> to the given position <paramref name="newX"/>.
/// When the distances between vertices are too small for the new vertex positions, the adjacent vertices are moved towards <paramref name="newX"/>.
/// </summary>
public void SetVertexPosition(int index, float newX)
{
if (index < 0 || index >= vertices.Count)
throw new ArgumentOutOfRangeException(nameof(index));
if (!float.IsFinite(newX))
throw new ArgumentOutOfRangeException(nameof(newX));
var newVertex = new JuiceStreamPathVertex(vertices[index].Distance, newX);
for (int i = index - 1; i >= 0 && !canConnect(vertices[i], newVertex); i--)
{
float clampedX = clampToConnectablePosition(newVertex, vertices[i]);
vertices[i] = new JuiceStreamPathVertex(vertices[i].Distance, clampedX);
}
for (int i = index + 1; i < vertices.Count; i++)
{
float clampedX = clampToConnectablePosition(newVertex, vertices[i]);
vertices[i] = new JuiceStreamPathVertex(vertices[i].Distance, clampedX);
}
vertices[index] = newVertex;
invalidate();
}
/// <summary>
/// Add a new vertex at given <paramref name="distance"/> and position.
/// Adjacent vertices are moved when necessary in the same way as <see cref="SetVertexPosition"/>.
/// </summary>
public void Add(double distance, float x)
{
int index = InsertVertex(distance);
SetVertexPosition(index, x);
}
/// <summary>
/// Remove all vertices that satisfy the given <paramref name="predicate"/>.
/// </summary>
/// <remarks>
/// If all vertices are removed, a new vertex <c>(0, 0)</c> is added.
/// </remarks>
/// <param name="predicate">The predicate to determine whether a vertex should be removed given the vertex and its index in the path.</param>
/// <returns>The number of removed vertices.</returns>
public int RemoveVertices(Func<JuiceStreamPathVertex, int, bool> predicate)
{
int index = 0;
int removeCount = vertices.RemoveAll(vertex => predicate(vertex, index++));
if (vertices.Count == 0)
vertices.Add(new JuiceStreamPathVertex());
if (removeCount != 0)
invalidate();
return removeCount;
}
/// <summary>
/// Recreate this path by using difference set of vertices at given distances.
/// In addition to the given <paramref name="sampleDistances"/>, the first vertex and the last vertex are always added to the new path.
/// New vertices use the positions on the original path. Thus, <see cref="PositionAtDistance"/>s at <paramref name="sampleDistances"/> are preserved.
/// </summary>
public void ResampleVertices(IEnumerable<double> sampleDistances)
{
var sampledVertices = new List<JuiceStreamPathVertex>();
foreach (double distance in sampleDistances)
{
if (!double.IsFinite(distance))
throw new ArgumentOutOfRangeException(nameof(sampleDistances));
double clampedDistance = Math.Clamp(distance, vertices[0].Distance, vertices[^1].Distance);
float x = PositionAtDistance(clampedDistance);
sampledVertices.Add(new JuiceStreamPathVertex(clampedDistance, x));
}
sampledVertices.Sort();
// The first vertex and the last vertex are always used in the result.
vertices.RemoveRange(1, vertices.Count - (vertices.Count == 1 ? 1 : 2));
vertices.InsertRange(1, sampledVertices);
invalidate();
}
/// <summary>
/// Convert a <see cref="SliderPath"/> to list of vertices and write the result to this <see cref="JuiceStreamPath"/>.
/// </summary>
/// <remarks>
/// Duplicated vertices are automatically removed.
/// </remarks>
public void ConvertFromSliderPath(SliderPath sliderPath)
{
var sliderPathVertices = new List<Vector2>();
sliderPath.GetPathToProgress(sliderPathVertices, 0, 1);
double distance = 0;
vertices.Clear();
vertices.Add(new JuiceStreamPathVertex(0, sliderPathVertices.FirstOrDefault().X));
for (int i = 1; i < sliderPathVertices.Count; i++)
{
distance += Vector2.Distance(sliderPathVertices[i - 1], sliderPathVertices[i]);
if (!Precision.AlmostEquals(vertices[^1].Distance, distance))
vertices.Add(new JuiceStreamPathVertex(distance, sliderPathVertices[i].X));
}
invalidate();
}
/// <summary>
/// Convert the path of this <see cref="JuiceStreamPath"/> to a <see cref="SliderPath"/> and write the result to <paramref name="sliderPath"/>.
/// The resulting slider is "folded" to make it vertically contained in the playfield `(0..<see cref="OSU_PLAYFIELD_HEIGHT"/>)` assuming the slider start position is <paramref name="sliderStartY"/>.
/// </summary>
public void ConvertToSliderPath(SliderPath sliderPath, float sliderStartY)
{
const float margin = 1;
// Note: these two variables and `sliderPath` are modified by the local functions.
double currentDistance = 0;
Vector2 lastPosition = new Vector2(vertices[0].X, 0);
sliderPath.ControlPoints.Clear();
sliderPath.ControlPoints.Add(new PathControlPoint(lastPosition));
for (int i = 1; i < vertices.Count; i++)
{
sliderPath.ControlPoints[^1].Type.Value = PathType.Linear;
float deltaX = vertices[i].X - lastPosition.X;
double length = vertices[i].Distance - currentDistance;
// Should satisfy `deltaX^2 + deltaY^2 = length^2`.
// By invariants, the expression inside the `sqrt` is (almost) non-negative.
double deltaY = Math.Sqrt(Math.Max(0, length * length - (double)deltaX * deltaX));
// When `deltaY` is small, one segment is always enough.
// This case is handled separately to prevent divide-by-zero.
if (deltaY <= OSU_PLAYFIELD_HEIGHT / 2 - margin)
{
float nextX = vertices[i].X;
float nextY = (float)(lastPosition.Y + getYDirection() * deltaY);
addControlPoint(nextX, nextY);
continue;
}
// When `deltaY` is large or when the slider velocity is fast, the segment must be partitioned to subsegments to stay in bounds.
for (double currentProgress = 0; currentProgress < deltaY;)
{
double nextProgress = Math.Min(currentProgress + getMaxDeltaY(), deltaY);
float nextX = (float)(vertices[i - 1].X + nextProgress / deltaY * deltaX);
float nextY = (float)(lastPosition.Y + getYDirection() * (nextProgress - currentProgress));
addControlPoint(nextX, nextY);
currentProgress = nextProgress;
}
}
int getYDirection()
{
float lastSliderY = sliderStartY + lastPosition.Y;
return lastSliderY < OSU_PLAYFIELD_HEIGHT / 2 ? 1 : -1;
}
float getMaxDeltaY()
{
float lastSliderY = sliderStartY + lastPosition.Y;
return Math.Max(lastSliderY, OSU_PLAYFIELD_HEIGHT - lastSliderY) - margin;
}
void addControlPoint(float nextX, float nextY)
{
Vector2 nextPosition = new Vector2(nextX, nextY);
sliderPath.ControlPoints.Add(new PathControlPoint(nextPosition));
currentDistance += Vector2.Distance(lastPosition, nextPosition);
lastPosition = nextPosition;
}
}
/// <summary>
/// Find the index at which a new vertex with <paramref name="distance"/> can be inserted.
/// </summary>
private int vertexIndexAtDistance(double distance)
{
// The position of `(distance, Infinity)` is uniquely determined because infinite positions are not allowed.
int i = vertices.BinarySearch(new JuiceStreamPathVertex(distance, float.PositiveInfinity));
return i < 0 ? ~i : i;
}
/// <summary>
/// Compute the position at the given <paramref name="distance"/>, assuming <paramref name="index"/> is the vertex index returned by <see cref="vertexIndexAtDistance"/>.
/// </summary>
private float positionAtDistance(double distance, int index)
{
if (index <= 0)
return vertices[0].X;
if (index >= vertices.Count)
return vertices[^1].X;
double length = vertices[index].Distance - vertices[index - 1].Distance;
if (Precision.AlmostEquals(length, 0))
return vertices[index].X;
float deltaX = vertices[index].X - vertices[index - 1].X;
return (float)(vertices[index - 1].X + deltaX * ((distance - vertices[index - 1].Distance) / length));
}
/// <summary>
/// Check the two vertices can connected directly while satisfying the slope condition.
/// </summary>
private bool canConnect(JuiceStreamPathVertex vertex1, JuiceStreamPathVertex vertex2, float allowance = 0)
{
double xDistance = Math.Abs((double)vertex2.X - vertex1.X);
float length = (float)Math.Abs(vertex2.Distance - vertex1.Distance);
return xDistance <= length + allowance;
}
/// <summary>
/// Move the position of <paramref name="movableVertex"/> towards the position of <paramref name="fixedVertex"/>
/// until the vertex pair satisfies the condition <see cref="canConnect"/>.
/// </summary>
/// <returns>The resulting position of <paramref name="movableVertex"/>.</returns>
private float clampToConnectablePosition(JuiceStreamPathVertex fixedVertex, JuiceStreamPathVertex movableVertex)
{
float length = (float)Math.Abs(movableVertex.Distance - fixedVertex.Distance);
return Math.Clamp(movableVertex.X, fixedVertex.X - length, fixedVertex.X + length);
}
private void invalidate() => InvalidationID++;
}
}

View File

@ -0,0 +1,33 @@
// 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;
#nullable enable
namespace osu.Game.Rulesets.Catch.Objects
{
/// <summary>
/// A vertex of a <see cref="JuiceStreamPath"/>.
/// </summary>
public readonly struct JuiceStreamPathVertex : IComparable<JuiceStreamPathVertex>
{
public readonly double Distance;
public readonly float X;
public JuiceStreamPathVertex(double distance, float x)
{
Distance = distance;
X = x;
}
public int CompareTo(JuiceStreamPathVertex other)
{
int c = Distance.CompareTo(other.Distance);
return c != 0 ? c : X.CompareTo(other.X);
}
public override string ToString() => $"({Distance}, {X})";
}
}

View File

@ -1,10 +1,10 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using Newtonsoft.Json;
using osu.Framework.Bindables;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Skinning;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Catch.Objects
@ -45,6 +45,6 @@ namespace osu.Game.Rulesets.Catch.Objects
}
}
Color4 IHasComboInformation.GetComboColour(IReadOnlyList<Color4> comboColours) => comboColours[(IndexInBeatmap + 1) % comboColours.Count];
Color4 IHasComboInformation.GetComboColour(ISkin skin) => IHasComboInformation.GetSkinComboColour(this, skin, IndexInBeatmap + 1);
}
}

View File

@ -353,6 +353,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public TargetBeatContainer(double firstHitTime)
{
this.firstHitTime = firstHitTime;
AllowMistimedEventFiring = false;
Divisor = 1;
}
@ -374,8 +375,7 @@ namespace osu.Game.Rulesets.Osu.Mods
int timeSignature = (int)timingPoint.TimeSignature;
// play metronome from one measure before the first object.
// TODO: Use BeatSyncClock from https://github.com/ppy/osu/pull/13894.
if (Clock.CurrentTime < firstHitTime - timingPoint.BeatLength * timeSignature)
if (BeatSyncClock.CurrentTime < firstHitTime - timingPoint.BeatLength * timeSignature)
return;
sample.Frequency.Value = beatIndex % timeSignature == 0 ? 1 : 0.5f;

View File

@ -129,14 +129,8 @@ namespace osu.Game.Tests.Gameplay
{
switch (lookup)
{
case GlobalSkinColours global:
switch (global)
{
case GlobalSkinColours.ComboColours:
return SkinUtils.As<TValue>(new Bindable<IReadOnlyList<Color4>>(ComboColours));
}
break;
case SkinComboColourLookup comboColour:
return SkinUtils.As<TValue>(new Bindable<Color4>(ComboColours[comboColour.ColourIndex % ComboColours.Count]));
}
throw new NotImplementedException();

View File

@ -248,13 +248,13 @@ namespace osu.Game.Tests.NonVisual
}
[Test]
public void TestCreateCopyIsDeepClone()
public void TestDeepClone()
{
var cpi = new ControlPointInfo();
cpi.Add(1000, new TimingControlPoint { BeatLength = 500 });
var cpiCopy = cpi.CreateCopy();
var cpiCopy = cpi.DeepClone();
cpiCopy.Add(2000, new TimingControlPoint { BeatLength = 500 });

View File

@ -0,0 +1,33 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
namespace osu.Game.Tests.NonVisual
{
[TestFixture]
public class ScoreInfoTest
{
[Test]
public void TestDeepClone()
{
var score = new ScoreInfo();
score.Statistics.Add(HitResult.Good, 10);
score.Rank = ScoreRank.B;
var scoreCopy = score.DeepClone();
score.Statistics[HitResult.Good]++;
score.Rank = ScoreRank.X;
Assert.That(scoreCopy.Statistics[HitResult.Good], Is.EqualTo(10));
Assert.That(score.Statistics[HitResult.Good], Is.EqualTo(11));
Assert.That(scoreCopy.Rank, Is.EqualTo(ScoreRank.B));
Assert.That(score.Rank, Is.EqualTo(ScoreRank.X));
}
}
}

View File

@ -4,13 +4,11 @@
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Game.Graphics;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.OnlinePlay.Lounge.Components;
using osu.Game.Tests.Visual.OnlinePlay;
using osuTK.Graphics;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Multiplayer
@ -29,7 +27,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 0.5f,
JoinRequested = joinRequested
};
});
@ -43,11 +40,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddAssert("has 2 rooms", () => container.Rooms.Count == 2);
AddAssert("first room removed", () => container.Rooms.All(r => r.Room.RoomID.Value != 0));
AddStep("select first room", () => container.Rooms.First().Action?.Invoke());
AddStep("select first room", () => container.Rooms.First().Click());
AddAssert("first room selected", () => checkRoomSelected(RoomManager.Rooms.First()));
AddStep("join first room", () => container.Rooms.First().Action?.Invoke());
AddAssert("first room joined", () => RoomManager.Rooms.First().Status.Value is JoinedRoomStatus);
}
[Test]
@ -66,9 +60,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
press(Key.Down);
press(Key.Down);
AddAssert("last room selected", () => checkRoomSelected(RoomManager.Rooms.Last()));
press(Key.Enter);
AddAssert("last room joined", () => RoomManager.Rooms.Last().Status.Value is JoinedRoomStatus);
}
[Test]
@ -123,15 +114,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("3 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 3);
}
private bool checkRoomSelected(Room room) => SelectedRoom.Value == room;
private void joinRequested(Room room) => room.Status.Value = new JoinedRoomStatus();
private class JoinedRoomStatus : RoomStatus
[Test]
public void TestPasswordProtectedRooms()
{
public override string Message => "Joined";
public override Color4 GetAppropriateColour(OsuColour colours) => colours.Yellow;
AddStep("add rooms", () => RoomManager.AddRooms(3, withPassword: true));
}
private bool checkRoomSelected(Room room) => SelectedRoom.Value == room;
}
}

View File

@ -7,6 +7,7 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Platform;
using osu.Framework.Screens;
using osu.Framework.Testing;
@ -22,6 +23,7 @@ using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens;
using osu.Game.Screens.OnlinePlay.Components;
using osu.Game.Screens.OnlinePlay.Lounge;
using osu.Game.Screens.OnlinePlay.Lounge.Components;
using osu.Game.Screens.OnlinePlay.Match.Components;
using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
@ -85,6 +87,154 @@ namespace osu.Game.Tests.Visual.Multiplayer
// used to test the flow of multiplayer from visual tests.
}
[Test]
public void TestCreateRoomWithoutPassword()
{
createRoom(() => new Room
{
Name = { Value = "Test Room" },
Playlist =
{
new PlaylistItem
{
Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo },
Ruleset = { Value = new OsuRuleset().RulesetInfo },
}
}
});
}
[Test]
public void TestExitMidJoin()
{
Room room = null;
AddStep("create room", () =>
{
room = new Room
{
Name = { Value = "Test Room" },
Playlist =
{
new PlaylistItem
{
Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo },
Ruleset = { Value = new OsuRuleset().RulesetInfo },
}
}
};
});
AddStep("refresh rooms", () => multiplayerScreen.RoomManager.Filter.Value = new FilterCriteria());
AddStep("select room", () => InputManager.Key(Key.Down));
AddStep("join room and immediately exit", () =>
{
multiplayerScreen.ChildrenOfType<LoungeSubScreen>().Single().Open(room);
Schedule(() => Stack.CurrentScreen.Exit());
});
}
[Test]
public void TestJoinRoomWithoutPassword()
{
AddStep("create room", () =>
{
API.Queue(new CreateRoomRequest(new Room
{
Name = { Value = "Test Room" },
Playlist =
{
new PlaylistItem
{
Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo },
Ruleset = { Value = new OsuRuleset().RulesetInfo },
}
}
}));
});
AddStep("refresh rooms", () => multiplayerScreen.RoomManager.Filter.Value = new FilterCriteria());
AddStep("select room", () => InputManager.Key(Key.Down));
AddStep("join room", () => InputManager.Key(Key.Enter));
AddUntilStep("wait for room open", () => this.ChildrenOfType<MultiplayerMatchSubScreen>().FirstOrDefault()?.IsLoaded == true);
AddUntilStep("wait for join", () => client.Room != null);
}
[Test]
public void TestCreateRoomWithPassword()
{
createRoom(() => new Room
{
Name = { Value = "Test Room" },
Password = { Value = "password" },
Playlist =
{
new PlaylistItem
{
Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo },
Ruleset = { Value = new OsuRuleset().RulesetInfo },
}
}
});
AddAssert("room has password", () => client.APIRoom?.Password.Value == "password");
}
[Test]
public void TestJoinRoomWithPassword()
{
AddStep("create room", () =>
{
API.Queue(new CreateRoomRequest(new Room
{
Name = { Value = "Test Room" },
Password = { Value = "password" },
Playlist =
{
new PlaylistItem
{
Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo },
Ruleset = { Value = new OsuRuleset().RulesetInfo },
}
}
}));
});
AddStep("refresh rooms", () => multiplayerScreen.RoomManager.Filter.Value = new FilterCriteria());
AddStep("select room", () => InputManager.Key(Key.Down));
AddStep("join room", () => InputManager.Key(Key.Enter));
DrawableRoom.PasswordEntryPopover passwordEntryPopover = null;
AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType<DrawableRoom.PasswordEntryPopover>().FirstOrDefault()) != null);
AddStep("enter password in text box", () => passwordEntryPopover.ChildrenOfType<TextBox>().First().Text = "password");
AddStep("press join room button", () => passwordEntryPopover.ChildrenOfType<OsuButton>().First().Click());
AddUntilStep("wait for room open", () => this.ChildrenOfType<MultiplayerMatchSubScreen>().FirstOrDefault()?.IsLoaded == true);
AddUntilStep("wait for join", () => client.Room != null);
}
[Test]
public void TestLocalPasswordUpdatedWhenMultiplayerSettingsChange()
{
createRoom(() => new Room
{
Name = { Value = "Test Room" },
Password = { Value = "password" },
Playlist =
{
new PlaylistItem
{
Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo },
Ruleset = { Value = new OsuRuleset().RulesetInfo },
}
}
});
AddStep("change password", () => client.ChangeSettings(password: "password2"));
AddUntilStep("local password changed", () => client.APIRoom?.Password.Value == "password2");
}
[Test]
public void TestUserSetToIdleWhenBeatmapDeleted()
{

View File

@ -0,0 +1,105 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay.Lounge;
using osu.Game.Screens.OnlinePlay.Lounge.Components;
using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Tests.Visual.OnlinePlay;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneMultiplayerLoungeSubScreen : OnlinePlayTestScene
{
protected new BasicTestRoomManager RoomManager => (BasicTestRoomManager)base.RoomManager;
private LoungeSubScreen loungeScreen;
private Room lastJoinedRoom;
private string lastJoinedPassword;
public override void SetUpSteps()
{
base.SetUpSteps();
AddStep("push screen", () => LoadScreen(loungeScreen = new MultiplayerLoungeSubScreen()));
AddUntilStep("wait for present", () => loungeScreen.IsCurrentScreen());
AddStep("bind to event", () =>
{
lastJoinedRoom = null;
lastJoinedPassword = null;
RoomManager.JoinRoomRequested = onRoomJoined;
});
}
[Test]
public void TestJoinRoomWithoutPassword()
{
AddStep("add room", () => RoomManager.AddRooms(1, withPassword: false));
AddStep("select room", () => InputManager.Key(Key.Down));
AddStep("join room", () => InputManager.Key(Key.Enter));
AddAssert("room join requested", () => lastJoinedRoom == RoomManager.Rooms.First());
AddAssert("room join password correct", () => lastJoinedPassword == null);
}
[Test]
public void TestPopoverHidesOnLeavingScreen()
{
AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true));
AddStep("select room", () => InputManager.Key(Key.Down));
AddStep("attempt join room", () => InputManager.Key(Key.Enter));
AddUntilStep("password prompt appeared", () => InputManager.ChildrenOfType<DrawableRoom.PasswordEntryPopover>().Any());
AddStep("exit screen", () => Stack.Exit());
AddUntilStep("password prompt hidden", () => !InputManager.ChildrenOfType<DrawableRoom.PasswordEntryPopover>().Any());
}
[Test]
public void TestJoinRoomWithPassword()
{
DrawableRoom.PasswordEntryPopover passwordEntryPopover = null;
AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true));
AddStep("select room", () => InputManager.Key(Key.Down));
AddStep("attempt join room", () => InputManager.Key(Key.Enter));
AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType<DrawableRoom.PasswordEntryPopover>().FirstOrDefault()) != null);
AddStep("enter password in text box", () => passwordEntryPopover.ChildrenOfType<TextBox>().First().Text = "password");
AddStep("press join room button", () => passwordEntryPopover.ChildrenOfType<OsuButton>().First().Click());
AddAssert("room join requested", () => lastJoinedRoom == RoomManager.Rooms.First());
AddAssert("room join password correct", () => lastJoinedPassword == "password");
}
[Test]
public void TestJoinRoomWithPasswordViaKeyboardOnly()
{
DrawableRoom.PasswordEntryPopover passwordEntryPopover = null;
AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true));
AddStep("select room", () => InputManager.Key(Key.Down));
AddStep("attempt join room", () => InputManager.Key(Key.Enter));
AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType<DrawableRoom.PasswordEntryPopover>().FirstOrDefault()) != null);
AddStep("enter password in text box", () => passwordEntryPopover.ChildrenOfType<TextBox>().First().Text = "password");
AddStep("press enter", () => InputManager.Key(Key.Enter));
AddAssert("room join requested", () => lastJoinedRoom == RoomManager.Rooms.First());
AddAssert("room join password correct", () => lastJoinedPassword == "password");
}
private void onRoomJoined(Room room, string password)
{
lastJoinedRoom = room;
lastJoinedPassword = password;
}
}
}

View File

@ -2,8 +2,12 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Online.Rooms;
using osu.Game.Online.Rooms.RoomStatuses;
using osu.Game.Screens.OnlinePlay.Lounge.Components;
@ -12,40 +16,66 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneRoomStatus : OsuTestScene
{
public TestSceneRoomStatus()
[Test]
public void TestMultipleStatuses()
{
Child = new FillFlowContainer
AddStep("create rooms", () =>
{
RelativeSizeAxes = Axes.Both,
Width = 0.5f,
Children = new Drawable[]
Child = new FillFlowContainer
{
new DrawableRoom(new Room
RelativeSizeAxes = Axes.Both,
Width = 0.5f,
Children = new Drawable[]
{
Name = { Value = "Open - ending in 1 day" },
Status = { Value = new RoomStatusOpen() },
EndDate = { Value = DateTimeOffset.Now.AddDays(1) }
}) { MatchingFilter = true },
new DrawableRoom(new Room
{
Name = { Value = "Playing - ending in 1 day" },
Status = { Value = new RoomStatusPlaying() },
EndDate = { Value = DateTimeOffset.Now.AddDays(1) }
}) { MatchingFilter = true },
new DrawableRoom(new Room
{
Name = { Value = "Ended" },
Status = { Value = new RoomStatusEnded() },
EndDate = { Value = DateTimeOffset.Now }
}) { MatchingFilter = true },
new DrawableRoom(new Room
{
Name = { Value = "Open" },
Status = { Value = new RoomStatusOpen() },
Category = { Value = RoomCategory.Realtime }
}) { MatchingFilter = true },
}
};
new DrawableRoom(new Room
{
Name = { Value = "Open - ending in 1 day" },
Status = { Value = new RoomStatusOpen() },
EndDate = { Value = DateTimeOffset.Now.AddDays(1) }
}) { MatchingFilter = true },
new DrawableRoom(new Room
{
Name = { Value = "Playing - ending in 1 day" },
Status = { Value = new RoomStatusPlaying() },
EndDate = { Value = DateTimeOffset.Now.AddDays(1) }
}) { MatchingFilter = true },
new DrawableRoom(new Room
{
Name = { Value = "Ended" },
Status = { Value = new RoomStatusEnded() },
EndDate = { Value = DateTimeOffset.Now }
}) { MatchingFilter = true },
new DrawableRoom(new Room
{
Name = { Value = "Open" },
Status = { Value = new RoomStatusOpen() },
Category = { Value = RoomCategory.Realtime }
}) { MatchingFilter = true },
}
};
});
}
[Test]
public void TestEnableAndDisablePassword()
{
DrawableRoom drawableRoom = null;
Room room = null;
AddStep("create room", () => Child = drawableRoom = new DrawableRoom(room = new Room
{
Name = { Value = "Room with password" },
Status = { Value = new RoomStatusOpen() },
Category = { Value = RoomCategory.Realtime },
}) { MatchingFilter = true });
AddAssert("password icon hidden", () => Precision.AlmostEquals(0, drawableRoom.ChildrenOfType<DrawableRoom.PasswordProtectedIcon>().Single().Alpha));
AddStep("set password", () => room.Password.Value = "password");
AddAssert("password icon visible", () => Precision.AlmostEquals(1, drawableRoom.ChildrenOfType<DrawableRoom.PasswordProtectedIcon>().Single().Alpha));
AddStep("unset password", () => room.Password.Value = string.Empty);
AddAssert("password icon hidden", () => Precision.AlmostEquals(0, drawableRoom.ChildrenOfType<DrawableRoom.PasswordProtectedIcon>().Single().Alpha));
}
}
}

View File

@ -37,7 +37,7 @@ namespace osu.Game.Tests.Visual.Playlists
AddUntilStep("first room is not masked", () => checkRoomVisible(roomsContainer.Rooms.First()));
AddStep("select last room", () => roomsContainer.Rooms.Last().Action?.Invoke());
AddStep("select last room", () => roomsContainer.Rooms.Last().Click());
AddUntilStep("first room is masked", () => !checkRoomVisible(roomsContainer.Rooms.First()));
AddUntilStep("last room is not masked", () => checkRoomVisible(roomsContainer.Rooms.Last()));

View File

@ -152,7 +152,7 @@ namespace osu.Game.Tests.Visual.Playlists
onSuccess?.Invoke(room);
}
public void JoinRoom(Room room, Action<Room> onSuccess = null, Action<string> onError = null) => throw new NotImplementedException();
public void JoinRoom(Room room, string password, Action<Room> onSuccess = null, Action<string> onError = null) => throw new NotImplementedException();
public void PartRoom() => throw new NotImplementedException();
}

View File

@ -5,18 +5,19 @@ using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio.Track;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Timing;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Overlays;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Play;
using osuTK.Graphics;
namespace osu.Game.Tests.Visual.UserInterface
@ -24,37 +25,125 @@ namespace osu.Game.Tests.Visual.UserInterface
[TestFixture]
public class TestSceneBeatSyncedContainer : OsuTestScene
{
private readonly NowPlayingOverlay np;
private TestBeatSyncedContainer beatContainer;
public TestSceneBeatSyncedContainer()
private MasterGameplayClockContainer gameplayClockContainer;
[SetUpSteps]
public void SetUpSteps()
{
Clock = new FramedClock();
Clock.ProcessFrame();
AddRange(new Drawable[]
AddStep("Set beatmap", () =>
{
new BeatContainer
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
},
np = new NowPlayingOverlay
{
Origin = Anchor.TopRight,
Anchor = Anchor.TopRight,
}
Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
});
AddStep("Create beat sync container", () =>
{
Children = new Drawable[]
{
gameplayClockContainer = new MasterGameplayClockContainer(Beatmap.Value, 0)
{
Child = beatContainer = new TestBeatSyncedContainer
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
},
}
};
});
AddStep("Start playback", () => gameplayClockContainer.Start());
}
protected override void LoadComplete()
[TestCase(false)]
[TestCase(true)]
public void TestDisallowMistimedEventFiring(bool allowMistimed)
{
base.LoadComplete();
np.ToggleVisibility();
int? lastBeatIndex = null;
double? lastActuationTime = null;
TimingControlPoint lastTimingPoint = null;
AddStep($"set mistimed to {(allowMistimed ? "allowed" : "disallowed")}", () => beatContainer.AllowMistimedEventFiring = allowMistimed);
AddStep("Set time before zero", () =>
{
beatContainer.NewBeat = (i, timingControlPoint, effectControlPoint, channelAmplitudes) =>
{
lastActuationTime = gameplayClockContainer.CurrentTime;
lastTimingPoint = timingControlPoint;
lastBeatIndex = i;
beatContainer.NewBeat = null;
};
gameplayClockContainer.Seek(-1000);
});
AddUntilStep("wait for trigger", () => lastBeatIndex != null);
if (!allowMistimed)
{
AddAssert("trigger is near beat length", () => lastActuationTime != null && lastBeatIndex != null && Precision.AlmostEquals(lastTimingPoint.Time + lastBeatIndex.Value * lastTimingPoint.BeatLength, lastActuationTime.Value, BeatSyncedContainer.MISTIMED_ALLOWANCE));
}
else
{
AddAssert("trigger is not near beat length", () => lastActuationTime != null && lastBeatIndex != null && !Precision.AlmostEquals(lastTimingPoint.Time + lastBeatIndex.Value * lastTimingPoint.BeatLength, lastActuationTime.Value, BeatSyncedContainer.MISTIMED_ALLOWANCE));
}
}
private class BeatContainer : BeatSyncedContainer
[Test]
public void TestNegativeBeatsStillUsingBeatmapTiming()
{
private const int flash_layer_heigth = 150;
int? lastBeatIndex = null;
double? lastBpm = null;
AddStep("Set time before zero", () =>
{
beatContainer.NewBeat = (i, timingControlPoint, effectControlPoint, channelAmplitudes) =>
{
lastBeatIndex = i;
lastBpm = timingControlPoint.BPM;
};
gameplayClockContainer.Seek(-1000);
});
AddUntilStep("wait for trigger", () => lastBpm != null);
AddAssert("bpm is from beatmap", () => lastBpm != null && Precision.AlmostEquals(lastBpm.Value, 128));
AddAssert("beat index is less than zero", () => lastBeatIndex < 0);
}
[Test]
public void TestIdleBeatOnPausedClock()
{
double? lastBpm = null;
AddStep("bind event", () =>
{
beatContainer.NewBeat = (i, timingControlPoint, effectControlPoint, channelAmplitudes) => lastBpm = timingControlPoint.BPM;
});
AddUntilStep("wait for trigger", () => lastBpm != null);
AddAssert("bpm is from beatmap", () => lastBpm != null && Precision.AlmostEquals(lastBpm.Value, 128));
AddStep("pause gameplay clock", () =>
{
lastBpm = null;
gameplayClockContainer.Stop();
});
AddUntilStep("wait for trigger", () => lastBpm != null);
AddAssert("bpm is default", () => lastBpm != null && Precision.AlmostEquals(lastBpm.Value, 60));
}
private class TestBeatSyncedContainer : BeatSyncedContainer
{
private const int flash_layer_height = 150;
public new bool AllowMistimedEventFiring
{
get => base.AllowMistimedEventFiring;
set => base.AllowMistimedEventFiring = value;
}
private readonly InfoString timingPointCount;
private readonly InfoString currentTimingPoint;
@ -64,13 +153,11 @@ namespace osu.Game.Tests.Visual.UserInterface
private readonly InfoString adjustedBeatLength;
private readonly InfoString timeUntilNextBeat;
private readonly InfoString timeSinceLastBeat;
private readonly InfoString currentTime;
private readonly Box flashLayer;
[Resolved]
private MusicController musicController { get; set; }
public BeatContainer()
public TestBeatSyncedContainer()
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
@ -82,7 +169,7 @@ namespace osu.Game.Tests.Visual.UserInterface
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
AutoSizeAxes = Axes.Both,
Margin = new MarginPadding { Bottom = flash_layer_heigth },
Margin = new MarginPadding { Bottom = flash_layer_height },
Children = new Drawable[]
{
new Box
@ -98,6 +185,7 @@ namespace osu.Game.Tests.Visual.UserInterface
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
currentTime = new InfoString(@"Current time"),
timingPointCount = new InfoString(@"Timing points amount"),
currentTimingPoint = new InfoString(@"Current timing point"),
beatCount = new InfoString(@"Beats amount (in the current timing point)"),
@ -116,7 +204,7 @@ namespace osu.Game.Tests.Visual.UserInterface
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
RelativeSizeAxes = Axes.X,
Height = flash_layer_heigth,
Height = flash_layer_height,
Children = new Drawable[]
{
new Box
@ -133,8 +221,13 @@ namespace osu.Game.Tests.Visual.UserInterface
}
}
};
}
Beatmap.ValueChanged += delegate
protected override void LoadComplete()
{
base.LoadComplete();
Beatmap.BindValueChanged(_ =>
{
timingPointCount.Value = 0;
currentTimingPoint.Value = 0;
@ -144,7 +237,7 @@ namespace osu.Game.Tests.Visual.UserInterface
adjustedBeatLength.Value = 0;
timeUntilNextBeat.Value = 0;
timeSinceLastBeat.Value = 0;
};
}, true);
}
private List<TimingControlPoint> timingPoints => Beatmap.Value.Beatmap.ControlPointInfo.TimingPoints.ToList();
@ -164,7 +257,7 @@ namespace osu.Game.Tests.Visual.UserInterface
if (timingPoints.Count == 0) return 0;
if (timingPoints[^1] == current)
return (int)Math.Ceiling((musicController.CurrentTrack.Length - current.Time) / current.BeatLength);
return (int)Math.Ceiling((BeatSyncClock.CurrentTime - current.Time) / current.BeatLength);
return (int)Math.Ceiling((getNextTimingPoint(current).Time - current.Time) / current.BeatLength);
}
@ -174,8 +267,11 @@ namespace osu.Game.Tests.Visual.UserInterface
base.Update();
timeUntilNextBeat.Value = TimeUntilNextBeat;
timeSinceLastBeat.Value = TimeSinceLastBeat;
currentTime.Value = BeatSyncClock.CurrentTime;
}
public Action<int, TimingControlPoint, EffectControlPoint, ChannelAmplitudes> NewBeat;
protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes)
{
base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes);
@ -187,7 +283,9 @@ namespace osu.Game.Tests.Visual.UserInterface
beatsPerMinute.Value = 60000 / timingPoint.BeatLength;
adjustedBeatLength.Value = timingPoint.BeatLength;
flashLayer.FadeOutFromOne(timingPoint.BeatLength);
flashLayer.FadeOutFromOne(timingPoint.BeatLength / 4);
NewBeat?.Invoke(beatIndex, timingPoint, effectPoint, amplitudes);
}
}
@ -200,7 +298,7 @@ namespace osu.Game.Tests.Visual.UserInterface
public double Value
{
set => valueText.Text = $"{value:G}";
set => valueText.Text = $"{value:0.##}";
}
public InfoString(string header)

View File

@ -88,7 +88,7 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("create mods", () =>
{
original = new OsuModDoubleTime();
copy = (OsuModDoubleTime)original.CreateCopy();
copy = (OsuModDoubleTime)original.DeepClone();
});
AddStep("change property", () => original.SpeedChange.Value = 2);
@ -106,7 +106,7 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("create mods", () =>
{
original = new MultiMod(new OsuModDoubleTime());
copy = (MultiMod)original.CreateCopy();
copy = (MultiMod)original.DeepClone();
});
AddStep("change property", () => ((OsuModDoubleTime)original.Mods[0]).SpeedChange.Value = 2);

View File

@ -3,11 +3,12 @@
using System;
using osu.Game.Graphics;
using osu.Game.Utils;
using osuTK.Graphics;
namespace osu.Game.Beatmaps.ControlPoints
{
public abstract class ControlPoint : IComparable<ControlPoint>
public abstract class ControlPoint : IComparable<ControlPoint>, IDeepCloneable<ControlPoint>
{
/// <summary>
/// The time at which the control point takes effect.
@ -32,7 +33,7 @@ namespace osu.Game.Beatmaps.ControlPoints
/// <summary>
/// Create an unbound copy of this control point.
/// </summary>
public ControlPoint CreateCopy()
public ControlPoint DeepClone()
{
var copy = (ControlPoint)Activator.CreateInstance(GetType());

View File

@ -10,11 +10,12 @@ using osu.Framework.Bindables;
using osu.Framework.Lists;
using osu.Framework.Utils;
using osu.Game.Screens.Edit;
using osu.Game.Utils;
namespace osu.Game.Beatmaps.ControlPoints
{
[Serializable]
public class ControlPointInfo
public class ControlPointInfo : IDeepCloneable<ControlPointInfo>
{
/// <summary>
/// All control points grouped by time.
@ -350,12 +351,12 @@ namespace osu.Game.Beatmaps.ControlPoints
}
}
public ControlPointInfo CreateCopy()
public ControlPointInfo DeepClone()
{
var controlPointInfo = new ControlPointInfo();
foreach (var point in AllControlPoints)
controlPointInfo.Add(point.Time, point.CreateCopy());
controlPointInfo.Add(point.Time, point.DeepClone());
return controlPointInfo;
}

View File

@ -1,19 +1,32 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Diagnostics;
using osu.Framework.Allocation;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Containers;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Screens.Play;
namespace osu.Game.Graphics.Containers
{
/// <summary>
/// A container which fires a callback when a new beat is reached.
/// Consumes a parent <see cref="GameplayClock"/> or <see cref="Beatmap"/> (whichever is first available).
/// </summary>
/// <remarks>
/// This container does not set its own clock to the source used for beat matching.
/// This means that if the beat source clock is playing faster or slower, animations may unexpectedly overlap.
/// Make sure this container's Clock is also set to the expected source (or within a parent element which provides this).
///
/// This container will also trigger beat events when the beat matching clock is paused at <see cref="TimingControlPoint.DEFAULT"/>'s BPM.
/// </remarks>
public class BeatSyncedContainer : Container
{
protected readonly IBindable<WorkingBeatmap> Beatmap = new Bindable<WorkingBeatmap>();
private int lastBeat;
private TimingControlPoint lastTimingPoint;
@ -23,6 +36,19 @@ namespace osu.Game.Graphics.Containers
/// </summary>
protected double EarlyActivationMilliseconds;
/// <summary>
/// While this container automatically applied an animation delay (meaning any animations inside a <see cref="OnNewBeat"/> implementation will
/// always be correctly timed), the event itself can potentially fire away from the related beat.
///
/// By setting this to false, cases where the event is to be fired more than <see cref="MISTIMED_ALLOWANCE"/> from the related beat will be skipped.
/// </summary>
protected bool AllowMistimedEventFiring = true;
/// <summary>
/// The maximum deviance from the actual beat that an <see cref="OnNewBeat"/> can fire when <see cref="AllowMistimedEventFiring"/> is set to false.
/// </summary>
public const double MISTIMED_ALLOWANCE = 16;
/// <summary>
/// The time in milliseconds until the next beat.
/// </summary>
@ -43,16 +69,49 @@ namespace osu.Game.Graphics.Containers
/// </summary>
public double MinimumBeatLength { get; set; }
/// <summary>
/// Whether this container is currently tracking a beatmap's timing data.
/// </summary>
protected bool IsBeatSyncedWithTrack { get; private set; }
protected virtual void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes)
{
}
[Resolved]
protected IBindable<WorkingBeatmap> Beatmap { get; private set; }
[Resolved(canBeNull: true)]
protected GameplayClock GameplayClock { get; private set; }
protected IClock BeatSyncClock
{
get
{
if (GameplayClock != null)
return GameplayClock;
if (Beatmap.Value.TrackLoaded)
return Beatmap.Value.Track;
return null;
}
}
protected override void Update()
{
ITrack track = null;
IBeatmap beatmap = null;
double currentTrackTime = 0;
TimingControlPoint timingPoint = null;
EffectControlPoint effectPoint = null;
TimingControlPoint timingPoint;
EffectControlPoint effectPoint;
IClock clock = BeatSyncClock;
if (clock == null)
return;
double currentTrackTime = clock.CurrentTime;
if (Beatmap.Value.TrackLoaded && Beatmap.Value.BeatmapLoaded)
{
@ -60,23 +119,26 @@ namespace osu.Game.Graphics.Containers
beatmap = Beatmap.Value.Beatmap;
}
if (track != null && beatmap != null && track.IsRunning && track.Length > 0)
IsBeatSyncedWithTrack = beatmap != null && clock.IsRunning && track?.Length > 0;
if (IsBeatSyncedWithTrack)
{
currentTrackTime = track.CurrentTime + EarlyActivationMilliseconds;
Debug.Assert(beatmap != null);
timingPoint = beatmap.ControlPointInfo.TimingPointAt(currentTrackTime);
effectPoint = beatmap.ControlPointInfo.EffectPointAt(currentTrackTime);
}
IsBeatSyncedWithTrack = timingPoint?.BeatLength > 0;
if (timingPoint == null || !IsBeatSyncedWithTrack)
else
{
// this may be the case where the beat syncing clock has been paused.
// we still want to show an idle animation, so use this container's time instead.
currentTrackTime = Clock.CurrentTime;
timingPoint = TimingControlPoint.DEFAULT;
effectPoint = EffectControlPoint.DEFAULT;
}
currentTrackTime += EarlyActivationMilliseconds;
double beatLength = timingPoint.BeatLength / Divisor;
while (beatLength < MinimumBeatLength)
@ -89,7 +151,7 @@ namespace osu.Game.Graphics.Containers
beatIndex--;
TimeUntilNextBeat = (timingPoint.Time - currentTrackTime) % beatLength;
if (TimeUntilNextBeat < 0)
if (TimeUntilNextBeat <= 0)
TimeUntilNextBeat += beatLength;
TimeSinceLastBeat = beatLength - TimeUntilNextBeat;
@ -97,21 +159,16 @@ namespace osu.Game.Graphics.Containers
if (timingPoint == lastTimingPoint && beatIndex == lastBeat)
return;
using (BeginDelayedSequence(-TimeSinceLastBeat))
OnNewBeat(beatIndex, timingPoint, effectPoint, track?.CurrentAmplitudes ?? ChannelAmplitudes.Empty);
// as this event is sometimes used for sound triggers where `BeginDelayedSequence` has no effect, avoid firing it if too far away from the beat.
// this can happen after a seek operation.
if (AllowMistimedEventFiring || Math.Abs(TimeSinceLastBeat) < MISTIMED_ALLOWANCE)
{
using (BeginDelayedSequence(-TimeSinceLastBeat))
OnNewBeat(beatIndex, timingPoint, effectPoint, track?.CurrentAmplitudes ?? ChannelAmplitudes.Empty);
}
lastBeat = beatIndex;
lastTimingPoint = timingPoint;
}
[BackgroundDependencyLoader]
private void load(IBindable<WorkingBeatmap> beatmap)
{
Beatmap.BindTo(beatmap);
}
protected virtual void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes)
{
}
}
}

View File

@ -0,0 +1,15 @@
// 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.Online.API
{
public class APIException : InvalidOperationException
{
public APIException(string messsage, Exception innerException)
: base(messsage, innerException)
{
}
}
}

View File

@ -79,7 +79,13 @@ namespace osu.Game.Online.API
/// </summary>
public event APIFailureHandler Failure;
private bool cancelled;
private readonly object completionStateLock = new object();
/// <summary>
/// The state of this request, from an outside perspective.
/// This is used to ensure correct notification events are fired.
/// </summary>
private APIRequestCompletionState completionState;
private Action pendingFailure;
@ -116,12 +122,7 @@ namespace osu.Game.Online.API
PostProcess();
API.Schedule(delegate
{
if (cancelled) return;
TriggerSuccess();
});
API.Schedule(TriggerSuccess);
}
/// <summary>
@ -131,16 +132,29 @@ namespace osu.Game.Online.API
{
}
private bool succeeded;
internal virtual void TriggerSuccess()
{
succeeded = true;
lock (completionStateLock)
{
if (completionState != APIRequestCompletionState.Waiting)
return;
completionState = APIRequestCompletionState.Completed;
}
Success?.Invoke();
}
internal void TriggerFailure(Exception e)
{
lock (completionStateLock)
{
if (completionState != APIRequestCompletionState.Waiting)
return;
completionState = APIRequestCompletionState.Failed;
}
Failure?.Invoke(e);
}
@ -148,10 +162,14 @@ namespace osu.Game.Online.API
public void Fail(Exception e)
{
if (succeeded || cancelled)
return;
lock (completionStateLock)
{
// while it doesn't matter if code following this check is run more than once,
// this avoids unnecessarily performing work where we are already sure the user has been informed.
if (completionState != APIRequestCompletionState.Waiting)
return;
}
cancelled = true;
WebRequest?.Abort();
string responseString = WebRequest?.GetResponseString();
@ -181,7 +199,11 @@ namespace osu.Game.Online.API
/// <returns>Whether we are in a failed or cancelled state.</returns>
private bool checkAndScheduleFailure()
{
if (pendingFailure == null) return cancelled;
lock (completionStateLock)
{
if (pendingFailure == null)
return completionState == APIRequestCompletionState.Failed;
}
if (API == null)
pendingFailure();
@ -199,14 +221,6 @@ namespace osu.Game.Online.API
}
}
public class APIException : InvalidOperationException
{
public APIException(string messsage, Exception innerException)
: base(messsage, innerException)
{
}
}
public delegate void APIFailureHandler(Exception e);
public delegate void APISuccessHandler();

View File

@ -0,0 +1,23 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
namespace osu.Game.Online.API
{
public enum APIRequestCompletionState
{
/// <summary>
/// Not yet run or currently waiting on response.
/// </summary>
Waiting,
/// <summary>
/// Ran to completion.
/// </summary>
Completed,
/// <summary>
/// Cancelled or failed due to error.
/// </summary>
Failed
}
}

View File

@ -26,9 +26,9 @@ namespace osu.Game.Online.API.Requests
public enum BeatmapSetType
{
Favourite,
RankedAndApproved,
Ranked,
Loved,
Unranked,
Pending,
Graveyard
}
}

View File

@ -15,6 +15,16 @@ namespace osu.Game.Online.Multiplayer
/// </summary>
/// <param name="roomId">The databased room ID.</param>
/// <exception cref="InvalidStateException">If the user is already in the requested (or another) room.</exception>
/// <exception cref="InvalidPasswordException">If the room required a password.</exception>
Task<MultiplayerRoom> JoinRoom(long roomId);
/// <summary>
/// Request to join a multiplayer room with a provided password.
/// </summary>
/// <param name="roomId">The databased room ID.</param>
/// <param name="password">The password for the join request.</param>
/// <exception cref="InvalidStateException">If the user is already in the requested (or another) room.</exception>
/// <exception cref="InvalidPasswordException">If the room provided password was incorrect.</exception>
Task<MultiplayerRoom> JoinRoomWithPassword(long roomId, string password);
}
}

View File

@ -0,0 +1,22 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Runtime.Serialization;
using Microsoft.AspNetCore.SignalR;
namespace osu.Game.Online.Multiplayer
{
[Serializable]
public class InvalidPasswordException : HubException
{
public InvalidPasswordException()
{
}
protected InvalidPasswordException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
}
}
}

View File

@ -92,7 +92,7 @@ namespace osu.Game.Online.Multiplayer
[Resolved]
private UserLookupCache userLookupCache { get; set; } = null!;
private Room? apiRoom;
protected Room? APIRoom { get; private set; }
[BackgroundDependencyLoader]
private void load()
@ -115,7 +115,8 @@ namespace osu.Game.Online.Multiplayer
/// 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)
/// <param name="password">An optional password to use for the join operation.</param>
public async Task JoinRoom(Room room, string? password = null)
{
var cancellationSource = joinCancellationSource = new CancellationTokenSource();
@ -127,7 +128,7 @@ namespace osu.Game.Online.Multiplayer
Debug.Assert(room.RoomID.Value != null);
// Join the server-side room.
var joinedRoom = await JoinRoom(room.RoomID.Value.Value).ConfigureAwait(false);
var joinedRoom = await JoinRoom(room.RoomID.Value.Value, password ?? room.Password.Value).ConfigureAwait(false);
Debug.Assert(joinedRoom != null);
// Populate users.
@ -138,7 +139,7 @@ namespace osu.Game.Online.Multiplayer
await scheduleAsync(() =>
{
Room = joinedRoom;
apiRoom = room;
APIRoom = room;
foreach (var user in joinedRoom.Users)
updateUserPlayingState(user.UserID, user.State);
}, cancellationSource.Token).ConfigureAwait(false);
@ -152,8 +153,9 @@ namespace osu.Game.Online.Multiplayer
/// Joins the <see cref="MultiplayerRoom"/> with a given ID.
/// </summary>
/// <param name="roomId">The room ID.</param>
/// <param name="password">An optional password to use when joining the room.</param>
/// <returns>The joined <see cref="MultiplayerRoom"/>.</returns>
protected abstract Task<MultiplayerRoom> JoinRoom(long roomId);
protected abstract Task<MultiplayerRoom> JoinRoom(long roomId, string? password = null);
public Task LeaveRoom()
{
@ -166,7 +168,7 @@ namespace osu.Game.Online.Multiplayer
// 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;
APIRoom = null;
Room = null;
CurrentMatchPlayingUserIds.Clear();
@ -189,8 +191,9 @@ namespace osu.Game.Online.Multiplayer
/// A room must be joined for this to have any effect.
/// </remarks>
/// <param name="name">The new room name, if any.</param>
/// <param name="password">The new password, 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)
public Task ChangeSettings(Optional<string> name = default, Optional<string> password = default, Optional<PlaylistItem> item = default)
{
if (Room == null)
throw new InvalidOperationException("Must be joined to a match to change settings.");
@ -212,6 +215,7 @@ namespace osu.Game.Online.Multiplayer
return ChangeSettings(new MultiplayerRoomSettings
{
Name = name.GetOr(Room.Settings.Name),
Password = password.GetOr(Room.Settings.Password),
BeatmapID = item.GetOr(existingPlaylistItem).BeatmapID,
BeatmapChecksum = item.GetOr(existingPlaylistItem).Beatmap.Value.MD5Hash,
RulesetID = item.GetOr(existingPlaylistItem).RulesetID,
@ -301,22 +305,22 @@ namespace osu.Game.Online.Multiplayer
if (Room == null)
return;
Debug.Assert(apiRoom != null);
Debug.Assert(APIRoom != null);
Room.State = state;
switch (state)
{
case MultiplayerRoomState.Open:
apiRoom.Status.Value = new RoomStatusOpen();
APIRoom.Status.Value = new RoomStatusOpen();
break;
case MultiplayerRoomState.Playing:
apiRoom.Status.Value = new RoomStatusPlaying();
APIRoom.Status.Value = new RoomStatusPlaying();
break;
case MultiplayerRoomState.Closed:
apiRoom.Status.Value = new RoomStatusEnded();
APIRoom.Status.Value = new RoomStatusEnded();
break;
}
@ -377,12 +381,12 @@ namespace osu.Game.Online.Multiplayer
if (Room == null)
return;
Debug.Assert(apiRoom != null);
Debug.Assert(APIRoom != null);
var user = Room.Users.FirstOrDefault(u => u.UserID == userId);
Room.Host = user;
apiRoom.Host.Value = user?.User;
APIRoom.Host.Value = user?.User;
RoomUpdated?.Invoke();
}, false);
@ -525,11 +529,12 @@ namespace osu.Game.Online.Multiplayer
if (Room == null)
return;
Debug.Assert(apiRoom != null);
Debug.Assert(APIRoom != null);
// Update a few properties of the room instantaneously.
Room.Settings = settings;
apiRoom.Name.Value = Room.Settings.Name;
APIRoom.Name.Value = Room.Settings.Name;
APIRoom.Password.Value = Room.Settings.Password;
// 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.
@ -551,7 +556,7 @@ namespace osu.Game.Online.Multiplayer
if (Room == null || !Room.Settings.Equals(settings))
return;
Debug.Assert(apiRoom != null);
Debug.Assert(APIRoom != null);
var beatmap = beatmapSet.Beatmaps.Single(b => b.OnlineBeatmapID == settings.BeatmapID);
beatmap.MD5Hash = settings.BeatmapChecksum;
@ -561,7 +566,7 @@ namespace osu.Game.Online.Multiplayer
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);
var playlistItem = APIRoom.Playlist.FirstOrDefault(i => i.ID == settings.PlaylistItemId);
if (playlistItem != null)
updateItem(playlistItem);
@ -569,7 +574,7 @@ namespace osu.Game.Online.Multiplayer
{
// An existing playlist item does not exist, so append a new one.
updateItem(playlistItem = new PlaylistItem());
apiRoom.Playlist.Add(playlistItem);
APIRoom.Playlist.Add(playlistItem);
}
CurrentMatchPlayingItem.Value = playlistItem;

View File

@ -36,12 +36,16 @@ namespace osu.Game.Online.Multiplayer
[Key(6)]
public long PlaylistItemId { get; set; }
[Key(7)]
public string Password { get; set; } = string.Empty;
public bool Equals(MultiplayerRoomSettings other)
=> BeatmapID == other.BeatmapID
&& BeatmapChecksum == other.BeatmapChecksum
&& RequiredMods.SequenceEqual(other.RequiredMods)
&& AllowedMods.SequenceEqual(other.AllowedMods)
&& RulesetID == other.RulesetID
&& Password.Equals(other.Password, StringComparison.Ordinal)
&& Name.Equals(other.Name, StringComparison.Ordinal)
&& PlaylistItemId == other.PlaylistItemId;
@ -49,6 +53,7 @@ namespace osu.Game.Online.Multiplayer
+ $" Beatmap:{BeatmapID} ({BeatmapChecksum})"
+ $" RequiredMods:{string.Join(',', RequiredMods)}"
+ $" AllowedMods:{string.Join(',', AllowedMods)}"
+ $" Password:{(string.IsNullOrEmpty(Password) ? "no" : "yes")}"
+ $" Ruleset:{RulesetID}"
+ $" Item:{PlaylistItemId}";
}

View File

@ -62,12 +62,12 @@ namespace osu.Game.Online.Multiplayer
}
}
protected override Task<MultiplayerRoom> JoinRoom(long roomId)
protected override Task<MultiplayerRoom> JoinRoom(long roomId, string? password = null)
{
if (!IsConnected.Value)
return Task.FromCanceled<MultiplayerRoom>(new CancellationToken(true));
return connection.InvokeAsync<MultiplayerRoom>(nameof(IMultiplayerServer.JoinRoom), roomId);
return connection.InvokeAsync<MultiplayerRoom>(nameof(IMultiplayerServer.JoinRoomWithPassword), roomId, password ?? string.Empty);
}
protected override Task LeaveRoomInternal()

View File

@ -70,7 +70,7 @@ namespace osu.Game.Online
return true;
}
// not ennough time has passed since the last poll. we do want to schedule a poll to happen, though.
// not enough time has passed since the last poll. we do want to schedule a poll to happen, though.
scheduleNextPoll();
return false;
}

View File

@ -9,11 +9,13 @@ namespace osu.Game.Online.Rooms
{
public class JoinRoomRequest : APIRequest
{
private readonly Room room;
public readonly Room Room;
public readonly string Password;
public JoinRoomRequest(Room room)
public JoinRoomRequest(Room room, string password)
{
this.room = room;
Room = room;
Password = password;
}
protected override WebRequest CreateWebRequest()
@ -23,6 +25,7 @@ namespace osu.Game.Online.Rooms
return req;
}
protected override string Target => $"rooms/{room.RoomID.Value}/users/{User.Id}";
// Todo: Password needs to be specified here rather than via AddParameter() because this is a PUT request. May be a framework bug.
protected override string Target => $"rooms/{Room.RoomID.Value}/users/{User.Id}?password={Password}";
}
}

View File

@ -10,10 +10,11 @@ using osu.Game.IO.Serialization.Converters;
using osu.Game.Online.Rooms.GameTypes;
using osu.Game.Online.Rooms.RoomStatuses;
using osu.Game.Users;
using osu.Game.Utils;
namespace osu.Game.Online.Rooms
{
public class Room
public class Room : IDeepCloneable<Room>
{
[Cached]
[JsonProperty("id")]
@ -48,10 +49,6 @@ namespace osu.Game.Online.Rooms
set => Category.Value = value;
}
[Cached]
[JsonIgnore]
public readonly Bindable<TimeSpan?> Duration = new Bindable<TimeSpan?>();
[Cached]
[JsonIgnore]
public readonly Bindable<int?> MaxAttempts = new Bindable<int?>();
@ -76,6 +73,9 @@ namespace osu.Game.Online.Rooms
[JsonProperty("current_user_score")]
public readonly Bindable<PlaylistAggregateScore> UserScore = new Bindable<PlaylistAggregateScore>();
[JsonProperty("has_password")]
public readonly BindableBool HasPassword = new BindableBool();
[Cached]
[JsonProperty("recent_participants")]
public readonly BindableList<User> RecentParticipants = new BindableList<User>();
@ -84,6 +84,16 @@ namespace osu.Game.Online.Rooms
[JsonProperty("participant_count")]
public readonly Bindable<int> ParticipantCount = new Bindable<int>();
#region Properties only used for room creation request
[Cached(Name = nameof(Password))]
[JsonProperty("password")]
public readonly Bindable<string> Password = new Bindable<string>();
[Cached]
[JsonIgnore]
public readonly Bindable<TimeSpan?> Duration = new Bindable<TimeSpan?>();
[JsonProperty("duration")]
private int? duration
{
@ -97,6 +107,8 @@ namespace osu.Game.Online.Rooms
}
}
#endregion
// Only supports retrieval for now
[Cached]
[JsonProperty("ends_at")]
@ -116,11 +128,16 @@ namespace osu.Game.Online.Rooms
[JsonIgnore]
public readonly Bindable<int> Position = new Bindable<int>(-1);
public Room()
{
Password.BindValueChanged(p => HasPassword.Value = !string.IsNullOrEmpty(p.NewValue));
}
/// <summary>
/// Create a copy of this room without online information.
/// Should be used to create a local copy of a room for submitting in the future.
/// </summary>
public Room CreateCopy()
public Room DeepClone()
{
var copy = new Room();
@ -144,6 +161,7 @@ namespace osu.Game.Online.Rooms
ChannelId.Value = other.ChannelId.Value;
Status.Value = other.Status.Value;
Availability.Value = other.Availability.Value;
HasPassword.Value = other.HasPassword.Value;
Type.Value = other.Type.Value;
MaxParticipants.Value = other.MaxParticipants.Value;
ParticipantCount.Value = other.ParticipantCount.Value;

View File

@ -13,6 +13,7 @@ using osu.Framework.Development;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.IO.Stores;
using osu.Framework.Platform;
using osu.Game.Beatmaps;
@ -341,7 +342,11 @@ namespace osu.Game
globalBindings = new GlobalActionContainer(this)
};
MenuCursorContainer.Child = content = new OsuTooltipContainer(MenuCursorContainer.Cursor) { RelativeSizeAxes = Axes.Both };
MenuCursorContainer.Child = new PopoverContainer
{
RelativeSizeAxes = Axes.Both,
Child = content = new OsuTooltipContainer(MenuCursorContainer.Cursor) { RelativeSizeAxes = Axes.Both }
};
base.Content.Add(CreateScalingContainer().WithChildren(mainContent));

View File

@ -429,7 +429,7 @@ namespace osu.Game.Overlays.Mods
if (!Stacked)
modEnumeration = ModUtils.FlattenMods(modEnumeration);
section.Mods = modEnumeration.Select(getValidModOrNull).Where(m => m != null).Select(m => m.CreateCopy());
section.Mods = modEnumeration.Select(getValidModOrNull).Where(m => m != null).Select(m => m.DeepClone());
}
updateSelectedButtons();

View File

@ -8,6 +8,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Textures;
using osu.Game.Overlays.Profile.Header.Components;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Users;
using osuTK;
@ -119,12 +120,12 @@ namespace osu.Game.Overlays.Profile.Header
{
hiddenDetailGlobal = new OverlinedInfoContainer
{
Title = "Global Ranking",
Title = UsersStrings.ShowRankGlobalSimple,
LineColour = colourProvider.Highlight1
},
hiddenDetailCountry = new OverlinedInfoContainer
{
Title = "Country Ranking",
Title = UsersStrings.ShowRankCountrySimple,
LineColour = colourProvider.Highlight1
},
}

View File

@ -10,6 +10,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Graphics.UserInterface;
using osu.Game.Resources.Localisation.Web;
using osuTK;
namespace osu.Game.Overlays.Profile.Header.Components
@ -18,7 +19,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
{
public readonly BindableBool DetailsVisible = new BindableBool();
public override LocalisableString TooltipText => DetailsVisible.Value ? "collapse" : "expand";
public override LocalisableString TooltipText => DetailsVisible.Value ? CommonStrings.ButtonsCollapse : CommonStrings.ButtonsExpand;
private SpriteIcon icon;
private Sample sampleOpen;

View File

@ -5,6 +5,7 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Users;
namespace osu.Game.Overlays.Profile.Header.Components
@ -13,7 +14,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
{
public readonly Bindable<User> User = new Bindable<User>();
public override LocalisableString TooltipText => "followers";
public override LocalisableString TooltipText => FriendsStrings.ButtonsDisabled;
protected override IconUsage Icon => FontAwesome.Solid.User;

View File

@ -11,6 +11,7 @@ using osu.Framework.Graphics.Textures;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Users;
namespace osu.Game.Overlays.Profile.Header.Components
@ -19,13 +20,13 @@ namespace osu.Game.Overlays.Profile.Header.Components
{
public readonly Bindable<User> User = new Bindable<User>();
public LocalisableString TooltipText { get; }
public LocalisableString TooltipText { get; private set; }
private OsuSpriteText levelText;
public LevelBadge()
{
TooltipText = "level";
TooltipText = UsersStrings.ShowStatsLevel("0");
}
[BackgroundDependencyLoader]
@ -53,6 +54,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
private void updateLevel(User user)
{
levelText.Text = user?.Statistics?.Level.Current.ToString() ?? "0";
TooltipText = UsersStrings.ShowStatsLevel(user?.Statistics?.Level.Current.ToString());
}
}
}

View File

@ -10,6 +10,7 @@ using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Users;
using osuTK.Graphics;
@ -26,7 +27,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
public LevelProgressBar()
{
TooltipText = "progress to next level";
TooltipText = UsersStrings.ShowStatsLevelProgress;
}
[BackgroundDependencyLoader]

View File

@ -5,6 +5,7 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Users;
namespace osu.Game.Overlays.Profile.Header.Components
@ -13,7 +14,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
{
public readonly Bindable<User> User = new Bindable<User>();
public override LocalisableString TooltipText => "mapping subscribers";
public override LocalisableString TooltipText => FollowsStrings.MappingFollowers;
protected override IconUsage Icon => FontAwesome.Solid.Bell;

View File

@ -8,6 +8,7 @@ using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Online.API;
using osu.Game.Online.Chat;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Users;
using osuTK;
@ -17,7 +18,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
{
public readonly Bindable<User> User = new Bindable<User>();
public override LocalisableString TooltipText => "send message";
public override LocalisableString TooltipText => UsersStrings.CardSendMessage;
[Resolved(CanBeNull = true)]
private ChannelManager channelManager { get; set; }

View File

@ -4,6 +4,7 @@
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osuTK.Graphics;
@ -16,7 +17,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
private readonly OsuSpriteText title;
private readonly OsuSpriteText content;
public string Title
public LocalisableString Title
{
set => title.Text = value;
}

View File

@ -7,6 +7,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Localisation;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Users;
namespace osu.Game.Overlays.Profile.Header.Components
@ -31,7 +32,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
{
InternalChild = info = new OverlinedInfoContainer
{
Title = "Total Play Time",
Title = UsersStrings.ShowStatsPlayTime,
LineColour = colourProvider.Highlight1,
};

View File

@ -12,6 +12,7 @@ using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Users;
using osuTK;
@ -68,7 +69,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Text = @"formerly known as",
Text = UsersStrings.ShowPreviousUsernames,
Font = OsuFont.GetFont(size: 10, italics: true)
}
},

View File

@ -9,6 +9,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Users;
namespace osu.Game.Overlays.Profile.Header.Components
@ -27,7 +28,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Text = "No recent plays",
Text = UsersStrings.ShowExtraUnranked,
Font = OsuFont.GetFont(size: 12, weight: FontWeight.Regular)
});
}
@ -74,7 +75,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
private class RankGraphTooltip : UserGraphTooltip
{
public RankGraphTooltip()
: base("Global Ranking")
: base(UsersStrings.ShowRankGlobalSimple)
{
}

View File

@ -10,6 +10,7 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Overlays.Profile.Header.Components
{
@ -19,7 +20,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
private readonly FillFlowContainer iconContainer;
private readonly CircularContainer content;
public LocalisableString TooltipText => "osu!supporter";
public LocalisableString TooltipText => UsersStrings.ShowIsSupporter;
public int SupportLevel
{

View File

@ -11,6 +11,7 @@ using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.Leaderboards;
using osu.Game.Overlays.Profile.Header.Components;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Scoring;
using osu.Game.Users;
using osuTK;
@ -100,7 +101,7 @@ namespace osu.Game.Overlays.Profile.Header
},
medalInfo = new OverlinedInfoContainer
{
Title = "Medals",
Title = UsersStrings.ShowStatsMedals,
LineColour = colours.GreenLight,
},
ppInfo = new OverlinedInfoContainer
@ -151,12 +152,12 @@ namespace osu.Game.Overlays.Profile.Header
{
detailGlobalRank = new OverlinedInfoContainer(true, 110)
{
Title = "Global Ranking",
Title = UsersStrings.ShowRankGlobalSimple,
LineColour = colourProvider.Highlight1,
},
detailCountryRank = new OverlinedInfoContainer(false, 110)
{
Title = "Country Ranking",
Title = UsersStrings.ShowRankCountrySimple,
LineColour = colourProvider.Highlight1,
},
}

View File

@ -7,11 +7,13 @@ using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Overlays.Profile.Header.Components;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Users;
using osu.Game.Users.Drawables;
using osuTK;
@ -179,19 +181,19 @@ namespace osu.Game.Overlays.Profile.Header
if (user?.Statistics != null)
{
userStats.Add(new UserStatsLine("Ranked Score", user.Statistics.RankedScore.ToString("#,##0")));
userStats.Add(new UserStatsLine("Hit Accuracy", user.Statistics.DisplayAccuracy));
userStats.Add(new UserStatsLine("Play Count", user.Statistics.PlayCount.ToString("#,##0")));
userStats.Add(new UserStatsLine("Total Score", user.Statistics.TotalScore.ToString("#,##0")));
userStats.Add(new UserStatsLine("Total Hits", user.Statistics.TotalHits.ToString("#,##0")));
userStats.Add(new UserStatsLine("Maximum Combo", user.Statistics.MaxCombo.ToString("#,##0")));
userStats.Add(new UserStatsLine("Replays Watched by Others", user.Statistics.ReplaysWatched.ToString("#,##0")));
userStats.Add(new UserStatsLine(UsersStrings.ShowStatsRankedScore, user.Statistics.RankedScore.ToString("#,##0")));
userStats.Add(new UserStatsLine(UsersStrings.ShowStatsHitAccuracy, user.Statistics.DisplayAccuracy));
userStats.Add(new UserStatsLine(UsersStrings.ShowStatsPlayCount, user.Statistics.PlayCount.ToString("#,##0")));
userStats.Add(new UserStatsLine(UsersStrings.ShowStatsTotalScore, user.Statistics.TotalScore.ToString("#,##0")));
userStats.Add(new UserStatsLine(UsersStrings.ShowStatsTotalHits, user.Statistics.TotalHits.ToString("#,##0")));
userStats.Add(new UserStatsLine(UsersStrings.ShowStatsMaximumCombo, user.Statistics.MaxCombo.ToString("#,##0")));
userStats.Add(new UserStatsLine(UsersStrings.ShowStatsReplaysWatchedByOthers, user.Statistics.ReplaysWatched.ToString("#,##0")));
}
}
private class UserStatsLine : Container
{
public UserStatsLine(string left, string right)
public UserStatsLine(LocalisableString left, string right)
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;

View File

@ -7,12 +7,14 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Localisation;
using osu.Game.Overlays.Profile.Header;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Users;
namespace osu.Game.Overlays.Profile
{
public class ProfileHeader : TabControlOverlayHeader<string>
public class ProfileHeader : TabControlOverlayHeader<LocalisableString>
{
private UserCoverBackground coverContainer;
@ -27,8 +29,8 @@ namespace osu.Game.Overlays.Profile
User.ValueChanged += e => updateDisplay(e.NewValue);
TabControl.AddItem("info");
TabControl.AddItem("modding");
TabControl.AddItem(LayoutStrings.HeaderUsersShow);
TabControl.AddItem(LayoutStrings.HeaderUsersModding);
centreHeaderContainer.DetailsVisible.BindValueChanged(visible => detailHeaderContainer.Expanded = visible.NewValue, true);
}
@ -96,7 +98,7 @@ namespace osu.Game.Overlays.Profile
{
public ProfileHeaderTitle()
{
Title = "player info";
Title = PageTitleStrings.MainUsersControllerDefault;
IconTexture = "Icons/Hexacons/profile";
}
}

View File

@ -8,6 +8,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Backgrounds;
using osu.Game.Graphics.Sprites;
@ -17,7 +18,7 @@ namespace osu.Game.Overlays.Profile
{
public abstract class ProfileSection : Container
{
public abstract string Title { get; }
public abstract LocalisableString Title { get; }
public abstract string Identifier { get; }

View File

@ -1,12 +1,15 @@
// 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.Localisation;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Overlays.Profile.Sections
{
public class AboutSection : ProfileSection
{
public override string Title => "me!";
public override LocalisableString Title => UsersStrings.ShowExtraMeTitle;
public override string Identifier => "me";
public override string Identifier => @"me";
}
}

View File

@ -5,6 +5,7 @@ using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Localisation;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
@ -19,7 +20,7 @@ namespace osu.Game.Overlays.Profile.Sections.Beatmaps
private const float panel_padding = 10f;
private readonly BeatmapSetType type;
public PaginatedBeatmapContainer(BeatmapSetType type, Bindable<User> user, string headerText)
public PaginatedBeatmapContainer(BeatmapSetType type, Bindable<User> user, LocalisableString headerText)
: base(user, headerText)
{
this.type = type;
@ -45,11 +46,11 @@ namespace osu.Game.Overlays.Profile.Sections.Beatmaps
case BeatmapSetType.Loved:
return user.LovedBeatmapsetCount;
case BeatmapSetType.RankedAndApproved:
return user.RankedAndApprovedBeatmapsetCount;
case BeatmapSetType.Ranked:
return user.RankedBeatmapsetCount;
case BeatmapSetType.Unranked:
return user.UnrankedBeatmapsetCount;
case BeatmapSetType.Pending:
return user.PendingBeatmapsetCount;
default:
return 0;

View File

@ -1,26 +1,28 @@
// 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.Localisation;
using osu.Game.Online.API.Requests;
using osu.Game.Overlays.Profile.Sections.Beatmaps;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Overlays.Profile.Sections
{
public class BeatmapsSection : ProfileSection
{
public override string Title => "Beatmaps";
public override LocalisableString Title => UsersStrings.ShowExtraBeatmapsTitle;
public override string Identifier => "beatmaps";
public override string Identifier => @"beatmaps";
public BeatmapsSection()
{
Children = new[]
{
new PaginatedBeatmapContainer(BeatmapSetType.Favourite, User, "Favourite Beatmaps"),
new PaginatedBeatmapContainer(BeatmapSetType.RankedAndApproved, User, "Ranked & Approved Beatmaps"),
new PaginatedBeatmapContainer(BeatmapSetType.Loved, User, "Loved Beatmaps"),
new PaginatedBeatmapContainer(BeatmapSetType.Unranked, User, "Pending Beatmaps"),
new PaginatedBeatmapContainer(BeatmapSetType.Graveyard, User, "Graveyarded Beatmaps")
new PaginatedBeatmapContainer(BeatmapSetType.Favourite, User, UsersStrings.ShowExtraBeatmapsFavouriteTitle),
new PaginatedBeatmapContainer(BeatmapSetType.Ranked, User, UsersStrings.ShowExtraBeatmapsRankedTitle),
new PaginatedBeatmapContainer(BeatmapSetType.Loved, User, UsersStrings.ShowExtraBeatmapsLovedTitle),
new PaginatedBeatmapContainer(BeatmapSetType.Pending, User, UsersStrings.ShowExtraBeatmapsPendingTitle),
new PaginatedBeatmapContainer(BeatmapSetType.Graveyard, User, UsersStrings.ShowExtraBeatmapsGraveyardTitle)
};
}
}

View File

@ -6,6 +6,7 @@ using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Localisation;
using osu.Game.Users;
using static osu.Game.Users.User;
@ -18,9 +19,9 @@ namespace osu.Game.Overlays.Profile.Sections.Historical
/// <summary>
/// Text describing the value being plotted on the graph, which will be displayed as a prefix to the value in the history graph tooltip.
/// </summary>
protected abstract string GraphCounterName { get; }
protected abstract LocalisableString GraphCounterName { get; }
protected ChartProfileSubsection(Bindable<User> user, string headerText)
protected ChartProfileSubsection(Bindable<User> user, LocalisableString headerText)
: base(user, headerText)
{
}

View File

@ -13,6 +13,7 @@ using osu.Game.Graphics.Sprites;
using osuTK;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Localisation;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Overlays.Profile.Sections.Historical
{
@ -143,7 +144,7 @@ namespace osu.Game.Overlays.Profile.Sections.Historical
private class PlayCountText : CompositeDrawable, IHasTooltip
{
public LocalisableString TooltipText => "times played";
public LocalisableString TooltipText => UsersStrings.ShowExtraHistoricalMostPlayedCount;
public PlayCountText(int playCount)
{

View File

@ -9,6 +9,7 @@ using osu.Framework.Graphics.Containers;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Users;
namespace osu.Game.Overlays.Profile.Sections.Historical
@ -16,7 +17,7 @@ namespace osu.Game.Overlays.Profile.Sections.Historical
public class PaginatedMostPlayedBeatmapContainer : PaginatedProfileSubsection<APIUserMostPlayedBeatmap>
{
public PaginatedMostPlayedBeatmapContainer(Bindable<User> user)
: base(user, "Most Played Beatmaps")
: base(user, UsersStrings.ShowExtraHistoricalMostPlayedTitle)
{
ItemsPerPage = 5;
}

View File

@ -2,6 +2,8 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables;
using osu.Framework.Localisation;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Users;
using static osu.Game.Users.User;
@ -9,10 +11,10 @@ namespace osu.Game.Overlays.Profile.Sections.Historical
{
public class PlayHistorySubsection : ChartProfileSubsection
{
protected override string GraphCounterName => "Plays";
protected override LocalisableString GraphCounterName => UsersStrings.ShowExtraHistoricalMonthlyPlaycountsCountLabel;
public PlayHistorySubsection(Bindable<User> user)
: base(user, "Play History")
: base(user, UsersStrings.ShowExtraHistoricalMonthlyPlaycountsTitle)
{
}

View File

@ -12,6 +12,7 @@ using osu.Framework.Allocation;
using osu.Game.Graphics;
using osu.Framework.Graphics.Shapes;
using osuTK;
using osu.Framework.Localisation;
using static osu.Game.Users.User;
namespace osu.Game.Overlays.Profile.Sections.Historical
@ -42,7 +43,7 @@ namespace osu.Game.Overlays.Profile.Sections.Historical
private readonly Container<TickLine> rowLinesContainer;
private readonly Container<TickLine> columnLinesContainer;
public ProfileLineChart(string graphCounterName)
public ProfileLineChart(LocalisableString graphCounterName)
{
RelativeSizeAxes = Axes.X;
Height = 250;

View File

@ -2,6 +2,8 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables;
using osu.Framework.Localisation;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Users;
using static osu.Game.Users.User;
@ -9,10 +11,10 @@ namespace osu.Game.Overlays.Profile.Sections.Historical
{
public class ReplaysSubsection : ChartProfileSubsection
{
protected override string GraphCounterName => "Replays Watched";
protected override LocalisableString GraphCounterName => UsersStrings.ShowExtraHistoricalReplaysWatchedCountsCountLabel;
public ReplaysSubsection(Bindable<User> user)
: base(user, "Replays Watched History")
: base(user, UsersStrings.ShowExtraHistoricalReplaysWatchedCountsTitle)
{
}

View File

@ -5,13 +5,14 @@ using System;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Localisation;
using static osu.Game.Users.User;
namespace osu.Game.Overlays.Profile.Sections.Historical
{
public class UserHistoryGraph : UserGraph<DateTime, long>
{
private readonly string tooltipCounterName;
private readonly LocalisableString tooltipCounterName;
[CanBeNull]
public UserHistoryCount[] Values
@ -19,7 +20,7 @@ namespace osu.Game.Overlays.Profile.Sections.Historical
set => Data = value?.Select(v => new KeyValuePair<DateTime, long>(v.Date, v.Count)).ToArray();
}
public UserHistoryGraph(string tooltipCounterName)
public UserHistoryGraph(LocalisableString tooltipCounterName)
{
this.tooltipCounterName = tooltipCounterName;
}
@ -40,9 +41,9 @@ namespace osu.Game.Overlays.Profile.Sections.Historical
protected class HistoryGraphTooltip : UserGraphTooltip
{
private readonly string tooltipCounterName;
private readonly LocalisableString tooltipCounterName;
public HistoryGraphTooltip(string tooltipCounterName)
public HistoryGraphTooltip(LocalisableString tooltipCounterName)
: base(tooltipCounterName)
{
this.tooltipCounterName = tooltipCounterName;
@ -61,7 +62,7 @@ namespace osu.Game.Overlays.Profile.Sections.Historical
private class TooltipDisplayContent
{
public string Name;
public LocalisableString Name;
public string Count;
public string Date;
}

View File

@ -2,17 +2,19 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Localisation;
using osu.Game.Online.API.Requests;
using osu.Game.Overlays.Profile.Sections.Historical;
using osu.Game.Overlays.Profile.Sections.Ranks;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Overlays.Profile.Sections
{
public class HistoricalSection : ProfileSection
{
public override string Title => "Historical";
public override LocalisableString Title => UsersStrings.ShowExtraHistoricalTitle;
public override string Identifier => "historical";
public override string Identifier => @"historical";
public HistoricalSection()
{
@ -20,7 +22,7 @@ namespace osu.Game.Overlays.Profile.Sections
{
new PlayHistorySubsection(User),
new PaginatedMostPlayedBeatmapContainer(User),
new PaginatedScoreContainer(ScoreType.Recent, User, "Recent Plays (24h)"),
new PaginatedScoreContainer(ScoreType.Recent, User, UsersStrings.ShowExtraHistoricalRecentPlaysTitle),
new ReplaysSubsection(User)
};
}

View File

@ -12,6 +12,8 @@ using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Users;
using osu.Framework.Allocation;
using osu.Game.Resources.Localisation.Web;
using osu.Framework.Localisation;
namespace osu.Game.Overlays.Profile.Sections.Kudosu
{
@ -37,7 +39,7 @@ namespace osu.Game.Overlays.Profile.Sections.Kudosu
private class CountTotal : CountSection
{
public CountTotal()
: base("Total Kudosu Earned")
: base(UsersStrings.ShowExtraKudosuTotal)
{
DescriptionText.AddText("Based on how much of a contribution the user has made to beatmap moderation. See ");
DescriptionText.AddLink("this page", "https://osu.ppy.sh/wiki/Kudosu");
@ -56,7 +58,7 @@ namespace osu.Game.Overlays.Profile.Sections.Kudosu
set => valueText.Text = value.ToString("N0");
}
public CountSection(string header)
public CountSection(LocalisableString header)
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;

View File

@ -8,13 +8,14 @@ using osu.Framework.Bindables;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.API;
using System.Collections.Generic;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Overlays.Profile.Sections.Kudosu
{
public class PaginatedKudosuHistoryContainer : PaginatedProfileSubsection<APIKudosuHistory>
{
public PaginatedKudosuHistoryContainer(Bindable<User> user)
: base(user, missingText: "This user hasn't received any kudosu!")
: base(user, missingText: UsersStrings.ShowExtraKudosuEntryEmpty)
{
ItemsPerPage = 5;
}

View File

@ -3,14 +3,16 @@
using osu.Framework.Graphics;
using osu.Game.Overlays.Profile.Sections.Kudosu;
using osu.Framework.Localisation;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Overlays.Profile.Sections
{
public class KudosuSection : ProfileSection
{
public override string Title => "Kudosu!";
public override LocalisableString Title => UsersStrings.ShowExtraKudosuTitle;
public override string Identifier => "kudosu";
public override string Identifier => @"kudosu";
public KudosuSection()
{

View File

@ -1,12 +1,15 @@
// 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.Localisation;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Overlays.Profile.Sections
{
public class MedalsSection : ProfileSection
{
public override string Title => "Medals";
public override LocalisableString Title => UsersStrings.ShowExtraMedalsTitle;
public override string Identifier => "medals";
public override string Identifier => @"medals";
}
}

View File

@ -15,6 +15,7 @@ using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Framework.Localisation;
namespace osu.Game.Overlays.Profile.Sections
{
@ -36,9 +37,9 @@ namespace osu.Game.Overlays.Profile.Sections
private ShowMoreButton moreButton;
private OsuSpriteText missing;
private readonly string missingText;
private readonly LocalisableString? missingText;
protected PaginatedProfileSubsection(Bindable<User> user, string headerText = "", string missingText = "")
protected PaginatedProfileSubsection(Bindable<User> user, LocalisableString? headerText = null, LocalisableString? missingText = null)
: base(user, headerText, CounterVisibilityState.AlwaysVisible)
{
this.missingText = missingText;
@ -68,7 +69,7 @@ namespace osu.Game.Overlays.Profile.Sections
missing = new OsuSpriteText
{
Font = OsuFont.GetFont(size: 15),
Text = missingText,
Text = missingText ?? string.Empty,
Alpha = 0,
}
}
@ -114,7 +115,7 @@ namespace osu.Game.Overlays.Profile.Sections
moreButton.Hide();
moreButton.IsLoading = false;
if (!string.IsNullOrEmpty(missingText))
if (missingText.HasValue)
missing.Show();
return;

View File

@ -7,6 +7,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Users;
using JetBrains.Annotations;
using osu.Framework.Localisation;
namespace osu.Game.Overlays.Profile.Sections
{
@ -14,14 +15,14 @@ namespace osu.Game.Overlays.Profile.Sections
{
protected readonly Bindable<User> User = new Bindable<User>();
private readonly string headerText;
private readonly LocalisableString headerText;
private readonly CounterVisibilityState counterVisibilityState;
private ProfileSubsectionHeader header;
protected ProfileSubsection(Bindable<User> user, string headerText = "", CounterVisibilityState counterVisibilityState = CounterVisibilityState.AlwaysHidden)
protected ProfileSubsection(Bindable<User> user, LocalisableString? headerText = null, CounterVisibilityState counterVisibilityState = CounterVisibilityState.AlwaysHidden)
{
this.headerText = headerText;
this.headerText = headerText ?? string.Empty;
this.counterVisibilityState = counterVisibilityState;
User.BindTo(user);
}
@ -37,7 +38,7 @@ namespace osu.Game.Overlays.Profile.Sections
{
header = new ProfileSubsectionHeader(headerText, counterVisibilityState)
{
Alpha = string.IsNullOrEmpty(headerText) ? 0 : 1
Alpha = string.IsNullOrEmpty(headerText.ToString()) ? 0 : 1
},
CreateContent()
};

View File

@ -11,6 +11,7 @@ using osu.Framework.Graphics.Shapes;
using osuTK;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Framework.Localisation;
namespace osu.Game.Overlays.Profile.Sections
{
@ -24,12 +25,12 @@ namespace osu.Game.Overlays.Profile.Sections
set => current.Current = value;
}
private readonly string text;
private readonly LocalisableString text;
private readonly CounterVisibilityState counterState;
private CounterPill counterPill;
public ProfileSubsectionHeader(string text, CounterVisibilityState counterState)
public ProfileSubsectionHeader(LocalisableString text, CounterVisibilityState counterState)
{
this.text = text;
this.counterState = counterState;

View File

@ -5,6 +5,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Scoring;
using osuTK;
@ -51,7 +52,7 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks
new OsuSpriteText
{
Font = OsuFont.GetFont(size: 12),
Text = $@"weighted {weight:0%}"
Text = UsersStrings.ShowExtraTopRanksPpWeight(weight.ToString("0%"))
}
}
};

View File

@ -11,6 +11,7 @@ using osu.Game.Online.API.Requests.Responses;
using System.Collections.Generic;
using osu.Game.Online.API;
using osu.Framework.Allocation;
using osu.Framework.Localisation;
namespace osu.Game.Overlays.Profile.Sections.Ranks
{
@ -18,7 +19,7 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks
{
private readonly ScoreType type;
public PaginatedScoreContainer(ScoreType type, Bindable<User> user, string headerText)
public PaginatedScoreContainer(ScoreType type, Bindable<User> user, LocalisableString headerText)
: base(user, headerText)
{
this.type = type;

View File

@ -3,21 +3,23 @@
using osu.Game.Overlays.Profile.Sections.Ranks;
using osu.Game.Online.API.Requests;
using osu.Framework.Localisation;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Overlays.Profile.Sections
{
public class RanksSection : ProfileSection
{
public override string Title => "Ranks";
public override LocalisableString Title => UsersStrings.ShowExtraTopRanksTitle;
public override string Identifier => "top_ranks";
public override string Identifier => @"top_ranks";
public RanksSection()
{
Children = new[]
{
new PaginatedScoreContainer(ScoreType.Best, User, "Best Performance"),
new PaginatedScoreContainer(ScoreType.Firsts, User, "First Place Ranks")
new PaginatedScoreContainer(ScoreType.Best, User, UsersStrings.ShowExtraTopRanksBestTitle),
new PaginatedScoreContainer(ScoreType.Firsts, User, UsersStrings.ShowExtraTopRanksFirstTitle)
};
}
}

View File

@ -10,13 +10,14 @@ using osu.Game.Online.API;
using System.Collections.Generic;
using osuTK;
using osu.Framework.Allocation;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Overlays.Profile.Sections.Recent
{
public class PaginatedRecentActivityContainer : PaginatedProfileSubsection<APIRecentActivity>
{
public PaginatedRecentActivityContainer(Bindable<User> user)
: base(user, missingText: "This user hasn't done anything notable recently!")
: base(user, missingText: EventsStrings.Empty)
{
ItemsPerPage = 10;
}

View File

@ -1,15 +1,17 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Localisation;
using osu.Game.Overlays.Profile.Sections.Recent;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Overlays.Profile.Sections
{
public class RecentSection : ProfileSection
{
public override string Title => "Recent";
public override LocalisableString Title => UsersStrings.ShowExtraRecentActivityTitle;
public override string Identifier => "recent_activity";
public override string Identifier => @"recent_activity";
public RecentSection()
{

View File

@ -11,6 +11,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
@ -211,7 +212,7 @@ namespace osu.Game.Overlays.Profile
protected readonly OsuSpriteText Counter, BottomText;
private readonly Box background;
protected UserGraphTooltip(string tooltipCounterName)
protected UserGraphTooltip(LocalisableString tooltipCounterName)
{
AutoSizeAxes = Axes.Both;
Masking = true;

View File

@ -2,11 +2,13 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Game.Rulesets.Replays;
using osu.Game.Utils;
namespace osu.Game.Replays
{
public class Replay
public class Replay : IDeepCloneable<Replay>
{
/// <summary>
/// Whether all frames for this replay have been received.
@ -15,5 +17,15 @@ namespace osu.Game.Replays
public bool HasReceivedAllFrames = true;
public List<ReplayFrame> Frames = new List<ReplayFrame>();
public Replay DeepClone()
{
return new Replay
{
HasReceivedAllFrames = HasReceivedAllFrames,
// individual frames are mutable for now but hopefully this will not be a thing in the future.
Frames = Frames.ToList(),
};
}
}
}

View File

@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Difficulty
/// <returns>A structure describing the difficulty of the beatmap.</returns>
public DifficultyAttributes Calculate(params Mod[] mods)
{
mods = mods.Select(m => m.CreateCopy()).ToArray();
mods = mods.Select(m => m.DeepClone()).ToArray();
IBeatmap playableBeatmap = beatmap.GetPlayableBeatmap(ruleset.RulesetInfo, mods);

View File

@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Mods
/// The base class for gameplay modifiers.
/// </summary>
[ExcludeFromDynamicCompile]
public abstract class Mod : IMod, IEquatable<Mod>, IJsonSerializable
public abstract class Mod : IMod, IEquatable<Mod>, IJsonSerializable, IDeepCloneable<Mod>
{
/// <summary>
/// The name of this mod.
@ -132,7 +132,7 @@ namespace osu.Game.Rulesets.Mods
/// <summary>
/// Creates a copy of this <see cref="Mod"/> initialised to a default state.
/// </summary>
public virtual Mod CreateCopy()
public virtual Mod DeepClone()
{
var result = (Mod)Activator.CreateInstance(GetType());
result.CopyFrom(this);

View File

@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Mods
Mods = mods;
}
public override Mod CreateCopy() => new MultiMod(Mods.Select(m => m.CreateCopy()).ToArray());
public override Mod DeepClone() => new MultiMod(Mods.Select(m => m.DeepClone()).ToArray());
public override Type[] IncompatibleMods => Mods.SelectMany(m => m.IncompatibleMods).ToArray();
}

View File

@ -502,8 +502,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
{
if (!(HitObject is IHasComboInformation combo)) return;
var comboColours = CurrentSkin.GetConfig<GlobalSkinColours, IReadOnlyList<Color4>>(GlobalSkinColours.ComboColours)?.Value ?? Array.Empty<Color4>();
AccentColour.Value = combo.GetComboColour(comboColours);
AccentColour.Value = combo.GetComboColour(CurrentSkin);
}
/// <summary>

View File

@ -1,9 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using JetBrains.Annotations;
using osu.Framework.Bindables;
using osu.Game.Skinning;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Objects.Types
@ -40,11 +39,21 @@ namespace osu.Game.Rulesets.Objects.Types
bool LastInCombo { get; set; }
/// <summary>
/// Retrieves the colour of the combo described by this <see cref="IHasComboInformation"/> object from a set of possible combo colours.
/// Defaults to using <see cref="ComboIndex"/> to decide the colour.
/// Retrieves the colour of the combo described by this <see cref="IHasComboInformation"/> object.
/// </summary>
/// <param name="comboColours">A list of possible combo colours provided by the beatmap or skin.</param>
/// <returns>The colour of the combo described by this <see cref="IHasComboInformation"/> object.</returns>
Color4 GetComboColour([NotNull] IReadOnlyList<Color4> comboColours) => comboColours.Count > 0 ? comboColours[ComboIndex % comboColours.Count] : Color4.White;
/// <param name="skin">The skin to retrieve the combo colour from, if wanted.</param>
Color4 GetComboColour(ISkin skin) => GetSkinComboColour(this, skin, ComboIndex);
/// <summary>
/// Retrieves the colour of the combo described by a given <see cref="IHasComboInformation"/> object from a given skin.
/// </summary>
/// <param name="combo">The combo information, should be <c>this</c>.</param>
/// <param name="skin">The skin to retrieve the combo colour from.</param>
/// <param name="comboIndex">The index to retrieve the combo colour with.</param>
/// <returns></returns>
protected static Color4 GetSkinComboColour(IHasComboInformation combo, ISkin skin, int comboIndex)
{
return skin.GetConfig<SkinComboColourLookup, Color4>(new SkinComboColourLookup(comboIndex, combo))?.Value ?? Color4.White;
}
}
}

View File

@ -338,7 +338,6 @@ namespace osu.Game.Rulesets.Scoring
score.MaxCombo = HighestCombo.Value;
score.Accuracy = Accuracy.Value;
score.Rank = Rank.Value;
score.Date = DateTimeOffset.Now;
foreach (var result in Enum.GetValues(typeof(HitResult)).OfType<HitResult>().Where(r => r.IsScorable()))
score.Statistics[result] = GetStatistic(result);

View File

@ -2,12 +2,22 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Replays;
using osu.Game.Utils;
namespace osu.Game.Scoring
{
public class Score
public class Score : IDeepCloneable<Score>
{
public ScoreInfo ScoreInfo = new ScoreInfo();
public Replay Replay = new Replay();
public Score DeepClone()
{
return new Score
{
ScoreInfo = ScoreInfo.DeepClone(),
Replay = Replay.DeepClone(),
};
}
}
}

View File

@ -18,7 +18,7 @@ using osu.Game.Utils;
namespace osu.Game.Scoring
{
public class ScoreInfo : IHasFiles<ScoreFileInfo>, IHasPrimaryKey, ISoftDelete, IEquatable<ScoreInfo>
public class ScoreInfo : IHasFiles<ScoreFileInfo>, IHasPrimaryKey, ISoftDelete, IEquatable<ScoreInfo>, IDeepCloneable<ScoreInfo>
{
public int ID { get; set; }
@ -242,6 +242,15 @@ namespace osu.Game.Scoring
}
}
public ScoreInfo DeepClone()
{
var clone = (ScoreInfo)MemberwiseClone();
clone.Statistics = new Dictionary<HitResult, int>(clone.Statistics);
return clone;
}
public override string ToString() => $"{User} playing {Beatmap}";
public bool Equals(ScoreInfo other)

View File

@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@ -153,11 +152,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
break;
case IHasComboInformation combo:
{
var comboColours = skin.GetConfig<GlobalSkinColours, IReadOnlyList<Color4>>(GlobalSkinColours.ComboColours)?.Value ?? Array.Empty<Color4>();
colour = combo.GetComboColour(comboColours);
colour = combo.GetComboColour(skin);
break;
}
default:
return;

View File

@ -128,7 +128,7 @@ namespace osu.Game.Screens.Edit
// clone these locally for now to avoid incurring overhead on GetPlayableBeatmap usages.
// eventually we will want to improve how/where this is done as there are issues with *not* cloning it in all cases.
playableBeatmap.ControlPointInfo = playableBeatmap.ControlPointInfo.CreateCopy();
playableBeatmap.ControlPointInfo = playableBeatmap.ControlPointInfo.DeepClone();
}
catch (Exception e)
{

View File

@ -72,8 +72,6 @@ namespace osu.Game.Screens.Menu
set => colourAndTriangles.FadeTo(value ? 1 : 0, transition_length, Easing.OutQuint);
}
public bool BeatMatching = true;
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => logoContainer.ReceivePositionalInputAt(screenSpacePos);
public bool Ripple
@ -272,8 +270,6 @@ namespace osu.Game.Screens.Menu
{
base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes);
if (!BeatMatching) return;
lastBeatIndex = beatIndex;
var beatLength = timingPoint.BeatLength;

View File

@ -84,10 +84,10 @@ namespace osu.Game.Screens.OnlinePlay.Components
private JoinRoomRequest currentJoinRoomRequest;
public virtual void JoinRoom(Room room, Action<Room> onSuccess = null, Action<string> onError = null)
public virtual void JoinRoom(Room room, string password = null, Action<Room> onSuccess = null, Action<string> onError = null)
{
currentJoinRoomRequest?.Cancel();
currentJoinRoomRequest = new JoinRoomRequest(room);
currentJoinRoomRequest = new JoinRoomRequest(room, password);
currentJoinRoomRequest.Success += () =>
{

View File

@ -6,6 +6,8 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Game.Online.Rooms;
#nullable enable
namespace osu.Game.Screens.OnlinePlay
{
[Cached(typeof(IRoomManager))]
@ -32,15 +34,16 @@ namespace osu.Game.Screens.OnlinePlay
/// <param name="room">The <see cref="Room"/> to create.</param>
/// <param name="onSuccess">An action to be invoked if the creation succeeds.</param>
/// <param name="onError">An action to be invoked if an error occurred.</param>
void CreateRoom(Room room, Action<Room> onSuccess = null, Action<string> onError = null);
void CreateRoom(Room room, Action<Room>? onSuccess = null, Action<string>? onError = null);
/// <summary>
/// Joins a <see cref="Room"/>.
/// </summary>
/// <param name="room">The <see cref="Room"/> to join. <see cref="Room.RoomID"/> must be populated.</param>
/// <param name="password">An optional password to use for the join operation.</param>
/// <param name="onSuccess"></param>
/// <param name="onError"></param>
void JoinRoom(Room room, Action<Room> onSuccess = null, Action<string> onError = null);
void JoinRoom(Room room, string? password = null, Action<Room>? onSuccess = null, Action<string>? onError = null);
/// <summary>
/// Parts the currently-joined <see cref="Room"/>.

View File

@ -6,19 +6,25 @@ using System.Collections.Generic;
using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Input.Bindings;
using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay.Components;
using osuTK;
@ -26,7 +32,7 @@ using osuTK.Graphics;
namespace osu.Game.Screens.OnlinePlay.Lounge.Components
{
public class DrawableRoom : OsuClickableContainer, IStateful<SelectionState>, IFilterable, IHasContextMenu
public class DrawableRoom : OsuClickableContainer, IStateful<SelectionState>, IFilterable, IHasContextMenu, IHasPopover, IKeyBindingHandler<GlobalAction>
{
public const float SELECTION_BORDER_WIDTH = 4;
private const float corner_radius = 5;
@ -46,6 +52,12 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
[Resolved]
private BeatmapManager beatmaps { get; set; }
[Resolved(canBeNull: true)]
private Bindable<Room> selectedRoom { get; set; }
[Resolved(canBeNull: true)]
private LoungeSubScreen lounge { get; set; }
public readonly Room Room;
private SelectionState state;
@ -91,6 +103,10 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
public bool FilteringActive { get; set; }
private PasswordProtectedIcon passwordIcon;
private readonly Bindable<bool> hasPassword = new Bindable<bool>();
public DrawableRoom(Room room)
{
Room = room;
@ -200,6 +216,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
},
},
},
passwordIcon = new PasswordProtectedIcon { Alpha = 0 }
},
},
},
@ -222,10 +239,69 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
this.FadeInFromZero(transition_duration);
else
Alpha = 0;
hasPassword.BindTo(Room.HasPassword);
hasPassword.BindValueChanged(v => passwordIcon.Alpha = v.NewValue ? 1 : 0, true);
}
public Popover GetPopover() => new PasswordEntryPopover(Room) { JoinRequested = lounge.Join };
public MenuItem[] ContextMenuItems => new MenuItem[]
{
new OsuMenuItem("Create copy", MenuItemType.Standard, () =>
{
parentScreen?.OpenNewRoom(Room.DeepClone());
})
};
public bool OnPressed(GlobalAction action)
{
if (selectedRoom.Value != Room)
return false;
switch (action)
{
case GlobalAction.Select:
Click();
return true;
}
return false;
}
public void OnReleased(GlobalAction action)
{
}
protected override bool ShouldBeConsideredForInput(Drawable child) => state == SelectionState.Selected;
protected override bool OnMouseDown(MouseDownEvent e)
{
if (selectedRoom.Value != Room)
return true;
return base.OnMouseDown(e);
}
protected override bool OnClick(ClickEvent e)
{
if (Room != selectedRoom.Value)
{
selectedRoom.Value = Room;
return true;
}
if (Room.HasPassword.Value)
{
this.ShowPopover();
return true;
}
lounge?.Join(Room, null);
return base.OnClick(e);
}
private class RoomName : OsuSpriteText
{
[Resolved(typeof(Room), nameof(Online.Rooms.Room.Name))]
@ -238,12 +314,84 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
}
}
public MenuItem[] ContextMenuItems => new MenuItem[]
public class PasswordProtectedIcon : CompositeDrawable
{
new OsuMenuItem("Create copy", MenuItemType.Standard, () =>
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
parentScreen?.OpenNewRoom(Room.CreateCopy());
})
};
Anchor = Anchor.TopRight;
Origin = Anchor.TopRight;
Size = new Vector2(32);
InternalChildren = new Drawable[]
{
new Box
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopCentre,
Colour = colours.Gray5,
Rotation = 45,
RelativeSizeAxes = Axes.Both,
Width = 2,
},
new SpriteIcon
{
Icon = FontAwesome.Solid.Lock,
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
Margin = new MarginPadding(6),
Size = new Vector2(14),
}
};
}
}
public class PasswordEntryPopover : OsuPopover
{
private readonly Room room;
public Action<Room, string> JoinRequested;
public PasswordEntryPopover(Room room)
{
this.room = room;
}
private OsuPasswordTextBox passwordTextbox;
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
Child = new FillFlowContainer
{
Margin = new MarginPadding(10),
Spacing = new Vector2(5),
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Children = new Drawable[]
{
passwordTextbox = new OsuPasswordTextBox
{
Width = 200,
},
new TriangleButton
{
Width = 80,
Text = "Join Room",
Action = () => JoinRequested?.Invoke(room, passwordTextbox.Text)
}
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
Schedule(() => GetContainingInputManager().ChangeFocus(passwordTextbox));
passwordTextbox.OnCommit += (_, __) => JoinRequested?.Invoke(room, passwordTextbox.Text);
}
}
}
}

View File

@ -24,8 +24,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
{
public class RoomsContainer : CompositeDrawable, IKeyBindingHandler<GlobalAction>
{
public Action<Room> JoinRequested;
private readonly IBindableList<Room> rooms = new BindableList<Room>();
private readonly FillFlowContainer<DrawableRoom> roomFlow;
@ -121,19 +119,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
{
foreach (var room in rooms)
{
roomFlow.Add(new DrawableRoom(room)
{
Action = () =>
{
if (room == selectedRoom.Value)
{
joinSelected();
return;
}
selectRoom(room);
}
});
roomFlow.Add(new DrawableRoom(room));
}
Filter(filter?.Value);
@ -150,7 +136,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
roomFlow.Remove(toRemove);
selectRoom(null);
selectedRoom.Value = null;
}
}
@ -160,18 +146,9 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
roomFlow.SetLayoutPosition(room, room.Room.Position.Value);
}
private void selectRoom(Room room) => selectedRoom.Value = room;
private void joinSelected()
{
if (selectedRoom.Value == null) return;
JoinRequested?.Invoke(selectedRoom.Value);
}
protected override bool OnClick(ClickEvent e)
{
selectRoom(null);
selectedRoom.Value = null;
return base.OnClick(e);
}
@ -181,10 +158,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
{
switch (action)
{
case GlobalAction.Select:
joinSelected();
return true;
case GlobalAction.SelectNext:
beginRepeatSelection(() => selectNext(1), action);
return true;
@ -253,7 +226,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
// we already have a valid selection only change selection if we still have a room to switch to.
if (room != null)
selectRoom(room);
selectedRoom.Value = room;
}
#endregion

View File

@ -6,6 +6,7 @@ using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events;
@ -46,10 +47,11 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
[CanBeNull]
private IDisposable joiningRoomOperation { get; set; }
private RoomsContainer roomsContainer;
[BackgroundDependencyLoader]
private void load()
{
RoomsContainer roomsContainer;
OsuScrollContainer scrollContainer;
InternalChildren = new Drawable[]
@ -70,7 +72,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
RelativeSizeAxes = Axes.Both,
ScrollbarOverlapsContent = false,
Padding = new MarginPadding(10),
Child = roomsContainer = new RoomsContainer { JoinRequested = joinRequested }
Child = roomsContainer = new RoomsContainer()
},
loadingLayer = new LoadingLayer(true),
}
@ -150,31 +152,39 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
onReturning();
}
private void onReturning()
{
filter.HoldFocus = true;
}
public override bool OnExiting(IScreen next)
{
filter.HoldFocus = false;
onLeaving();
return base.OnExiting(next);
}
public override void OnSuspending(IScreen next)
{
onLeaving();
base.OnSuspending(next);
filter.HoldFocus = false;
}
private void joinRequested(Room room)
private void onReturning()
{
filter.HoldFocus = true;
}
private void onLeaving()
{
filter.HoldFocus = false;
// ensure any password prompt is dismissed.
this.HidePopover();
}
public void Join(Room room, string password)
{
if (joiningRoomOperation != null)
return;
joiningRoomOperation = ongoingOperationTracker?.BeginOperation();
RoomManager?.JoinRoom(room, r =>
RoomManager?.JoinRoom(room, password, r =>
{
Open(room);
joiningRoomOperation?.Dispose();

View File

@ -25,8 +25,12 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components
private void load()
{
Masking = true;
Add(Settings = CreateSettings());
}
protected abstract OnlinePlayComposite CreateSettings();
protected override void PopIn()
{
Settings.MoveToY(0, TRANSITION_DURATION, Easing.OutQuint);

View File

@ -27,16 +27,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
{
public class MultiplayerMatchSettingsOverlay : MatchSettingsOverlay
{
[BackgroundDependencyLoader]
private void load()
{
Child = Settings = new MatchSettings
protected override OnlinePlayComposite CreateSettings()
=> new MatchSettings
{
RelativeSizeAxes = Axes.Both,
RelativePositionAxes = Axes.Y,
SettingsApplied = Hide
};
}
protected class MatchSettings : OnlinePlayComposite
{
@ -47,6 +44,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
public OsuTextBox NameField, MaxParticipantsField;
public RoomAvailabilityPicker AvailabilityPicker;
public GameTypePicker TypePicker;
public OsuTextBox PasswordTextBox;
public TriangleButton ApplyButton;
public OsuSpriteText ErrorText;
@ -193,12 +191,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
},
new Section("Password (optional)")
{
Alpha = disabled_alpha,
Child = new SettingsPasswordTextBox
Child = PasswordTextBox = new SettingsPasswordTextBox
{
RelativeSizeAxes = Axes.X,
TabbableContentContainer = this,
ReadOnly = true,
},
},
}
@ -275,6 +271,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
Type.BindValueChanged(type => TypePicker.Current.Value = type.NewValue, true);
MaxParticipants.BindValueChanged(count => MaxParticipantsField.Text = count.NewValue?.ToString(), true);
RoomID.BindValueChanged(roomId => initialBeatmapControl.Alpha = roomId.NewValue == null ? 1 : 0, true);
Password.BindValueChanged(password => PasswordTextBox.Text = password.NewValue ?? string.Empty, true);
operationInProgress.BindTo(ongoingOperationTracker.InProgress);
operationInProgress.BindValueChanged(v =>
@ -307,7 +304,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
// Otherwise, update the room directly in preparation for it to be submitted to the API on match creation.
if (client.Room != null)
{
client.ChangeSettings(name: NameField.Text).ContinueWith(t => Schedule(() =>
client.ChangeSettings(name: NameField.Text, password: PasswordTextBox.Text).ContinueWith(t => Schedule(() =>
{
if (t.IsCompletedSuccessfully)
onSuccess(currentRoom.Value);
@ -320,6 +317,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
currentRoom.Value.Name.Value = NameField.Text;
currentRoom.Value.Availability.Value = AvailabilityPicker.Current.Value;
currentRoom.Value.Type.Value = TypePicker.Current.Value;
currentRoom.Value.Password.Value = PasswordTextBox.Current.Value;
if (int.TryParse(MaxParticipantsField.Text, out int max))
currentRoom.Value.MaxParticipants.Value = max;

View File

@ -38,9 +38,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
}
public override void CreateRoom(Room room, Action<Room> onSuccess = null, Action<string> onError = null)
=> base.CreateRoom(room, r => joinMultiplayerRoom(r, onSuccess, onError), onError);
=> base.CreateRoom(room, r => joinMultiplayerRoom(r, r.Password.Value, onSuccess, onError), onError);
public override void JoinRoom(Room room, Action<Room> onSuccess = null, Action<string> onError = null)
public override void JoinRoom(Room room, string password = null, Action<Room> onSuccess = null, Action<string> onError = null)
{
if (!multiplayerClient.IsConnected.Value)
{
@ -56,7 +56,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
return;
}
base.JoinRoom(room, r => joinMultiplayerRoom(r, onSuccess, onError), onError);
base.JoinRoom(room, password, r => joinMultiplayerRoom(r, password, onSuccess, onError), onError);
}
public override void PartRoom()
@ -79,11 +79,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
});
}
private void joinMultiplayerRoom(Room room, Action<Room> onSuccess = null, Action<string> onError = null)
private void joinMultiplayerRoom(Room room, string password, Action<Room> onSuccess = null, Action<string> onError = null)
{
Debug.Assert(room.RoomID.Value != null);
multiplayerClient.JoinRoom(room).ContinueWith(t =>
multiplayerClient.JoinRoom(room, password).ContinueWith(t =>
{
if (t.IsCompletedSuccessfully)
Schedule(() => onSuccess?.Invoke(room));

View File

@ -56,6 +56,9 @@ namespace osu.Game.Screens.OnlinePlay
[Resolved(typeof(Room))]
protected Bindable<RoomAvailability> Availability { get; private set; }
[Resolved(typeof(Room), nameof(Room.Password))]
public Bindable<string> Password { get; private set; }
[Resolved(typeof(Room))]
protected Bindable<TimeSpan?> Duration { get; private set; }

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Diagnostics;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
@ -30,17 +31,19 @@ namespace osu.Game.Screens.OnlinePlay
[Cached]
public abstract class OnlinePlayScreen : OsuScreen, IHasSubScreenStack
{
public override bool CursorVisible => (screenStack.CurrentScreen as IOnlinePlaySubScreen)?.CursorVisible ?? true;
public override bool CursorVisible => (screenStack?.CurrentScreen as IOnlinePlaySubScreen)?.CursorVisible ?? true;
// this is required due to PlayerLoader eventually being pushed to the main stack
// while leases may be taken out by a subscreen.
public override bool DisallowExternalBeatmapRulesetChanges => true;
private readonly MultiplayerWaveContainer waves;
private MultiplayerWaveContainer waves;
private readonly OsuButton createButton;
private readonly LoungeSubScreen loungeSubScreen;
private readonly ScreenStack screenStack;
private OsuButton createButton;
private ScreenStack screenStack;
private LoungeSubScreen loungeSubScreen;
private readonly IBindable<bool> isIdle = new BindableBool();
@ -54,7 +57,7 @@ namespace osu.Game.Screens.OnlinePlay
private readonly Bindable<FilterCriteria> currentFilter = new Bindable<FilterCriteria>(new FilterCriteria());
[Cached]
private OngoingOperationTracker ongoingOperationTracker { get; set; }
private readonly OngoingOperationTracker ongoingOperationTracker = new OngoingOperationTracker();
[Resolved(CanBeNull = true)]
private MusicController music { get; set; }
@ -65,11 +68,14 @@ namespace osu.Game.Screens.OnlinePlay
[Resolved]
protected IAPIProvider API { get; private set; }
[Resolved(CanBeNull = true)]
private IdleTracker idleTracker { get; set; }
[Resolved(CanBeNull = true)]
private OsuLogo logo { get; set; }
private readonly Drawable header;
private readonly Drawable headerBackground;
private Drawable header;
private Drawable headerBackground;
protected OnlinePlayScreen()
{
@ -78,6 +84,14 @@ namespace osu.Game.Screens.OnlinePlay
RelativeSizeAxes = Axes.Both;
Padding = new MarginPadding { Horizontal = -HORIZONTAL_OVERFLOW_PADDING };
RoomManager = CreateRoomManager();
}
private readonly IBindable<APIState> apiState = new Bindable<APIState>();
[BackgroundDependencyLoader]
private void load()
{
var backgroundColour = Color4Extensions.FromHex(@"3e3a44");
InternalChild = waves = new MultiplayerWaveContainer
@ -144,27 +158,14 @@ namespace osu.Game.Screens.OnlinePlay
};
button.Action = () => OpenNewRoom();
}),
RoomManager = CreateRoomManager(),
ongoingOperationTracker = new OngoingOperationTracker()
RoomManager,
ongoingOperationTracker,
}
};
screenStack.ScreenPushed += screenPushed;
screenStack.ScreenExited += screenExited;
screenStack.Push(loungeSubScreen = CreateLounge());
}
private readonly IBindable<APIState> apiState = new Bindable<APIState>();
[BackgroundDependencyLoader(true)]
private void load(IdleTracker idleTracker)
{
apiState.BindTo(API.State);
apiState.BindValueChanged(onlineStateChanged, true);
if (idleTracker != null)
isIdle.BindTo(idleTracker.IsIdle);
// a lot of the functionality in this class depends on loungeSubScreen being in a ready to go state.
// as such, we intentionally load this inline so it is ready alongside this screen.
LoadComponent(loungeSubScreen = CreateLounge());
}
private void onlineStateChanged(ValueChangedEvent<APIState> state) => Schedule(() =>
@ -179,7 +180,20 @@ namespace osu.Game.Screens.OnlinePlay
protected override void LoadComplete()
{
base.LoadComplete();
isIdle.BindValueChanged(idle => UpdatePollingRate(idle.NewValue), true);
screenStack.ScreenPushed += screenPushed;
screenStack.ScreenExited += screenExited;
screenStack.Push(loungeSubScreen);
apiState.BindTo(API.State);
apiState.BindValueChanged(onlineStateChanged, true);
if (idleTracker != null)
{
isIdle.BindTo(idleTracker.IsIdle);
isIdle.BindValueChanged(idle => UpdatePollingRate(idle.NewValue), true);
}
}
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
@ -222,7 +236,9 @@ namespace osu.Game.Screens.OnlinePlay
this.FadeIn(250);
this.ScaleTo(1, 250, Easing.OutSine);
screenStack.CurrentScreen?.OnResuming(last);
Debug.Assert(screenStack.CurrentScreen != null);
screenStack.CurrentScreen.OnResuming(last);
base.OnResuming(last);
UpdatePollingRate(isIdle.Value);
@ -233,14 +249,16 @@ namespace osu.Game.Screens.OnlinePlay
this.ScaleTo(1.1f, 250, Easing.InSine);
this.FadeOut(250);
screenStack.CurrentScreen?.OnSuspending(next);
Debug.Assert(screenStack.CurrentScreen != null);
screenStack.CurrentScreen.OnSuspending(next);
UpdatePollingRate(isIdle.Value);
}
public override bool OnExiting(IScreen next)
{
if (screenStack.CurrentScreen?.OnExiting(next) == true)
var subScreen = screenStack.CurrentScreen as Drawable;
if (subScreen?.IsLoaded == true && screenStack.CurrentScreen.OnExiting(next))
return true;
RoomManager.PartRoom();

View File

@ -72,8 +72,8 @@ namespace osu.Game.Screens.OnlinePlay
// At this point, Mods contains both the required and allowed mods. For selection purposes, it should only contain the required mods.
// Similarly, freeMods is currently empty but should only contain the allowed mods.
Mods.Value = selectedItem?.Value?.RequiredMods.Select(m => m.CreateCopy()).ToArray() ?? Array.Empty<Mod>();
FreeMods.Value = selectedItem?.Value?.AllowedMods.Select(m => m.CreateCopy()).ToArray() ?? Array.Empty<Mod>();
Mods.Value = selectedItem?.Value?.RequiredMods.Select(m => m.DeepClone()).ToArray() ?? Array.Empty<Mod>();
FreeMods.Value = selectedItem?.Value?.AllowedMods.Select(m => m.DeepClone()).ToArray() ?? Array.Empty<Mod>();
Mods.BindValueChanged(onModsChanged);
Ruleset.BindValueChanged(onRulesetChanged);
@ -108,8 +108,8 @@ namespace osu.Game.Screens.OnlinePlay
}
};
item.RequiredMods.AddRange(Mods.Value.Select(m => m.CreateCopy()));
item.AllowedMods.AddRange(FreeMods.Value.Select(m => m.CreateCopy()));
item.RequiredMods.AddRange(Mods.Value.Select(m => m.DeepClone()));
item.AllowedMods.AddRange(FreeMods.Value.Select(m => m.DeepClone()));
SelectItem(item);
return true;

View File

@ -26,16 +26,13 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
{
public Action EditPlaylist;
[BackgroundDependencyLoader]
private void load()
{
Child = Settings = new MatchSettings
protected override OnlinePlayComposite CreateSettings()
=> new MatchSettings
{
RelativeSizeAxes = Axes.Both,
RelativePositionAxes = Axes.Y,
EditPlaylist = () => EditPlaylist?.Invoke()
};
}
protected class MatchSettings : OnlinePlayComposite
{

View File

@ -55,10 +55,10 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
item.Ruleset.Value = Ruleset.Value;
item.RequiredMods.Clear();
item.RequiredMods.AddRange(Mods.Value.Select(m => m.CreateCopy()));
item.RequiredMods.AddRange(Mods.Value.Select(m => m.DeepClone()));
item.AllowedMods.Clear();
item.AllowedMods.AddRange(FreeMods.Value.Select(m => m.CreateCopy()));
item.AllowedMods.AddRange(FreeMods.Value.Select(m => m.DeepClone()));
}
}
}

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