mirror of
https://github.com/ppy/osu.git
synced 2025-03-05 13:13:22 +08:00
Merge branch 'ppy:master' into timeline-object-interactions
This commit is contained in:
commit
8b09ddbcd6
@ -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.
|
||||
|
@ -6,7 +6,7 @@
|
||||
<Description>A free-to-win rhythm game. Rhythm is just a *click* away!</Description>
|
||||
<AssemblyName>osu!</AssemblyName>
|
||||
<Title>osu!</Title>
|
||||
<Product>osu!</Product>
|
||||
<Product>osu!(lazer)</Product>
|
||||
<ApplicationIcon>lazer.ico</ApplicationIcon>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<Version>0.0.0</Version>
|
||||
|
288
osu.Game.Rulesets.Catch.Tests/JuiceStreamPathTest.cs
Normal file
288
osu.Game.Rulesets.Catch.Tests/JuiceStreamPathTest.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -31,22 +31,9 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
[Resolved]
|
||||
private OsuConfigManager config { get; set; }
|
||||
|
||||
[Cached]
|
||||
private readonly DroppedObjectContainer droppedObjectContainer;
|
||||
|
||||
private readonly Container trailContainer;
|
||||
|
||||
private TestCatcher catcher;
|
||||
|
||||
public TestSceneCatcher()
|
||||
{
|
||||
Add(trailContainer = new Container
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Depth = -1
|
||||
});
|
||||
Add(droppedObjectContainer = new DroppedObjectContainer());
|
||||
}
|
||||
private DroppedObjectContainer droppedObjectContainer;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp() => Schedule(() =>
|
||||
@ -56,13 +43,25 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
CircleSize = 0,
|
||||
};
|
||||
|
||||
if (catcher != null)
|
||||
Remove(catcher);
|
||||
|
||||
Add(catcher = new TestCatcher(trailContainer, difficulty)
|
||||
var trailContainer = new Container
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
};
|
||||
droppedObjectContainer = new DroppedObjectContainer();
|
||||
Child = new DependencyProvidingContainer
|
||||
{
|
||||
CachedDependencies = new (Type, object)[]
|
||||
{
|
||||
(typeof(DroppedObjectContainer), droppedObjectContainer),
|
||||
},
|
||||
Children = new Drawable[]
|
||||
{
|
||||
droppedObjectContainer,
|
||||
catcher = new TestCatcher(trailContainer, difficulty),
|
||||
trailContainer
|
||||
},
|
||||
Anchor = Anchor.Centre
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
[Test]
|
||||
|
@ -23,7 +23,8 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
|
||||
|
||||
protected override IEnumerable<CatchHitObject> ConvertHitObject(HitObject obj, IBeatmap beatmap, CancellationToken cancellationToken)
|
||||
{
|
||||
var positionData = obj as IHasXPosition;
|
||||
var xPositionData = obj as IHasXPosition;
|
||||
var yPositionData = obj as IHasYPosition;
|
||||
var comboData = obj as IHasCombo;
|
||||
|
||||
switch (obj)
|
||||
@ -36,10 +37,11 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
|
||||
Path = curveData.Path,
|
||||
NodeSamples = curveData.NodeSamples,
|
||||
RepeatCount = curveData.RepeatCount,
|
||||
X = positionData?.X ?? 0,
|
||||
X = xPositionData?.X ?? 0,
|
||||
NewCombo = comboData?.NewCombo ?? false,
|
||||
ComboOffset = comboData?.ComboOffset ?? 0,
|
||||
LegacyLastTickOffset = (obj as IHasLegacyLastTickOffset)?.LegacyLastTickOffset ?? 0
|
||||
LegacyLastTickOffset = (obj as IHasLegacyLastTickOffset)?.LegacyLastTickOffset ?? 0,
|
||||
LegacyConvertedY = yPositionData?.Y ?? CatchHitObject.DEFAULT_LEGACY_CONVERT_Y
|
||||
}.Yield();
|
||||
|
||||
case IHasDuration endTime:
|
||||
@ -59,7 +61,8 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
|
||||
Samples = obj.Samples,
|
||||
NewCombo = comboData?.NewCombo ?? false,
|
||||
ComboOffset = comboData?.ComboOffset ?? 0,
|
||||
X = positionData?.X ?? 0
|
||||
X = xPositionData?.X ?? 0,
|
||||
LegacyConvertedY = yPositionData?.Y ?? CatchHitObject.DEFAULT_LEGACY_CONVERT_Y
|
||||
}.Yield();
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Game.Rulesets.Difficulty;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
@ -33,11 +32,11 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
{
|
||||
mods = Score.Mods;
|
||||
|
||||
fruitsHit = Score.Statistics.GetOrDefault(HitResult.Great);
|
||||
ticksHit = Score.Statistics.GetOrDefault(HitResult.LargeTickHit);
|
||||
tinyTicksHit = Score.Statistics.GetOrDefault(HitResult.SmallTickHit);
|
||||
tinyTicksMissed = Score.Statistics.GetOrDefault(HitResult.SmallTickMiss);
|
||||
misses = Score.Statistics.GetOrDefault(HitResult.Miss);
|
||||
fruitsHit = Score.Statistics.GetValueOrDefault(HitResult.Great);
|
||||
ticksHit = Score.Statistics.GetValueOrDefault(HitResult.LargeTickHit);
|
||||
tinyTicksHit = Score.Statistics.GetValueOrDefault(HitResult.SmallTickHit);
|
||||
tinyTicksMissed = Score.Statistics.GetValueOrDefault(HitResult.SmallTickMiss);
|
||||
misses = Score.Statistics.GetValueOrDefault(HitResult.Miss);
|
||||
|
||||
// We are heavily relying on aim in catch the beat
|
||||
double value = Math.Pow(5.0 * Math.Max(1.0, Attributes.StarRating / 0.0049) - 4.0, 2.0) / 100000.0;
|
||||
|
@ -19,9 +19,8 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
{
|
||||
get
|
||||
{
|
||||
float x = HitObject.OriginalX;
|
||||
float y = HitObjectContainer.PositionAtTime(HitObject.StartTime);
|
||||
return HitObjectContainer.ToScreenSpace(new Vector2(x, y + HitObjectContainer.DrawHeight));
|
||||
Vector2 position = CatchHitObjectUtils.GetStartPosition(HitObjectContainer, HitObject);
|
||||
return HitObjectContainer.ToScreenSpace(position + new Vector2(0, HitObjectContainer.DrawHeight));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,14 +1,12 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Catch.Skinning.Default;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||
@ -28,10 +26,8 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||
Colour = osuColour.Yellow;
|
||||
}
|
||||
|
||||
public void UpdateFrom(ScrollingHitObjectContainer hitObjectContainer, CatchHitObject hitObject, [CanBeNull] CatchHitObject parent = null)
|
||||
public void UpdateFrom(CatchHitObject hitObject)
|
||||
{
|
||||
X = hitObject.EffectiveX - (parent?.OriginalX ?? 0);
|
||||
Y = hitObjectContainer.PositionAtTime(hitObject.StartTime, parent?.StartTime ?? hitObjectContainer.Time.Current);
|
||||
Scale = new Vector2(hitObject.Scale);
|
||||
}
|
||||
}
|
||||
|
@ -20,12 +20,6 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||
Anchor = Anchor.BottomLeft;
|
||||
}
|
||||
|
||||
public void UpdatePositionFrom(ScrollingHitObjectContainer hitObjectContainer, CatchHitObject parentHitObject)
|
||||
{
|
||||
X = parentHitObject.OriginalX;
|
||||
Y = hitObjectContainer.PositionAtTime(parentHitObject.StartTime);
|
||||
}
|
||||
|
||||
public void UpdateNestedObjectsFrom(ScrollingHitObjectContainer hitObjectContainer, CatchHitObject parentHitObject)
|
||||
{
|
||||
nestedHitObjects.Clear();
|
||||
@ -43,7 +37,8 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||
{
|
||||
var hitObject = nestedHitObjects[i];
|
||||
var outline = (FruitOutline)InternalChildren[i];
|
||||
outline.UpdateFrom(hitObjectContainer, hitObject, parentHitObject);
|
||||
outline.Position = CatchHitObjectUtils.GetStartPosition(hitObjectContainer, hitObject) - Position;
|
||||
outline.UpdateFrom(hitObject);
|
||||
outline.Scale *= hitObject is Droplet ? 0.5f : 1;
|
||||
}
|
||||
}
|
||||
|
@ -33,12 +33,6 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||
};
|
||||
}
|
||||
|
||||
public void UpdatePositionFrom(ScrollingHitObjectContainer hitObjectContainer, CatchHitObject hitObject)
|
||||
{
|
||||
X = hitObject.OriginalX;
|
||||
Y = hitObjectContainer.PositionAtTime(hitObject.StartTime);
|
||||
}
|
||||
|
||||
public void UpdatePathFrom(ScrollingHitObjectContainer hitObjectContainer, JuiceStream hitObject)
|
||||
{
|
||||
double distanceToYFactor = -hitObjectContainer.LengthAtTime(hitObject.StartTime, hitObject.StartTime + 1 / hitObject.Velocity);
|
||||
|
@ -29,7 +29,8 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
{
|
||||
base.Update();
|
||||
|
||||
outline.UpdateFrom(HitObjectContainer, HitObject);
|
||||
outline.Position = CatchHitObjectUtils.GetStartPosition(HitObjectContainer, HitObject);
|
||||
outline.UpdateFrom(HitObject);
|
||||
}
|
||||
|
||||
protected override bool OnMouseDown(MouseDownEvent e)
|
||||
|
@ -20,8 +20,10 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
{
|
||||
base.Update();
|
||||
|
||||
if (IsSelected)
|
||||
outline.UpdateFrom(HitObjectContainer, HitObject);
|
||||
if (!IsSelected) return;
|
||||
|
||||
outline.Position = CatchHitObjectUtils.GetStartPosition(HitObjectContainer, HitObject);
|
||||
outline.UpdateFrom(HitObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -49,8 +49,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
|
||||
if (!IsSelected) return;
|
||||
|
||||
scrollingPath.UpdatePositionFrom(HitObjectContainer, HitObject);
|
||||
nestedOutlineContainer.UpdatePositionFrom(HitObjectContainer, HitObject);
|
||||
nestedOutlineContainer.Position = scrollingPath.Position = CatchHitObjectUtils.GetStartPosition(HitObjectContainer, HitObject);
|
||||
|
||||
if (pathCache.IsValid) return;
|
||||
|
||||
|
24
osu.Game.Rulesets.Catch/Edit/CatchHitObjectUtils.cs
Normal file
24
osu.Game.Rulesets.Catch/Edit/CatchHitObjectUtils.cs
Normal file
@ -0,0 +1,24 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Edit
|
||||
{
|
||||
/// <summary>
|
||||
/// Utility functions used by the editor.
|
||||
/// </summary>
|
||||
public static class CatchHitObjectUtils
|
||||
{
|
||||
/// <summary>
|
||||
/// Get the position of the hit object in the playfield based on <see cref="CatchHitObject.OriginalX"/> and <see cref="HitObject.StartTime"/>.
|
||||
/// </summary>
|
||||
public static Vector2 GetStartPosition(ScrollingHitObjectContainer hitObjectContainer, CatchHitObject hitObject)
|
||||
{
|
||||
return new Vector2(hitObject.OriginalX, hitObjectContainer.PositionAtTime(hitObject.StartTime));
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
{
|
||||
|
@ -9,10 +9,11 @@ using osu.Game.Rulesets.Catch.UI;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Objects
|
||||
{
|
||||
public abstract class CatchHitObject : HitObject, IHasXPosition, IHasComboInformation
|
||||
public abstract class CatchHitObject : HitObject, IHasPosition, IHasComboInformation
|
||||
{
|
||||
public const float OBJECT_RADIUS = 64;
|
||||
|
||||
@ -31,8 +32,6 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
set => OriginalXBindable.Value = value;
|
||||
}
|
||||
|
||||
float IHasXPosition.X => OriginalXBindable.Value;
|
||||
|
||||
public readonly Bindable<float> XOffsetBindable = new Bindable<float>();
|
||||
|
||||
/// <summary>
|
||||
@ -131,5 +130,24 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
}
|
||||
|
||||
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
|
||||
|
||||
#region Hit object conversion
|
||||
|
||||
// The half of the height of the osu! playfield.
|
||||
public const float DEFAULT_LEGACY_CONVERT_Y = 192;
|
||||
|
||||
/// <summary>
|
||||
/// The Y position of the hit object is not used in the normal osu!catch gameplay.
|
||||
/// It is preserved to maximize the backward compatibility with the legacy editor, in which the mappers use the Y position to organize the patterns.
|
||||
/// </summary>
|
||||
public float LegacyConvertedY { get; set; } = DEFAULT_LEGACY_CONVERT_Y;
|
||||
|
||||
float IHasXPosition.X => OriginalX;
|
||||
|
||||
float IHasYPosition.Y => LegacyConvertedY;
|
||||
|
||||
Vector2 IHasPosition.Position => new Vector2(OriginalX, LegacyConvertedY);
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
340
osu.Game.Rulesets.Catch/Objects/JuiceStreamPath.cs
Normal file
340
osu.Game.Rulesets.Catch/Objects/JuiceStreamPath.cs
Normal 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++;
|
||||
}
|
||||
}
|
33
osu.Game.Rulesets.Catch/Objects/JuiceStreamPathVertex.cs
Normal file
33
osu.Game.Rulesets.Catch/Objects/JuiceStreamPathVertex.cs
Normal 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})";
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Game.Rulesets.Difficulty;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
@ -37,12 +36,12 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
{
|
||||
mods = Score.Mods;
|
||||
scaledScore = Score.TotalScore;
|
||||
countPerfect = Score.Statistics.GetOrDefault(HitResult.Perfect);
|
||||
countGreat = Score.Statistics.GetOrDefault(HitResult.Great);
|
||||
countGood = Score.Statistics.GetOrDefault(HitResult.Good);
|
||||
countOk = Score.Statistics.GetOrDefault(HitResult.Ok);
|
||||
countMeh = Score.Statistics.GetOrDefault(HitResult.Meh);
|
||||
countMiss = Score.Statistics.GetOrDefault(HitResult.Miss);
|
||||
countPerfect = Score.Statistics.GetValueOrDefault(HitResult.Perfect);
|
||||
countGreat = Score.Statistics.GetValueOrDefault(HitResult.Great);
|
||||
countGood = Score.Statistics.GetValueOrDefault(HitResult.Good);
|
||||
countOk = Score.Statistics.GetValueOrDefault(HitResult.Ok);
|
||||
countMeh = Score.Statistics.GetValueOrDefault(HitResult.Meh);
|
||||
countMiss = Score.Statistics.GetValueOrDefault(HitResult.Miss);
|
||||
|
||||
IEnumerable<Mod> scoreIncreaseMods = Ruleset.GetModsFor(ModType.DifficultyIncrease);
|
||||
|
||||
|
@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Mania
|
||||
{
|
||||
public class ManiaSettingsSubsection : RulesetSettingsSubsection
|
||||
{
|
||||
protected override string Header => "osu!mania";
|
||||
protected override LocalisableString Header => "osu!mania";
|
||||
|
||||
public ManiaSettingsSubsection(ManiaRuleset ruleset)
|
||||
: base(ruleset)
|
||||
|
@ -22,6 +22,10 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
|
||||
protected readonly IBindable<ScrollingDirection> Direction = new Bindable<ScrollingDirection>();
|
||||
|
||||
// Leaving the default (10s) makes hitobjects not appear, as this offset is used for the initial state transforms.
|
||||
// Calculated as DrawableManiaRuleset.MAX_TIME_RANGE + some additional allowance for velocity < 1.
|
||||
protected override double InitialLifetimeOffset => 30000;
|
||||
|
||||
[Resolved(canBeNull: true)]
|
||||
private ManiaPlayfield playfield { get; set; }
|
||||
|
||||
|
@ -33,12 +33,12 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
/// <summary>
|
||||
/// The minimum time range. This occurs at a <see cref="relativeTimeRange"/> of 40.
|
||||
/// </summary>
|
||||
public const double MIN_TIME_RANGE = 340;
|
||||
public const double MIN_TIME_RANGE = 290;
|
||||
|
||||
/// <summary>
|
||||
/// The maximum time range. This occurs at a <see cref="relativeTimeRange"/> of 1.
|
||||
/// </summary>
|
||||
public const double MAX_TIME_RANGE = 13720;
|
||||
public const double MAX_TIME_RANGE = 11485;
|
||||
|
||||
protected new ManiaPlayfield Playfield => (ManiaPlayfield)base.Playfield;
|
||||
|
||||
@ -67,7 +67,7 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
protected override ScrollVisualisationMethod VisualisationMethod => scrollMethod;
|
||||
|
||||
private readonly Bindable<ManiaScrollingDirection> configDirection = new Bindable<ManiaScrollingDirection>();
|
||||
private readonly Bindable<double> configTimeRange = new BindableDouble();
|
||||
private readonly BindableDouble configTimeRange = new BindableDouble();
|
||||
|
||||
// Stores the current speed adjustment active in gameplay.
|
||||
private readonly Track speedAdjustmentTrack = new TrackVirtual(0);
|
||||
@ -103,6 +103,8 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
configDirection.BindValueChanged(direction => Direction.Value = (ScrollingDirection)direction.NewValue, true);
|
||||
|
||||
Config.BindWith(ManiaRulesetSetting.ScrollTime, configTimeRange);
|
||||
TimeRange.MinValue = configTimeRange.MinValue;
|
||||
TimeRange.MaxValue = configTimeRange.MaxValue;
|
||||
}
|
||||
|
||||
protected override void AdjustScrollSpeed(int amount)
|
||||
|
@ -4,7 +4,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Game.Rulesets.Difficulty;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
@ -36,10 +35,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
mods = Score.Mods;
|
||||
accuracy = Score.Accuracy;
|
||||
scoreMaxCombo = Score.MaxCombo;
|
||||
countGreat = Score.Statistics.GetOrDefault(HitResult.Great);
|
||||
countOk = Score.Statistics.GetOrDefault(HitResult.Ok);
|
||||
countMeh = Score.Statistics.GetOrDefault(HitResult.Meh);
|
||||
countMiss = Score.Statistics.GetOrDefault(HitResult.Miss);
|
||||
countGreat = Score.Statistics.GetValueOrDefault(HitResult.Great);
|
||||
countOk = Score.Statistics.GetValueOrDefault(HitResult.Ok);
|
||||
countMeh = Score.Statistics.GetValueOrDefault(HitResult.Meh);
|
||||
countMiss = Score.Statistics.GetValueOrDefault(HitResult.Miss);
|
||||
|
||||
// Custom multipliers for NoFail and SpunOut.
|
||||
double multiplier = 1.12; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things
|
||||
@ -98,11 +97,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
|
||||
double approachRateFactor = 0.0;
|
||||
if (Attributes.ApproachRate > 10.33)
|
||||
approachRateFactor += 0.4 * (Attributes.ApproachRate - 10.33);
|
||||
approachRateFactor = Attributes.ApproachRate - 10.33;
|
||||
else if (Attributes.ApproachRate < 8.0)
|
||||
approachRateFactor += 0.01 * (8.0 - Attributes.ApproachRate);
|
||||
approachRateFactor = 0.025 * (8.0 - Attributes.ApproachRate);
|
||||
|
||||
aimValue *= 1.0 + Math.Min(approachRateFactor, approachRateFactor * (totalHits / 1000.0));
|
||||
double approachRateTotalHitsFactor = 1.0 / (1.0 + Math.Exp(-(0.007 * (totalHits - 400))));
|
||||
|
||||
aimValue *= 1.0 + (0.03 + 0.37 * approachRateTotalHitsFactor) * approachRateFactor;
|
||||
|
||||
// We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR.
|
||||
if (mods.Any(h => h is OsuModHidden))
|
||||
@ -145,9 +146,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
|
||||
double approachRateFactor = 0.0;
|
||||
if (Attributes.ApproachRate > 10.33)
|
||||
approachRateFactor += 0.4 * (Attributes.ApproachRate - 10.33);
|
||||
approachRateFactor = Attributes.ApproachRate - 10.33;
|
||||
|
||||
speedValue *= 1.0 + Math.Min(approachRateFactor, approachRateFactor * (totalHits / 1000.0));
|
||||
double approachRateTotalHitsFactor = 1.0 / (1.0 + Math.Exp(-(0.007 * (totalHits - 400))));
|
||||
|
||||
speedValue *= 1.0 + (0.03 + 0.37 * approachRateTotalHitsFactor) * approachRateFactor;
|
||||
|
||||
if (mods.Any(m => m is OsuModHidden))
|
||||
speedValue *= 1.0 + 0.04 * (12.0 - Attributes.ApproachRate);
|
||||
|
@ -1,13 +1,43 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio.Track;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Beatmaps.Timing;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Overlays.Settings;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Beatmaps;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Rulesets.Osu.Utils;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
public class OsuModTarget : Mod
|
||||
public class OsuModTarget : ModWithVisibilityAdjustment, IApplicableToDrawableRuleset<OsuHitObject>,
|
||||
IApplicableToHealthProcessor, IApplicableToDifficulty, IApplicableFailOverride,
|
||||
IHasSeed, IHidesApproachCircles
|
||||
{
|
||||
public override string Name => "Target";
|
||||
public override string Acronym => "TP";
|
||||
@ -15,5 +45,510 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
public override IconUsage? Icon => OsuIcon.ModTarget;
|
||||
public override string Description => @"Practice keeping up with the beat of the song.";
|
||||
public override double ScoreMultiplier => 1;
|
||||
|
||||
public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles) };
|
||||
|
||||
[SettingSource("Seed", "Use a custom seed instead of a random one", SettingControlType = typeof(SettingsNumberBox))]
|
||||
public Bindable<int?> Seed { get; } = new Bindable<int?>
|
||||
{
|
||||
Default = null,
|
||||
Value = null
|
||||
};
|
||||
|
||||
#region Constants
|
||||
|
||||
/// <summary>
|
||||
/// Jump distance for circles in the last combo
|
||||
/// </summary>
|
||||
private const float max_base_distance = 333f;
|
||||
|
||||
/// <summary>
|
||||
/// The maximum allowed jump distance after multipliers are applied
|
||||
/// </summary>
|
||||
private const float distance_cap = 380f;
|
||||
|
||||
// The distances from the hit objects to the borders of the playfield they start to "turn around" and curve towards the middle.
|
||||
// The closer the hit objects draw to the border, the sharper the turn
|
||||
private const byte border_distance_x = 192;
|
||||
private const byte border_distance_y = 144;
|
||||
|
||||
/// <summary>
|
||||
/// The extent of rotation towards playfield centre when a circle is near the edge
|
||||
/// </summary>
|
||||
private const float edge_rotation_multiplier = 0.75f;
|
||||
|
||||
/// <summary>
|
||||
/// Number of recent circles to check for overlap
|
||||
/// </summary>
|
||||
private const int overlap_check_count = 5;
|
||||
|
||||
/// <summary>
|
||||
/// Duration of the undimming animation
|
||||
/// </summary>
|
||||
private const double undim_duration = 96;
|
||||
|
||||
/// <summary>
|
||||
/// Acceptable difference for timing comparisons
|
||||
/// </summary>
|
||||
private const double timing_precision = 1;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private Fields
|
||||
|
||||
private ControlPointInfo controlPointInfo;
|
||||
|
||||
private List<OsuHitObject> originalHitObjects;
|
||||
|
||||
private Random rng;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Sudden Death (IApplicableFailOverride)
|
||||
|
||||
public bool PerformFail() => true;
|
||||
|
||||
public bool RestartOnFail => false;
|
||||
|
||||
public void ApplyToHealthProcessor(HealthProcessor healthProcessor)
|
||||
{
|
||||
// Sudden death
|
||||
healthProcessor.FailConditions += (_, result)
|
||||
=> result.Type.AffectsCombo()
|
||||
&& !result.IsHit;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Reduce AR (IApplicableToDifficulty)
|
||||
|
||||
public void ReadFromDifficulty(BeatmapDifficulty difficulty)
|
||||
{
|
||||
}
|
||||
|
||||
public void ApplyToDifficulty(BeatmapDifficulty difficulty)
|
||||
{
|
||||
// Decrease AR to increase preempt time
|
||||
difficulty.ApproachRate *= 0.5f;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Circle Transforms (ModWithVisibilityAdjustment)
|
||||
|
||||
protected override void ApplyIncreasedVisibilityState(DrawableHitObject drawable, ArmedState state)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void ApplyNormalVisibilityState(DrawableHitObject drawable, ArmedState state)
|
||||
{
|
||||
if (!(drawable is DrawableHitCircle circle)) return;
|
||||
|
||||
double startTime = circle.HitObject.StartTime;
|
||||
double preempt = circle.HitObject.TimePreempt;
|
||||
|
||||
using (circle.BeginAbsoluteSequence(startTime - preempt))
|
||||
{
|
||||
// initial state
|
||||
circle.ScaleTo(0.5f)
|
||||
.FadeColour(OsuColour.Gray(0.5f));
|
||||
|
||||
// scale to final size
|
||||
circle.ScaleTo(1f, preempt);
|
||||
|
||||
// Remove approach circles
|
||||
circle.ApproachCircle.Hide();
|
||||
}
|
||||
|
||||
using (circle.BeginAbsoluteSequence(startTime - controlPointInfo.TimingPointAt(startTime).BeatLength - undim_duration))
|
||||
circle.FadeColour(Color4.White, undim_duration);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Beatmap Generation (IApplicableToBeatmap)
|
||||
|
||||
public override void ApplyToBeatmap(IBeatmap beatmap)
|
||||
{
|
||||
Seed.Value ??= RNG.Next();
|
||||
rng = new Random(Seed.Value.Value);
|
||||
|
||||
var osuBeatmap = (OsuBeatmap)beatmap;
|
||||
|
||||
if (osuBeatmap.HitObjects.Count == 0) return;
|
||||
|
||||
controlPointInfo = osuBeatmap.ControlPointInfo;
|
||||
originalHitObjects = osuBeatmap.HitObjects.OrderBy(x => x.StartTime).ToList();
|
||||
|
||||
var hitObjects = generateBeats(osuBeatmap)
|
||||
.Select(beat =>
|
||||
{
|
||||
var newCircle = new HitCircle();
|
||||
newCircle.ApplyDefaults(controlPointInfo, osuBeatmap.BeatmapInfo.BaseDifficulty);
|
||||
newCircle.StartTime = beat;
|
||||
return (OsuHitObject)newCircle;
|
||||
}).ToList();
|
||||
|
||||
addHitSamples(hitObjects);
|
||||
|
||||
fixComboInfo(hitObjects);
|
||||
|
||||
randomizeCirclePos(hitObjects);
|
||||
|
||||
osuBeatmap.HitObjects = hitObjects;
|
||||
|
||||
base.ApplyToBeatmap(beatmap);
|
||||
}
|
||||
|
||||
private IEnumerable<double> generateBeats(IBeatmap beatmap)
|
||||
{
|
||||
var startTime = originalHitObjects.First().StartTime;
|
||||
var endTime = originalHitObjects.Last().GetEndTime();
|
||||
|
||||
var beats = beatmap.ControlPointInfo.TimingPoints
|
||||
// Ignore timing points after endTime
|
||||
.Where(timingPoint => !definitelyBigger(timingPoint.Time, endTime))
|
||||
// Generate the beats
|
||||
.SelectMany(timingPoint => getBeatsForTimingPoint(timingPoint, endTime))
|
||||
// Remove beats before startTime
|
||||
.Where(beat => almostBigger(beat, startTime))
|
||||
// Remove beats during breaks
|
||||
.Where(beat => !isInsideBreakPeriod(beatmap.Breaks, beat))
|
||||
.ToList();
|
||||
|
||||
// Remove beats that are too close to the next one (e.g. due to timing point changes)
|
||||
for (var i = beats.Count - 2; i >= 0; i--)
|
||||
{
|
||||
var beat = beats[i];
|
||||
|
||||
if (!definitelyBigger(beats[i + 1] - beat, beatmap.ControlPointInfo.TimingPointAt(beat).BeatLength / 2))
|
||||
beats.RemoveAt(i);
|
||||
}
|
||||
|
||||
return beats;
|
||||
}
|
||||
|
||||
private void addHitSamples(IEnumerable<OsuHitObject> hitObjects)
|
||||
{
|
||||
foreach (var obj in hitObjects)
|
||||
{
|
||||
var samples = getSamplesAtTime(originalHitObjects, obj.StartTime);
|
||||
|
||||
// If samples aren't available at the exact start time of the object,
|
||||
// use samples (without additions) in the closest original hit object instead
|
||||
obj.Samples = samples ?? getClosestHitObject(originalHitObjects, obj.StartTime).Samples.Where(s => !HitSampleInfo.AllAdditions.Contains(s.Name)).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
private void fixComboInfo(List<OsuHitObject> hitObjects)
|
||||
{
|
||||
// Copy combo indices from an original object at the same time or from the closest preceding object
|
||||
// (Objects lying between two combos are assumed to belong to the preceding combo)
|
||||
hitObjects.ForEach(newObj =>
|
||||
{
|
||||
var closestOrigObj = originalHitObjects.FindLast(y => almostBigger(newObj.StartTime, y.StartTime));
|
||||
|
||||
// It shouldn't be possible for closestOrigObj to be null
|
||||
// But if it is, obj should be in the first combo
|
||||
newObj.ComboIndex = closestOrigObj?.ComboIndex ?? 0;
|
||||
});
|
||||
|
||||
// The copied combo indices may not be continuous if the original map starts and ends a combo in between beats
|
||||
// e.g. A stream with each object starting a new combo
|
||||
// So combo indices need to be reprocessed to ensure continuity
|
||||
// Other kinds of combo info are also added in the process
|
||||
var combos = hitObjects.GroupBy(x => x.ComboIndex).ToList();
|
||||
|
||||
for (var i = 0; i < combos.Count; i++)
|
||||
{
|
||||
var group = combos[i].ToList();
|
||||
group.First().NewCombo = true;
|
||||
group.Last().LastInCombo = true;
|
||||
|
||||
for (var j = 0; j < group.Count; j++)
|
||||
{
|
||||
var x = group[j];
|
||||
x.ComboIndex = i;
|
||||
x.IndexInCurrentCombo = j;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void randomizeCirclePos(IReadOnlyList<OsuHitObject> hitObjects)
|
||||
{
|
||||
if (hitObjects.Count == 0) return;
|
||||
|
||||
float nextSingle(float max = 1f) => (float)(rng.NextDouble() * max);
|
||||
|
||||
const float two_pi = MathF.PI * 2;
|
||||
|
||||
var direction = two_pi * nextSingle();
|
||||
var maxComboIndex = hitObjects.Last().ComboIndex;
|
||||
|
||||
for (var i = 0; i < hitObjects.Count; i++)
|
||||
{
|
||||
var obj = hitObjects[i];
|
||||
var lastPos = i == 0
|
||||
? Vector2.Divide(OsuPlayfield.BASE_SIZE, 2)
|
||||
: hitObjects[i - 1].Position;
|
||||
|
||||
var distance = maxComboIndex == 0
|
||||
? (float)obj.Radius
|
||||
: mapRange(obj.ComboIndex, 0, maxComboIndex, (float)obj.Radius, max_base_distance);
|
||||
if (obj.NewCombo) distance *= 1.5f;
|
||||
if (obj.Kiai) distance *= 1.2f;
|
||||
distance = Math.Min(distance_cap, distance);
|
||||
|
||||
// Attempt to place the circle at a place that does not overlap with previous ones
|
||||
|
||||
var tryCount = 0;
|
||||
|
||||
// for checking overlap
|
||||
var precedingObjects = hitObjects.SkipLast(hitObjects.Count - i).TakeLast(overlap_check_count).ToList();
|
||||
|
||||
do
|
||||
{
|
||||
if (tryCount > 0) direction = two_pi * nextSingle();
|
||||
|
||||
var relativePos = new Vector2(
|
||||
distance * MathF.Cos(direction),
|
||||
distance * MathF.Sin(direction)
|
||||
);
|
||||
// Rotate the new circle away from playfield border
|
||||
relativePos = OsuHitObjectGenerationUtils.RotateAwayFromEdge(lastPos, relativePos, edge_rotation_multiplier);
|
||||
direction = MathF.Atan2(relativePos.Y, relativePos.X);
|
||||
|
||||
var newPosition = Vector2.Add(lastPos, relativePos);
|
||||
|
||||
obj.Position = newPosition;
|
||||
|
||||
clampToPlayfield(obj);
|
||||
|
||||
tryCount++;
|
||||
if (tryCount % 10 == 0) distance *= 0.9f;
|
||||
} while (distance >= obj.Radius * 2 && checkForOverlap(precedingObjects, obj));
|
||||
|
||||
if (obj.LastInCombo)
|
||||
direction = two_pi * nextSingle();
|
||||
else
|
||||
direction += distance / distance_cap * (nextSingle() * two_pi - MathF.PI);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Metronome (IApplicableToDrawableRuleset)
|
||||
|
||||
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
|
||||
{
|
||||
drawableRuleset.Overlays.Add(new TargetBeatContainer(drawableRuleset.Beatmap.HitObjects.First().StartTime));
|
||||
}
|
||||
|
||||
public class TargetBeatContainer : BeatSyncedContainer
|
||||
{
|
||||
private readonly double firstHitTime;
|
||||
|
||||
private PausableSkinnableSound sample;
|
||||
|
||||
public TargetBeatContainer(double firstHitTime)
|
||||
{
|
||||
this.firstHitTime = firstHitTime;
|
||||
AllowMistimedEventFiring = false;
|
||||
Divisor = 1;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
sample = new PausableSkinnableSound(new SampleInfo("Gameplay/catch-banana"))
|
||||
};
|
||||
}
|
||||
|
||||
protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes)
|
||||
{
|
||||
base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes);
|
||||
|
||||
if (!IsBeatSyncedWithTrack) return;
|
||||
|
||||
int timeSignature = (int)timingPoint.TimeSignature;
|
||||
|
||||
// play metronome from one measure before the first object.
|
||||
if (BeatSyncClock.CurrentTime < firstHitTime - timingPoint.BeatLength * timeSignature)
|
||||
return;
|
||||
|
||||
sample.Frequency.Value = beatIndex % timeSignature == 0 ? 1 : 0.5f;
|
||||
sample.Play();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Helper Subroutines
|
||||
|
||||
/// <summary>
|
||||
/// Check if a given time is inside a <see cref="BreakPeriod"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The given time is also considered to be inside a break if it is earlier than the
|
||||
/// start time of the first original hit object after the break.
|
||||
/// </remarks>
|
||||
/// <param name="breaks">The breaks of the beatmap.</param>
|
||||
/// <param name="time">The time to be checked.</param>=
|
||||
private bool isInsideBreakPeriod(IEnumerable<BreakPeriod> breaks, double time)
|
||||
{
|
||||
return breaks.Any(breakPeriod =>
|
||||
{
|
||||
var firstObjAfterBreak = originalHitObjects.First(obj => almostBigger(obj.StartTime, breakPeriod.EndTime));
|
||||
|
||||
return almostBigger(time, breakPeriod.StartTime)
|
||||
&& definitelyBigger(firstObjAfterBreak.StartTime, time);
|
||||
});
|
||||
}
|
||||
|
||||
private IEnumerable<double> getBeatsForTimingPoint(TimingControlPoint timingPoint, double mapEndTime)
|
||||
{
|
||||
var beats = new List<double>();
|
||||
int i = 0;
|
||||
var currentTime = timingPoint.Time;
|
||||
|
||||
while (!definitelyBigger(currentTime, mapEndTime) && controlPointInfo.TimingPointAt(currentTime) == timingPoint)
|
||||
{
|
||||
beats.Add(Math.Floor(currentTime));
|
||||
i++;
|
||||
currentTime = timingPoint.Time + i * timingPoint.BeatLength;
|
||||
}
|
||||
|
||||
return beats;
|
||||
}
|
||||
|
||||
private OsuHitObject getClosestHitObject(List<OsuHitObject> hitObjects, double time)
|
||||
{
|
||||
var precedingIndex = hitObjects.FindLastIndex(h => h.StartTime < time);
|
||||
|
||||
if (precedingIndex == hitObjects.Count - 1) return hitObjects[precedingIndex];
|
||||
|
||||
// return the closest preceding/succeeding hit object, whoever is closer in time
|
||||
return hitObjects[precedingIndex + 1].StartTime - time < time - hitObjects[precedingIndex].StartTime
|
||||
? hitObjects[precedingIndex + 1]
|
||||
: hitObjects[precedingIndex];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get samples (if any) for a specific point in time.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Samples will be returned if a hit circle or a slider node exists at that point of time.
|
||||
/// </remarks>
|
||||
/// <param name="hitObjects">The list of hit objects in a beatmap, ordered by StartTime</param>
|
||||
/// <param name="time">The point in time to get samples for</param>
|
||||
/// <returns>Hit samples</returns>
|
||||
private IList<HitSampleInfo> getSamplesAtTime(IEnumerable<OsuHitObject> hitObjects, double time)
|
||||
{
|
||||
// Get a hit object that
|
||||
// either has StartTime equal to the target time
|
||||
// or has a repeat node at the target time
|
||||
var sampleObj = hitObjects.FirstOrDefault(hitObject =>
|
||||
{
|
||||
if (almostEquals(time, hitObject.StartTime))
|
||||
return true;
|
||||
|
||||
if (!(hitObject is IHasRepeats s))
|
||||
return false;
|
||||
// If time is outside the duration of the IHasRepeats,
|
||||
// then this hitObject isn't the one we want
|
||||
if (!almostBigger(time, hitObject.StartTime)
|
||||
|| !almostBigger(s.EndTime, time))
|
||||
return false;
|
||||
|
||||
return nodeIndexFromTime(s, time - hitObject.StartTime) != -1;
|
||||
});
|
||||
if (sampleObj == null) return null;
|
||||
|
||||
IList<HitSampleInfo> samples;
|
||||
|
||||
if (sampleObj is IHasRepeats slider)
|
||||
samples = slider.NodeSamples[nodeIndexFromTime(slider, time - sampleObj.StartTime)];
|
||||
else
|
||||
samples = sampleObj.Samples;
|
||||
|
||||
return samples;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the repeat node at a point in time.
|
||||
/// </summary>
|
||||
/// <param name="curve">The slider.</param>
|
||||
/// <param name="timeSinceStart">The time since the start time of the slider.</param>
|
||||
/// <returns>Index of the node. -1 if there isn't a node at the specific time.</returns>
|
||||
private int nodeIndexFromTime(IHasRepeats curve, double timeSinceStart)
|
||||
{
|
||||
double spanDuration = curve.Duration / curve.SpanCount();
|
||||
double nodeIndex = timeSinceStart / spanDuration;
|
||||
|
||||
if (almostEquals(nodeIndex, Math.Round(nodeIndex)))
|
||||
return (int)Math.Round(nodeIndex);
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
private bool checkForOverlap(IEnumerable<OsuHitObject> objectsToCheck, OsuHitObject target)
|
||||
{
|
||||
return objectsToCheck.Any(h => Vector2.Distance(h.Position, target.Position) < target.Radius * 2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Move the hit object into playfield, taking its radius into account.
|
||||
/// </summary>
|
||||
/// <param name="obj">The hit object to be clamped.</param>
|
||||
private void clampToPlayfield(OsuHitObject obj)
|
||||
{
|
||||
var position = obj.Position;
|
||||
var radius = (float)obj.Radius;
|
||||
|
||||
if (position.Y < radius)
|
||||
position.Y = radius;
|
||||
else if (position.Y > OsuPlayfield.BASE_SIZE.Y - radius)
|
||||
position.Y = OsuPlayfield.BASE_SIZE.Y - radius;
|
||||
|
||||
if (position.X < radius)
|
||||
position.X = radius;
|
||||
else if (position.X > OsuPlayfield.BASE_SIZE.X - radius)
|
||||
position.X = OsuPlayfield.BASE_SIZE.X - radius;
|
||||
|
||||
obj.Position = position;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Re-maps a number from one range to another.
|
||||
/// </summary>
|
||||
/// <param name="value">The number to be re-mapped.</param>
|
||||
/// <param name="fromLow">Beginning of the original range.</param>
|
||||
/// <param name="fromHigh">End of the original range.</param>
|
||||
/// <param name="toLow">Beginning of the new range.</param>
|
||||
/// <param name="toHigh">End of the new range.</param>
|
||||
/// <returns>The re-mapped number.</returns>
|
||||
private static float mapRange(float value, float fromLow, float fromHigh, float toLow, float toHigh)
|
||||
{
|
||||
return (value - fromLow) * (toHigh - toLow) / (fromHigh - fromLow) + toLow;
|
||||
}
|
||||
|
||||
private static bool almostBigger(double value1, double value2)
|
||||
{
|
||||
return Precision.AlmostBigger(value1, value2, timing_precision);
|
||||
}
|
||||
|
||||
private static bool definitelyBigger(double value1, double value2)
|
||||
{
|
||||
return Precision.DefinitelyBigger(value1, value2, timing_precision);
|
||||
}
|
||||
|
||||
private static bool almostEquals(double value1, double value2)
|
||||
{
|
||||
return Precision.AlmostEquals(value1, value2, timing_precision);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Overlays.Settings;
|
||||
using osu.Game.Rulesets.Osu.Configuration;
|
||||
using osu.Game.Rulesets.UI;
|
||||
@ -11,7 +12,7 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
{
|
||||
public class OsuSettingsSubsection : RulesetSettingsSubsection
|
||||
{
|
||||
protected override string Header => "osu!";
|
||||
protected override LocalisableString Header => "osu!";
|
||||
|
||||
public OsuSettingsSubsection(Ruleset ruleset)
|
||||
: base(ruleset)
|
||||
|
@ -4,7 +4,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Game.Rulesets.Difficulty;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
@ -31,10 +30,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
public override double Calculate(Dictionary<string, double> categoryDifficulty = null)
|
||||
{
|
||||
mods = Score.Mods;
|
||||
countGreat = Score.Statistics.GetOrDefault(HitResult.Great);
|
||||
countOk = Score.Statistics.GetOrDefault(HitResult.Ok);
|
||||
countMeh = Score.Statistics.GetOrDefault(HitResult.Meh);
|
||||
countMiss = Score.Statistics.GetOrDefault(HitResult.Miss);
|
||||
countGreat = Score.Statistics.GetValueOrDefault(HitResult.Great);
|
||||
countOk = Score.Statistics.GetValueOrDefault(HitResult.Ok);
|
||||
countMeh = Score.Statistics.GetValueOrDefault(HitResult.Meh);
|
||||
countMiss = Score.Statistics.GetValueOrDefault(HitResult.Miss);
|
||||
|
||||
// Custom multipliers for NoFail and SpunOut.
|
||||
double multiplier = 1.1; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things
|
||||
|
@ -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();
|
||||
|
@ -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 });
|
||||
|
||||
|
33
osu.Game.Tests/NonVisual/ScoreInfoTest.cs
Normal file
33
osu.Game.Tests/NonVisual/ScoreInfoTest.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
@ -13,8 +13,8 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
{
|
||||
public TestSceneEditorComposeRadioButtons()
|
||||
{
|
||||
RadioButtonCollection collection;
|
||||
Add(collection = new RadioButtonCollection
|
||||
EditorRadioButtonCollection collection;
|
||||
Add(collection = new EditorRadioButtonCollection
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
|
@ -2,17 +2,24 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Edit.Tools;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Edit;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Screens.Edit.Components.RadioButtons;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Editing
|
||||
@ -20,37 +27,89 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
[TestFixture]
|
||||
public class TestSceneHitObjectComposer : EditorClockTestScene
|
||||
{
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
private OsuHitObjectComposer hitObjectComposer;
|
||||
private EditorBeatmapContainer editorBeatmapContainer;
|
||||
|
||||
private EditorBeatmap editorBeatmap => editorBeatmapContainer.EditorBeatmap;
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
Beatmap.Value = CreateWorkingBeatmap(new Beatmap
|
||||
AddStep("create beatmap", () =>
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
Beatmap.Value = CreateWorkingBeatmap(new Beatmap
|
||||
{
|
||||
new HitCircle { Position = new Vector2(256, 192), Scale = 0.5f },
|
||||
new HitCircle { Position = new Vector2(344, 148), Scale = 0.5f },
|
||||
new Slider
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
Position = new Vector2(128, 256),
|
||||
Path = new SliderPath(PathType.Linear, new[]
|
||||
new HitCircle { Position = new Vector2(256, 192), Scale = 0.5f },
|
||||
new HitCircle { Position = new Vector2(344, 148), Scale = 0.5f },
|
||||
new Slider
|
||||
{
|
||||
Vector2.Zero,
|
||||
new Vector2(216, 0),
|
||||
}),
|
||||
Scale = 0.5f,
|
||||
}
|
||||
},
|
||||
Position = new Vector2(128, 256),
|
||||
Path = new SliderPath(PathType.Linear, new[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
new Vector2(216, 0),
|
||||
}),
|
||||
Scale = 0.5f,
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
var editorBeatmap = new EditorBeatmap(Beatmap.Value.GetPlayableBeatmap(new OsuRuleset().RulesetInfo));
|
||||
AddStep("Create composer", () =>
|
||||
{
|
||||
Child = editorBeatmapContainer = new EditorBeatmapContainer(Beatmap.Value)
|
||||
{
|
||||
Child = hitObjectComposer = new OsuHitObjectComposer(new OsuRuleset())
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
var clock = new DecoupleableInterpolatingFramedClock { IsCoupled = false };
|
||||
Dependencies.CacheAs<IAdjustableClock>(clock);
|
||||
Dependencies.CacheAs<IFrameBasedClock>(clock);
|
||||
Dependencies.CacheAs(editorBeatmap);
|
||||
Dependencies.CacheAs<IBeatSnapProvider>(editorBeatmap);
|
||||
[Test]
|
||||
public void TestPlacementOnlyWorksWithTiming()
|
||||
{
|
||||
AddStep("clear all control points", () => editorBeatmap.ControlPointInfo.Clear());
|
||||
|
||||
Child = new OsuHitObjectComposer(new OsuRuleset());
|
||||
AddAssert("Tool is selection", () => hitObjectComposer.ChildrenOfType<ComposeBlueprintContainer>().First().CurrentTool is SelectTool);
|
||||
AddAssert("Hitcircle button not clickable", () => !hitObjectComposer.ChildrenOfType<EditorRadioButton>().First(d => d.Button.Label == "HitCircle").Enabled.Value);
|
||||
AddStep("Add timing point", () => editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint()));
|
||||
AddAssert("Hitcircle button is clickable", () => hitObjectComposer.ChildrenOfType<EditorRadioButton>().First(d => d.Button.Label == "HitCircle").Enabled.Value);
|
||||
AddStep("Change to hitcircle", () => hitObjectComposer.ChildrenOfType<EditorRadioButton>().First(d => d.Button.Label == "HitCircle").Click());
|
||||
AddAssert("Tool changed", () => hitObjectComposer.ChildrenOfType<ComposeBlueprintContainer>().First().CurrentTool is HitCircleCompositionTool);
|
||||
}
|
||||
|
||||
public class EditorBeatmapContainer : Container
|
||||
{
|
||||
private readonly WorkingBeatmap working;
|
||||
|
||||
public EditorBeatmap EditorBeatmap { get; private set; }
|
||||
|
||||
public EditorBeatmapContainer(WorkingBeatmap working)
|
||||
{
|
||||
this.working = working;
|
||||
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
}
|
||||
|
||||
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
|
||||
{
|
||||
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
|
||||
|
||||
EditorBeatmap = new EditorBeatmap(working.GetPlayableBeatmap(new OsuRuleset().RulesetInfo));
|
||||
|
||||
dependencies.CacheAs(EditorBeatmap);
|
||||
dependencies.CacheAs<IBeatSnapProvider>(EditorBeatmap);
|
||||
|
||||
return dependencies;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
Add(EditorBeatmap);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
{
|
||||
|
@ -0,0 +1,89 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
private void onRoomJoined(Room room, string password)
|
||||
{
|
||||
lastJoinedRoom = room;
|
||||
lastJoinedPassword = password;
|
||||
}
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Wiki;
|
||||
@ -96,7 +97,7 @@ namespace osu.Game.Tests.Visual.Online
|
||||
|
||||
private class TestHeader : WikiHeader
|
||||
{
|
||||
public IReadOnlyList<string> TabControlItems => TabControl.Items;
|
||||
public IReadOnlyList<LocalisableString?> TabControlItems => TabControl.Items;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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()));
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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);
|
||||
|
@ -28,6 +28,12 @@ namespace osu.Game.Tournament.Tests.Screens
|
||||
setMatchDate(TimeSpan.FromHours(3));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNoCurrentMatch()
|
||||
{
|
||||
AddStep("Set null current match", () => Ladder.CurrentMatch.Value = null);
|
||||
}
|
||||
|
||||
private void setMatchDate(TimeSpan relativeTime)
|
||||
// Humanizer cannot handle negative timespans.
|
||||
=> AddStep($"start time is {relativeTime}", () =>
|
||||
|
@ -1,9 +1,13 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Tournament.Models;
|
||||
using osu.Game.Tournament.Screens.Ladder.Components;
|
||||
using osu.Game.Tournament.Screens.TeamIntro;
|
||||
|
||||
namespace osu.Game.Tournament.Tests.Screens
|
||||
@ -11,16 +15,41 @@ namespace osu.Game.Tournament.Tests.Screens
|
||||
public class TestSceneSeedingScreen : TournamentTestScene
|
||||
{
|
||||
[Cached]
|
||||
private readonly LadderInfo ladder = new LadderInfo();
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
private readonly LadderInfo ladder = new LadderInfo
|
||||
{
|
||||
Add(new SeedingScreen
|
||||
Teams =
|
||||
{
|
||||
new TournamentTeam
|
||||
{
|
||||
FullName = { Value = @"Japan" },
|
||||
Acronym = { Value = "JPN" },
|
||||
SeedingResults =
|
||||
{
|
||||
new SeedingResult
|
||||
{
|
||||
// Mod intentionally left blank.
|
||||
Seed = { Value = 4 }
|
||||
},
|
||||
new SeedingResult
|
||||
{
|
||||
Mod = { Value = "DT" },
|
||||
Seed = { Value = 8 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
[Test]
|
||||
public void TestBasic()
|
||||
{
|
||||
AddStep("create seeding screen", () => Add(new SeedingScreen
|
||||
{
|
||||
FillMode = FillMode.Fit,
|
||||
FillAspectRatio = 16 / 9f
|
||||
});
|
||||
}));
|
||||
|
||||
AddStep("set team to Japan", () => this.ChildrenOfType<SettingsTeamDropdown>().Single().Current.Value = ladder.Teams.Single());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ using osu.Game.Tournament.IPC;
|
||||
|
||||
namespace osu.Game.Tournament.Screens
|
||||
{
|
||||
public abstract class BeatmapInfoScreen : TournamentScreen
|
||||
public abstract class BeatmapInfoScreen : TournamentMatchScreen
|
||||
{
|
||||
protected readonly SongBar SongBar;
|
||||
|
||||
|
@ -24,8 +24,6 @@ namespace osu.Game.Tournament.Screens.Gameplay
|
||||
{
|
||||
private readonly BindableBool warmup = new BindableBool();
|
||||
|
||||
private readonly Bindable<TournamentMatch> currentMatch = new Bindable<TournamentMatch>();
|
||||
|
||||
public readonly Bindable<TourneyState> State = new Bindable<TourneyState>();
|
||||
private OsuButton warmupButton;
|
||||
private MatchIPCInfo ipc;
|
||||
@ -131,14 +129,6 @@ namespace osu.Game.Tournament.Screens.Gameplay
|
||||
|
||||
ladder.ChromaKeyWidth.BindValueChanged(width => chroma.Width = width.NewValue, true);
|
||||
|
||||
currentMatch.BindValueChanged(m =>
|
||||
{
|
||||
warmup.Value = m.NewValue.Team1Score.Value + m.NewValue.Team2Score.Value == 0;
|
||||
scheduledOperation?.Cancel();
|
||||
});
|
||||
|
||||
currentMatch.BindTo(ladder.CurrentMatch);
|
||||
|
||||
warmup.BindValueChanged(w =>
|
||||
{
|
||||
warmupButton.Alpha = !w.NewValue ? 0.5f : 1;
|
||||
@ -146,6 +136,17 @@ namespace osu.Game.Tournament.Screens.Gameplay
|
||||
}, true);
|
||||
}
|
||||
|
||||
protected override void CurrentMatchChanged(ValueChangedEvent<TournamentMatch> match)
|
||||
{
|
||||
base.CurrentMatchChanged(match);
|
||||
|
||||
if (match.NewValue == null)
|
||||
return;
|
||||
|
||||
warmup.Value = match.NewValue.Team1Score.Value + match.NewValue.Team2Score.Value == 0;
|
||||
scheduledOperation?.Cancel();
|
||||
}
|
||||
|
||||
private ScheduledDelegate scheduledOperation;
|
||||
private MatchScoreDisplay scoreDisplay;
|
||||
|
||||
@ -161,9 +162,9 @@ namespace osu.Game.Tournament.Screens.Gameplay
|
||||
if (warmup.Value) return;
|
||||
|
||||
if (ipc.Score1.Value > ipc.Score2.Value)
|
||||
currentMatch.Value.Team1Score.Value++;
|
||||
CurrentMatch.Value.Team1Score.Value++;
|
||||
else
|
||||
currentMatch.Value.Team2Score.Value++;
|
||||
CurrentMatch.Value.Team2Score.Value++;
|
||||
}
|
||||
|
||||
scheduledOperation?.Cancel();
|
||||
@ -198,9 +199,9 @@ namespace osu.Game.Tournament.Screens.Gameplay
|
||||
// we should automatically proceed after a short delay
|
||||
if (lastState == TourneyState.Ranking && !warmup.Value)
|
||||
{
|
||||
if (currentMatch.Value?.Completed.Value == true)
|
||||
if (CurrentMatch.Value?.Completed.Value == true)
|
||||
scheduledOperation = Scheduler.AddDelayed(() => { sceneManager?.SetScreen(typeof(TeamWinScreen)); }, delay_before_progression);
|
||||
else if (currentMatch.Value?.Completed.Value == false)
|
||||
else if (CurrentMatch.Value?.Completed.Value == false)
|
||||
scheduledOperation = Scheduler.AddDelayed(() => { sceneManager?.SetScreen(typeof(MapPoolScreen)); }, delay_before_progression);
|
||||
}
|
||||
|
||||
|
@ -303,6 +303,15 @@ namespace osu.Game.Tournament.Screens.Ladder.Components
|
||||
Match.LosersProgression.Value = null;
|
||||
|
||||
ladderInfo.Matches.Remove(Match);
|
||||
|
||||
foreach (var m in ladderInfo.Matches)
|
||||
{
|
||||
if (m.Progression.Value == Match)
|
||||
m.Progression.Value = null;
|
||||
|
||||
if (m.LosersProgression.Value == Match)
|
||||
m.LosersProgression.Value = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -21,12 +21,10 @@ using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tournament.Screens.MapPool
|
||||
{
|
||||
public class MapPoolScreen : TournamentScreen
|
||||
public class MapPoolScreen : TournamentMatchScreen
|
||||
{
|
||||
private readonly FillFlowContainer<FillFlowContainer<TournamentBeatmapPanel>> mapFlows;
|
||||
|
||||
private readonly Bindable<TournamentMatch> currentMatch = new Bindable<TournamentMatch>();
|
||||
|
||||
[Resolved(canBeNull: true)]
|
||||
private TournamentSceneManager sceneManager { get; set; }
|
||||
|
||||
@ -96,7 +94,7 @@ namespace osu.Game.Tournament.Screens.MapPool
|
||||
Action = reset
|
||||
},
|
||||
new ControlPanel.Spacer(),
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -104,15 +102,12 @@ namespace osu.Game.Tournament.Screens.MapPool
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(MatchIPCInfo ipc)
|
||||
{
|
||||
currentMatch.BindValueChanged(matchChanged);
|
||||
currentMatch.BindTo(LadderInfo.CurrentMatch);
|
||||
|
||||
ipc.Beatmap.BindValueChanged(beatmapChanged);
|
||||
}
|
||||
|
||||
private void beatmapChanged(ValueChangedEvent<BeatmapInfo> beatmap)
|
||||
{
|
||||
if (currentMatch.Value == null || currentMatch.Value.PicksBans.Count(p => p.Type == ChoiceType.Ban) < 2)
|
||||
if (CurrentMatch.Value == null || CurrentMatch.Value.PicksBans.Count(p => p.Type == ChoiceType.Ban) < 2)
|
||||
return;
|
||||
|
||||
// if bans have already been placed, beatmap changes result in a selection being made autoamtically
|
||||
@ -137,12 +132,12 @@ namespace osu.Game.Tournament.Screens.MapPool
|
||||
{
|
||||
const TeamColour roll_winner = TeamColour.Red; //todo: draw from match
|
||||
|
||||
var nextColour = (currentMatch.Value.PicksBans.LastOrDefault()?.Team ?? roll_winner) == TeamColour.Red ? TeamColour.Blue : TeamColour.Red;
|
||||
var nextColour = (CurrentMatch.Value.PicksBans.LastOrDefault()?.Team ?? roll_winner) == TeamColour.Red ? TeamColour.Blue : TeamColour.Red;
|
||||
|
||||
if (pickType == ChoiceType.Ban && currentMatch.Value.PicksBans.Count(p => p.Type == ChoiceType.Ban) >= 2)
|
||||
if (pickType == ChoiceType.Ban && CurrentMatch.Value.PicksBans.Count(p => p.Type == ChoiceType.Ban) >= 2)
|
||||
setMode(pickColour, ChoiceType.Pick);
|
||||
else
|
||||
setMode(nextColour, currentMatch.Value.PicksBans.Count(p => p.Type == ChoiceType.Ban) >= 2 ? ChoiceType.Pick : ChoiceType.Ban);
|
||||
setMode(nextColour, CurrentMatch.Value.PicksBans.Count(p => p.Type == ChoiceType.Ban) >= 2 ? ChoiceType.Pick : ChoiceType.Ban);
|
||||
}
|
||||
|
||||
protected override bool OnMouseDown(MouseDownEvent e)
|
||||
@ -156,11 +151,11 @@ namespace osu.Game.Tournament.Screens.MapPool
|
||||
addForBeatmap(map.Beatmap.OnlineBeatmapID.Value);
|
||||
else
|
||||
{
|
||||
var existing = currentMatch.Value.PicksBans.FirstOrDefault(p => p.BeatmapID == map.Beatmap.OnlineBeatmapID);
|
||||
var existing = CurrentMatch.Value.PicksBans.FirstOrDefault(p => p.BeatmapID == map.Beatmap.OnlineBeatmapID);
|
||||
|
||||
if (existing != null)
|
||||
{
|
||||
currentMatch.Value.PicksBans.Remove(existing);
|
||||
CurrentMatch.Value.PicksBans.Remove(existing);
|
||||
setNextMode();
|
||||
}
|
||||
}
|
||||
@ -173,7 +168,7 @@ namespace osu.Game.Tournament.Screens.MapPool
|
||||
|
||||
private void reset()
|
||||
{
|
||||
currentMatch.Value.PicksBans.Clear();
|
||||
CurrentMatch.Value.PicksBans.Clear();
|
||||
setNextMode();
|
||||
}
|
||||
|
||||
@ -181,18 +176,18 @@ namespace osu.Game.Tournament.Screens.MapPool
|
||||
|
||||
private void addForBeatmap(int beatmapId)
|
||||
{
|
||||
if (currentMatch.Value == null)
|
||||
if (CurrentMatch.Value == null)
|
||||
return;
|
||||
|
||||
if (currentMatch.Value.Round.Value.Beatmaps.All(b => b.BeatmapInfo.OnlineBeatmapID != beatmapId))
|
||||
if (CurrentMatch.Value.Round.Value.Beatmaps.All(b => b.BeatmapInfo.OnlineBeatmapID != beatmapId))
|
||||
// don't attempt to add if the beatmap isn't in our pool
|
||||
return;
|
||||
|
||||
if (currentMatch.Value.PicksBans.Any(p => p.BeatmapID == beatmapId))
|
||||
if (CurrentMatch.Value.PicksBans.Any(p => p.BeatmapID == beatmapId))
|
||||
// don't attempt to add if already exists.
|
||||
return;
|
||||
|
||||
currentMatch.Value.PicksBans.Add(new BeatmapChoice
|
||||
CurrentMatch.Value.PicksBans.Add(new BeatmapChoice
|
||||
{
|
||||
Team = pickColour,
|
||||
Type = pickType,
|
||||
@ -201,17 +196,22 @@ namespace osu.Game.Tournament.Screens.MapPool
|
||||
|
||||
setNextMode();
|
||||
|
||||
if (pickType == ChoiceType.Pick && currentMatch.Value.PicksBans.Any(i => i.Type == ChoiceType.Pick))
|
||||
if (pickType == ChoiceType.Pick && CurrentMatch.Value.PicksBans.Any(i => i.Type == ChoiceType.Pick))
|
||||
{
|
||||
scheduledChange?.Cancel();
|
||||
scheduledChange = Scheduler.AddDelayed(() => { sceneManager?.SetScreen(typeof(GameplayScreen)); }, 10000);
|
||||
}
|
||||
}
|
||||
|
||||
private void matchChanged(ValueChangedEvent<TournamentMatch> match)
|
||||
protected override void CurrentMatchChanged(ValueChangedEvent<TournamentMatch> match)
|
||||
{
|
||||
base.CurrentMatchChanged(match);
|
||||
|
||||
mapFlows.Clear();
|
||||
|
||||
if (match.NewValue == null)
|
||||
return;
|
||||
|
||||
int totalRows = 0;
|
||||
|
||||
if (match.NewValue.Round.Value != null)
|
||||
|
@ -96,19 +96,18 @@ namespace osu.Game.Tournament.Screens.Schedule
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
currentMatch.BindValueChanged(matchChanged);
|
||||
currentMatch.BindTo(ladder.CurrentMatch);
|
||||
currentMatch.BindValueChanged(matchChanged, true);
|
||||
}
|
||||
|
||||
private void matchChanged(ValueChangedEvent<TournamentMatch> match)
|
||||
{
|
||||
if (match.NewValue == null)
|
||||
{
|
||||
mainContainer.Clear();
|
||||
return;
|
||||
}
|
||||
|
||||
var upcoming = ladder.Matches.Where(p => !p.Completed.Value && p.Team1.Value != null && p.Team2.Value != null && Math.Abs(p.Date.Value.DayOfYear - DateTimeOffset.UtcNow.DayOfYear) < 4);
|
||||
var conditionals = ladder
|
||||
.Matches.Where(p => !p.Completed.Value && (p.Team1.Value == null || p.Team2.Value == null) && Math.Abs(p.Date.Value.DayOfYear - DateTimeOffset.UtcNow.DayOfYear) < 4)
|
||||
@ -117,6 +116,8 @@ namespace osu.Game.Tournament.Screens.Schedule
|
||||
upcoming = upcoming.Concat(conditionals);
|
||||
upcoming = upcoming.OrderBy(p => p.Date.Value).Take(8);
|
||||
|
||||
ScheduleContainer comingUpNext;
|
||||
|
||||
mainContainer.Child = new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
@ -153,57 +154,58 @@ namespace osu.Game.Tournament.Screens.Schedule
|
||||
}
|
||||
}
|
||||
},
|
||||
new ScheduleContainer("coming up next")
|
||||
comingUpNext = new ScheduleContainer("coming up next")
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Height = 0.25f,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new FillFlowContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Horizontal,
|
||||
Spacing = new Vector2(30),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new ScheduleMatch(match.NewValue, false)
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
},
|
||||
new TournamentSpriteTextWithBackground(match.NewValue.Round.Value?.Name.Value)
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Scale = new Vector2(0.5f)
|
||||
},
|
||||
new TournamentSpriteText
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Text = match.NewValue.Team1.Value?.FullName + " vs " + match.NewValue.Team2.Value?.FullName,
|
||||
Font = OsuFont.Torus.With(size: 24, weight: FontWeight.SemiBold)
|
||||
},
|
||||
new FillFlowContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Horizontal,
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new ScheduleMatchDate(match.NewValue.Date.Value)
|
||||
{
|
||||
Font = OsuFont.Torus.With(size: 24, weight: FontWeight.Regular)
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (match.NewValue != null)
|
||||
{
|
||||
comingUpNext.Child = new FillFlowContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Horizontal,
|
||||
Spacing = new Vector2(30),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new ScheduleMatch(match.NewValue, false)
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
},
|
||||
new TournamentSpriteTextWithBackground(match.NewValue.Round.Value?.Name.Value)
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Scale = new Vector2(0.5f)
|
||||
},
|
||||
new TournamentSpriteText
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Text = match.NewValue.Team1.Value?.FullName + " vs " + match.NewValue.Team2.Value?.FullName,
|
||||
Font = OsuFont.Torus.With(size: 24, weight: FontWeight.SemiBold)
|
||||
},
|
||||
new FillFlowContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Horizontal,
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new ScheduleMatchDate(match.NewValue.Date.Value)
|
||||
{
|
||||
Font = OsuFont.Torus.With(size: 24, weight: FontWeight.Regular)
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public class ScheduleMatch : DrawableTournamentMatch
|
||||
|
@ -2,10 +2,12 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Tournament.Components;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Tournament.Models;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Tournament.Screens.Showcase
|
||||
@ -39,5 +41,11 @@ namespace osu.Game.Tournament.Screens.Showcase
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected override void CurrentMatchChanged(ValueChangedEvent<TournamentMatch> match)
|
||||
{
|
||||
// showcase screen doesn't care about a match being selected.
|
||||
// base call intentionally omitted to not show match warning.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -18,12 +18,10 @@ using osuTK;
|
||||
|
||||
namespace osu.Game.Tournament.Screens.TeamIntro
|
||||
{
|
||||
public class SeedingScreen : TournamentScreen, IProvideVideo
|
||||
public class SeedingScreen : TournamentMatchScreen, IProvideVideo
|
||||
{
|
||||
private Container mainContainer;
|
||||
|
||||
private readonly Bindable<TournamentMatch> currentMatch = new Bindable<TournamentMatch>();
|
||||
|
||||
private readonly Bindable<TournamentTeam> currentTeam = new Bindable<TournamentTeam>();
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
@ -50,13 +48,13 @@ namespace osu.Game.Tournament.Screens.TeamIntro
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Text = "Show first team",
|
||||
Action = () => currentTeam.Value = currentMatch.Value.Team1.Value,
|
||||
Action = () => currentTeam.Value = CurrentMatch.Value.Team1.Value,
|
||||
},
|
||||
new TourneyButton
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Text = "Show second team",
|
||||
Action = () => currentTeam.Value = currentMatch.Value.Team2.Value,
|
||||
Action = () => currentTeam.Value = CurrentMatch.Value.Team2.Value,
|
||||
},
|
||||
new SettingsTeamDropdown(LadderInfo.Teams)
|
||||
{
|
||||
@ -67,9 +65,6 @@ namespace osu.Game.Tournament.Screens.TeamIntro
|
||||
}
|
||||
};
|
||||
|
||||
currentMatch.BindValueChanged(matchChanged);
|
||||
currentMatch.BindTo(LadderInfo.CurrentMatch);
|
||||
|
||||
currentTeam.BindValueChanged(teamChanged, true);
|
||||
}
|
||||
|
||||
@ -84,8 +79,15 @@ namespace osu.Game.Tournament.Screens.TeamIntro
|
||||
showTeam(team.NewValue);
|
||||
}
|
||||
|
||||
private void matchChanged(ValueChangedEvent<TournamentMatch> match) =>
|
||||
currentTeam.Value = currentMatch.Value.Team1.Value;
|
||||
protected override void CurrentMatchChanged(ValueChangedEvent<TournamentMatch> match)
|
||||
{
|
||||
base.CurrentMatchChanged(match);
|
||||
|
||||
if (match.NewValue == null)
|
||||
return;
|
||||
|
||||
currentTeam.Value = match.NewValue.Team1.Value;
|
||||
}
|
||||
|
||||
private void showTeam(TournamentTeam team)
|
||||
{
|
||||
@ -179,44 +181,48 @@ namespace osu.Game.Tournament.Screens.TeamIntro
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(TextureStore textures)
|
||||
{
|
||||
FillFlowContainer row;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new FillFlowContainer
|
||||
row = new FillFlowContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Horizontal,
|
||||
Spacing = new Vector2(5),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Sprite
|
||||
{
|
||||
Texture = textures.Get($"mods/{mods.ToLower()}"),
|
||||
Scale = new Vector2(0.5f)
|
||||
},
|
||||
new Container
|
||||
{
|
||||
Size = new Vector2(50, 16),
|
||||
CornerRadius = 10,
|
||||
Masking = true,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = TournamentGame.ELEMENT_BACKGROUND_COLOUR,
|
||||
},
|
||||
new TournamentSpriteText
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Text = seeding.ToString("#,0"),
|
||||
Colour = TournamentGame.ELEMENT_FOREGROUND_COLOUR
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(mods))
|
||||
{
|
||||
row.Add(new Sprite
|
||||
{
|
||||
Texture = textures.Get($"mods/{mods.ToLower()}"),
|
||||
Scale = new Vector2(0.5f)
|
||||
});
|
||||
}
|
||||
|
||||
row.Add(new Container
|
||||
{
|
||||
Size = new Vector2(50, 16),
|
||||
CornerRadius = 10,
|
||||
Masking = true,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = TournamentGame.ELEMENT_BACKGROUND_COLOUR,
|
||||
},
|
||||
new TournamentSpriteText
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Text = seeding.ToString("#,0"),
|
||||
Colour = TournamentGame.ELEMENT_FOREGROUND_COLOUR
|
||||
},
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -12,12 +12,10 @@ using osuTK;
|
||||
|
||||
namespace osu.Game.Tournament.Screens.TeamIntro
|
||||
{
|
||||
public class TeamIntroScreen : TournamentScreen, IProvideVideo
|
||||
public class TeamIntroScreen : TournamentMatchScreen, IProvideVideo
|
||||
{
|
||||
private Container mainContainer;
|
||||
|
||||
private readonly Bindable<TournamentMatch> currentMatch = new Bindable<TournamentMatch>();
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(Storage storage)
|
||||
{
|
||||
@ -35,18 +33,16 @@ namespace osu.Game.Tournament.Screens.TeamIntro
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
}
|
||||
};
|
||||
|
||||
currentMatch.BindValueChanged(matchChanged);
|
||||
currentMatch.BindTo(LadderInfo.CurrentMatch);
|
||||
}
|
||||
|
||||
private void matchChanged(ValueChangedEvent<TournamentMatch> match)
|
||||
protected override void CurrentMatchChanged(ValueChangedEvent<TournamentMatch> match)
|
||||
{
|
||||
base.CurrentMatchChanged(match);
|
||||
|
||||
mainContainer.Clear();
|
||||
|
||||
if (match.NewValue == null)
|
||||
{
|
||||
mainContainer.Clear();
|
||||
return;
|
||||
}
|
||||
|
||||
const float y_flag_offset = 292;
|
||||
|
||||
|
@ -13,11 +13,10 @@ using osuTK;
|
||||
|
||||
namespace osu.Game.Tournament.Screens.TeamWin
|
||||
{
|
||||
public class TeamWinScreen : TournamentScreen, IProvideVideo
|
||||
public class TeamWinScreen : TournamentMatchScreen, IProvideVideo
|
||||
{
|
||||
private Container mainContainer;
|
||||
|
||||
private readonly Bindable<TournamentMatch> currentMatch = new Bindable<TournamentMatch>();
|
||||
private readonly Bindable<bool> currentCompleted = new Bindable<bool>();
|
||||
|
||||
private TourneyVideo blueWinVideo;
|
||||
@ -48,17 +47,19 @@ namespace osu.Game.Tournament.Screens.TeamWin
|
||||
}
|
||||
};
|
||||
|
||||
currentMatch.BindValueChanged(matchChanged);
|
||||
currentMatch.BindTo(ladder.CurrentMatch);
|
||||
|
||||
currentCompleted.BindValueChanged(_ => update());
|
||||
}
|
||||
|
||||
private void matchChanged(ValueChangedEvent<TournamentMatch> match)
|
||||
protected override void CurrentMatchChanged(ValueChangedEvent<TournamentMatch> match)
|
||||
{
|
||||
currentCompleted.UnbindBindings();
|
||||
currentCompleted.BindTo(match.NewValue.Completed);
|
||||
base.CurrentMatchChanged(match);
|
||||
|
||||
currentCompleted.UnbindBindings();
|
||||
|
||||
if (match.NewValue == null)
|
||||
return;
|
||||
|
||||
currentCompleted.BindTo(match.NewValue.Completed);
|
||||
update();
|
||||
}
|
||||
|
||||
@ -66,7 +67,7 @@ namespace osu.Game.Tournament.Screens.TeamWin
|
||||
|
||||
private void update() => Schedule(() =>
|
||||
{
|
||||
var match = currentMatch.Value;
|
||||
var match = CurrentMatch.Value;
|
||||
|
||||
if (match.Winner == null)
|
||||
{
|
||||
|
34
osu.Game.Tournament/Screens/TournamentMatchScreen.cs
Normal file
34
osu.Game.Tournament/Screens/TournamentMatchScreen.cs
Normal file
@ -0,0 +1,34 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Tournament.Models;
|
||||
|
||||
namespace osu.Game.Tournament.Screens
|
||||
{
|
||||
public abstract class TournamentMatchScreen : TournamentScreen
|
||||
{
|
||||
protected readonly Bindable<TournamentMatch> CurrentMatch = new Bindable<TournamentMatch>();
|
||||
private WarningBox noMatchWarning;
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
CurrentMatch.BindTo(LadderInfo.CurrentMatch);
|
||||
CurrentMatch.BindValueChanged(CurrentMatchChanged, true);
|
||||
}
|
||||
|
||||
protected virtual void CurrentMatchChanged(ValueChangedEvent<TournamentMatch> match)
|
||||
{
|
||||
if (match.NewValue == null)
|
||||
{
|
||||
AddInternal(noMatchWarning = new WarningBox("Choose a match first from the brackets screen"));
|
||||
return;
|
||||
}
|
||||
|
||||
noMatchWarning?.Expire();
|
||||
noMatchWarning = null;
|
||||
}
|
||||
}
|
||||
}
|
@ -97,7 +97,12 @@ namespace osu.Game.Tournament
|
||||
},
|
||||
}
|
||||
},
|
||||
heightWarning = new WarningBox("Please make the window wider"),
|
||||
heightWarning = new WarningBox("Please make the window wider")
|
||||
{
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.BottomCentre,
|
||||
Margin = new MarginPadding(20),
|
||||
},
|
||||
new OsuContextMenuContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
|
@ -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());
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -251,11 +251,8 @@ namespace osu.Game.Beatmaps.Formats
|
||||
switch (beatmap.BeatmapInfo.RulesetID)
|
||||
{
|
||||
case 0:
|
||||
position = ((IHasPosition)hitObject).Position;
|
||||
break;
|
||||
|
||||
case 2:
|
||||
position.X = ((IHasXPosition)hitObject).X;
|
||||
position = ((IHasPosition)hitObject).Position;
|
||||
break;
|
||||
|
||||
case 3:
|
||||
|
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
|
||||
@ -153,6 +154,27 @@ namespace osu.Game.Graphics.UserInterface
|
||||
AutoSizeAxes = Axes.X;
|
||||
RelativeSizeAxes = Axes.Y;
|
||||
|
||||
LocalisableString text;
|
||||
|
||||
switch (value)
|
||||
{
|
||||
case IHasDescription hasDescription:
|
||||
text = hasDescription.GetDescription();
|
||||
break;
|
||||
|
||||
case Enum e:
|
||||
text = e.GetLocalisableDescription();
|
||||
break;
|
||||
|
||||
case LocalisableString l:
|
||||
text = l;
|
||||
break;
|
||||
|
||||
default:
|
||||
text = value.ToString();
|
||||
break;
|
||||
}
|
||||
|
||||
Children = new Drawable[]
|
||||
{
|
||||
Text = new OsuSpriteText
|
||||
@ -160,7 +182,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
Margin = new MarginPadding { Top = 5, Bottom = 5 },
|
||||
Origin = Anchor.BottomLeft,
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Text = (value as IHasDescription)?.Description ?? (value as Enum)?.GetLocalisableDescription() ?? value.ToString(),
|
||||
Text = text,
|
||||
Font = OsuFont.GetFont(size: 14)
|
||||
},
|
||||
Bar = new Box
|
||||
|
29
osu.Game/Localisation/BindingSettingsStrings.cs
Normal file
29
osu.Game/Localisation/BindingSettingsStrings.cs
Normal file
@ -0,0 +1,29 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Localisation;
|
||||
|
||||
namespace osu.Game.Localisation
|
||||
{
|
||||
public static class BindingSettingsStrings
|
||||
{
|
||||
private const string prefix = @"osu.Game.Resources.Localisation.BindingSettings";
|
||||
|
||||
/// <summary>
|
||||
/// "Shortcut and gameplay bindings"
|
||||
/// </summary>
|
||||
public static LocalisableString ShortcutAndGameplayBindings => new TranslatableString(getKey(@"shortcut_and_gameplay_bindings"), @"Shortcut and gameplay bindings");
|
||||
|
||||
/// <summary>
|
||||
/// "Configure"
|
||||
/// </summary>
|
||||
public static LocalisableString Configure => new TranslatableString(getKey(@"configure"), @"Configure");
|
||||
|
||||
/// <summary>
|
||||
/// "change global shortcut keys and gameplay bindings"
|
||||
/// </summary>
|
||||
public static LocalisableString ChangeBindingsButton => new TranslatableString(getKey(@"change_bindings_button"), @"change global shortcut keys and gameplay bindings");
|
||||
|
||||
private static string getKey(string key) => $@"{prefix}:{key}";
|
||||
}
|
||||
}
|
@ -14,6 +14,21 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
public static LocalisableString Cancel => new TranslatableString(getKey(@"cancel"), @"Cancel");
|
||||
|
||||
private static string getKey(string key) => $"{prefix}:{key}";
|
||||
/// <summary>
|
||||
/// "Enabled"
|
||||
/// </summary>
|
||||
public static LocalisableString Enabled => new TranslatableString(getKey(@"enabled"), @"Enabled");
|
||||
|
||||
/// <summary>
|
||||
/// "Width"
|
||||
/// </summary>
|
||||
public static LocalisableString Width => new TranslatableString(getKey(@"width"), @"Width");
|
||||
|
||||
/// <summary>
|
||||
/// "Height"
|
||||
/// </summary>
|
||||
public static LocalisableString Height => new TranslatableString(getKey(@"height"), @"Height");
|
||||
|
||||
private static string getKey(string key) => $@"{prefix}:{key}";
|
||||
}
|
||||
}
|
||||
}
|
59
osu.Game/Localisation/MouseSettingsStrings.cs
Normal file
59
osu.Game/Localisation/MouseSettingsStrings.cs
Normal file
@ -0,0 +1,59 @@
|
||||
// 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;
|
||||
|
||||
namespace osu.Game.Localisation
|
||||
{
|
||||
public static class MouseSettingsStrings
|
||||
{
|
||||
private const string prefix = @"osu.Game.Resources.Localisation.MouseSettings";
|
||||
|
||||
/// <summary>
|
||||
/// "Mouse"
|
||||
/// </summary>
|
||||
public static LocalisableString Mouse => new TranslatableString(getKey(@"mouse"), @"Mouse");
|
||||
|
||||
/// <summary>
|
||||
/// "Not applicable in full screen mode"
|
||||
/// </summary>
|
||||
public static LocalisableString NotApplicableFullscreen => new TranslatableString(getKey(@"not_applicable_full_screen"), @"Not applicable in full screen mode");
|
||||
|
||||
/// <summary>
|
||||
/// "High precision mouse"
|
||||
/// </summary>
|
||||
public static LocalisableString HighPrecisionMouse => new TranslatableString(getKey(@"high_precision_mouse"), @"High precision mouse");
|
||||
|
||||
/// <summary>
|
||||
/// "Attempts to bypass any operation system mouse acceleration. On windows, this is equivalent to what used to be known as "Raw Input"."
|
||||
/// </summary>
|
||||
public static LocalisableString HighPrecisionMouseTooltip => new TranslatableString(getKey(@"high_precision_mouse_tooltip"), @"Attempts to bypass any operation system mouse acceleration. On windows, this is equivalent to what used to be known as ""Raw Input"".");
|
||||
|
||||
/// <summary>
|
||||
/// "Confine mouse cursor to window"
|
||||
/// </summary>
|
||||
public static LocalisableString ConfineMouseMode => new TranslatableString(getKey(@"confine_mouse_mode"), @"Confine mouse cursor to window");
|
||||
|
||||
/// <summary>
|
||||
/// "Disable mouse wheel during gameplay"
|
||||
/// </summary>
|
||||
public static LocalisableString DisableMouseWheel => new TranslatableString(getKey(@"disable_mouse_wheel"), @"Disable mouse wheel during gameplay");
|
||||
|
||||
/// <summary>
|
||||
/// "Disable mouse buttons during gameplay"
|
||||
/// </summary>
|
||||
public static LocalisableString DisableMouseButtons => new TranslatableString(getKey(@"disable_mouse_buttons"), @"Disable mouse buttons during gameplay");
|
||||
|
||||
/// <summary>
|
||||
/// "Enable high precision mouse to adjust sensitivity"
|
||||
/// </summary>
|
||||
public static LocalisableString EnableHighPrecisionForSensitivityAdjust => new TranslatableString(getKey(@"enable_high_precision_for_sensitivity_adjust"), @"Enable high precision mouse to adjust sensitivity");
|
||||
|
||||
/// <summary>
|
||||
/// "Cursor sensitivity"
|
||||
/// </summary>
|
||||
public static LocalisableString CursorSensitivity => new TranslatableString(getKey(@"cursor_sensitivity"), @"Cursor sensitivity");
|
||||
|
||||
private static string getKey(string key) => $@"{prefix}:{key}";
|
||||
}
|
||||
}
|
59
osu.Game/Localisation/TabletSettingsStrings.cs
Normal file
59
osu.Game/Localisation/TabletSettingsStrings.cs
Normal file
@ -0,0 +1,59 @@
|
||||
// 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;
|
||||
|
||||
namespace osu.Game.Localisation
|
||||
{
|
||||
public static class TabletSettingsStrings
|
||||
{
|
||||
private const string prefix = @"osu.Game.Resources.Localisation.TabletSettings";
|
||||
|
||||
/// <summary>
|
||||
/// "Tablet"
|
||||
/// </summary>
|
||||
public static LocalisableString Tablet => new TranslatableString(getKey(@"tablet"), @"Tablet");
|
||||
|
||||
/// <summary>
|
||||
/// "No tablet detected!"
|
||||
/// </summary>
|
||||
public static LocalisableString NoTabletDetected => new TranslatableString(getKey(@"no_tablet_detected"), @"No tablet detected!");
|
||||
|
||||
/// <summary>
|
||||
/// "Reset to full area"
|
||||
/// </summary>
|
||||
public static LocalisableString ResetToFullArea => new TranslatableString(getKey(@"reset_to_full_area"), @"Reset to full area");
|
||||
|
||||
/// <summary>
|
||||
/// "Conform to current game aspect ratio"
|
||||
/// </summary>
|
||||
public static LocalisableString ConformToCurrentGameAspectRatio => new TranslatableString(getKey(@"conform_to_current_game_aspect_ratio"), @"Conform to current game aspect ratio");
|
||||
|
||||
/// <summary>
|
||||
/// "X Offset"
|
||||
/// </summary>
|
||||
public static LocalisableString XOffset => new TranslatableString(getKey(@"x_offset"), @"X Offset");
|
||||
|
||||
/// <summary>
|
||||
/// "Y Offset"
|
||||
/// </summary>
|
||||
public static LocalisableString YOffset => new TranslatableString(getKey(@"y_offset"), @"Y Offset");
|
||||
|
||||
/// <summary>
|
||||
/// "Rotation"
|
||||
/// </summary>
|
||||
public static LocalisableString Rotation => new TranslatableString(getKey(@"rotation"), @"Rotation");
|
||||
|
||||
/// <summary>
|
||||
/// "Aspect Ratio"
|
||||
/// </summary>
|
||||
public static LocalisableString AspectRatio => new TranslatableString(getKey(@"aspect_ratio"), @"Aspect Ratio");
|
||||
|
||||
/// <summary>
|
||||
/// "Lock aspect ratio"
|
||||
/// </summary>
|
||||
public static LocalisableString LockAspectRatio => new TranslatableString(getKey(@"lock_aspect_ratio"), @"Lock aspect ratio");
|
||||
|
||||
private static string getKey(string key) => $@"{prefix}:{key}";
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
22
osu.Game/Online/Multiplayer/InvalidPasswordException.cs
Normal file
22
osu.Game/Online/Multiplayer/InvalidPasswordException.cs
Normal 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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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}";
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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}";
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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));
|
||||
|
||||
|
@ -4,15 +4,16 @@
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
|
||||
namespace osu.Game.Overlays
|
||||
{
|
||||
public abstract class BreadcrumbControlOverlayHeader : TabControlOverlayHeader<string>
|
||||
public abstract class BreadcrumbControlOverlayHeader : TabControlOverlayHeader<LocalisableString?>
|
||||
{
|
||||
protected override OsuTabControl<string> CreateTabControl() => new OverlayHeaderBreadcrumbControl();
|
||||
protected override OsuTabControl<LocalisableString?> CreateTabControl() => new OverlayHeaderBreadcrumbControl();
|
||||
|
||||
public class OverlayHeaderBreadcrumbControl : BreadcrumbControl<string>
|
||||
public class OverlayHeaderBreadcrumbControl : BreadcrumbControl<LocalisableString?>
|
||||
{
|
||||
public OverlayHeaderBreadcrumbControl()
|
||||
{
|
||||
@ -26,7 +27,7 @@ namespace osu.Game.Overlays
|
||||
AccentColour = colourProvider.Light2;
|
||||
}
|
||||
|
||||
protected override TabItem<string> CreateTabItem(string value) => new ControlTabItem(value)
|
||||
protected override TabItem<LocalisableString?> CreateTabItem(LocalisableString? value) => new ControlTabItem(value)
|
||||
{
|
||||
AccentColour = AccentColour,
|
||||
};
|
||||
@ -35,7 +36,7 @@ namespace osu.Game.Overlays
|
||||
{
|
||||
protected override float ChevronSize => 8;
|
||||
|
||||
public ControlTabItem(string value)
|
||||
public ControlTabItem(LocalisableString? value)
|
||||
: base(value)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Y;
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Input.Bindings;
|
||||
using osu.Game.Overlays.Settings;
|
||||
|
||||
@ -28,7 +29,7 @@ namespace osu.Game.Overlays.KeyBinding
|
||||
|
||||
private class DefaultBindingsSubsection : KeyBindingsSubsection
|
||||
{
|
||||
protected override string Header => string.Empty;
|
||||
protected override LocalisableString Header => string.Empty;
|
||||
|
||||
public DefaultBindingsSubsection(GlobalActionContainer manager)
|
||||
: base(null)
|
||||
@ -39,7 +40,7 @@ namespace osu.Game.Overlays.KeyBinding
|
||||
|
||||
private class SongSelectKeyBindingSubsection : KeyBindingsSubsection
|
||||
{
|
||||
protected override string Header => "Song Select";
|
||||
protected override LocalisableString Header => "Song Select";
|
||||
|
||||
public SongSelectKeyBindingSubsection(GlobalActionContainer manager)
|
||||
: base(null)
|
||||
@ -50,7 +51,7 @@ namespace osu.Game.Overlays.KeyBinding
|
||||
|
||||
private class InGameKeyBindingsSubsection : KeyBindingsSubsection
|
||||
{
|
||||
protected override string Header => "In Game";
|
||||
protected override LocalisableString Header => "In Game";
|
||||
|
||||
public InGameKeyBindingsSubsection(GlobalActionContainer manager)
|
||||
: base(null)
|
||||
@ -61,7 +62,7 @@ namespace osu.Game.Overlays.KeyBinding
|
||||
|
||||
private class AudioControlKeyBindingsSubsection : KeyBindingsSubsection
|
||||
{
|
||||
protected override string Header => "Audio";
|
||||
protected override LocalisableString Header => "Audio";
|
||||
|
||||
public AudioControlKeyBindingsSubsection(GlobalActionContainer manager)
|
||||
: base(null)
|
||||
@ -72,7 +73,7 @@ namespace osu.Game.Overlays.KeyBinding
|
||||
|
||||
private class EditorKeyBindingsSubsection : KeyBindingsSubsection
|
||||
{
|
||||
protected override string Header => "Editor";
|
||||
protected override LocalisableString Header => "Editor";
|
||||
|
||||
public EditorKeyBindingsSubsection(GlobalActionContainer manager)
|
||||
: base(null)
|
||||
|
@ -1,13 +1,14 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Rulesets;
|
||||
|
||||
namespace osu.Game.Overlays.KeyBinding
|
||||
{
|
||||
public class VariantBindingsSubsection : KeyBindingsSubsection
|
||||
{
|
||||
protected override string Header { get; }
|
||||
protected override LocalisableString Header { get; }
|
||||
|
||||
public VariantBindingsSubsection(RulesetInfo ruleset, int variant)
|
||||
: base(variant)
|
||||
|
@ -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();
|
||||
|
@ -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
|
||||
},
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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]
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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; }
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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,
|
||||
};
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
},
|
||||
|
@ -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)
|
||||
{
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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";
|
||||
}
|
||||
}
|
||||
|
@ -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; }
|
||||
|
||||
|
@ -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";
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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.RankedAndApproved, User, UsersStrings.ShowExtraBeatmapsRankedTitle),
|
||||
new PaginatedBeatmapContainer(BeatmapSetType.Loved, User, UsersStrings.ShowExtraBeatmapsLovedTitle),
|
||||
new PaginatedBeatmapContainer(BeatmapSetType.Unranked, User, UsersStrings.ShowExtraBeatmapsPendingTitle),
|
||||
new PaginatedBeatmapContainer(BeatmapSetType.Graveyard, User, UsersStrings.ShowExtraBeatmapsGraveyardTitle)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
{
|
||||
}
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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)
|
||||
{
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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)
|
||||
{
|
||||
}
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user