1
0
mirror of https://github.com/ppy/osu.git synced 2025-03-25 18:57:18 +08:00

Merge branch 'master' into freestyle-mods

This commit is contained in:
Dean Herbert 2025-02-12 18:32:01 +09:00
commit bc94ffe21e
No known key found for this signature in database
119 changed files with 2922 additions and 1164 deletions

View File

@ -173,7 +173,7 @@ namespace osu.Desktop
new Button
{
Label = "View beatmap",
Url = $@"{api.WebsiteRootUrl}/beatmaps/{beatmapId}?mode={ruleset.Value.ShortName}"
Url = $@"{api.Endpoints.WebsiteUrl}/beatmaps/{beatmapId}?mode={ruleset.Value.ShortName}"
}
};
}

View File

@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Catch.Edit
//
// The implementation below is probably correct but should be checked if/when exposed via controls.
float expectedDistance = DurationToDistance(before, after.StartTime - before.GetEndTime());
float expectedDistance = DurationToDistance(after.StartTime - before.GetEndTime(), before.StartTime);
float previousEndX = (before as JuiceStream)?.EndX ?? ((CatchHitObject)before).EffectiveX;
float actualDistance = Math.Abs(previousEndX - ((CatchHitObject)after).EffectiveX);

View File

@ -3,6 +3,7 @@
using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
@ -12,6 +13,7 @@ using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI.Scrolling;
using osuTK;
using osuTK.Graphics;
@ -26,18 +28,22 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
[Resolved]
private OsuColour colours { get; set; } = null!;
private IBindable<ScrollingDirection> direction = null!;
public ArgonJudgementPiece(HitResult result)
: base(result)
{
AutoSizeAxes = Axes.Both;
Origin = Anchor.Centre;
Y = judgement_y_position;
}
[BackgroundDependencyLoader]
private void load()
private void load(IScrollingInfo scrollingInfo)
{
direction = scrollingInfo.Direction.GetBoundCopy();
direction.BindValueChanged(_ => onDirectionChanged(), true);
if (Result.IsHit())
{
AddInternal(ringExplosion = new RingExplosion(Result)
@ -47,6 +53,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
}
}
private void onDirectionChanged() => Y = direction.Value == ScrollingDirection.Up ? -judgement_y_position : judgement_y_position;
protected override SpriteText CreateJudgementText() =>
new OsuSpriteText
{
@ -78,7 +86,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
this.ScaleTo(1.6f);
this.ScaleTo(1, 100, Easing.In);
this.MoveToY(judgement_y_position);
this.MoveToY(direction.Value == ScrollingDirection.Up ? -judgement_y_position : judgement_y_position);
this.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint);
this.RotateTo(0);

View File

@ -2,12 +2,14 @@
// 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.Animations;
using osu.Framework.Graphics.Containers;
using osu.Framework.Utils;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Mania.Skinning.Legacy
@ -28,14 +30,16 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
AutoSizeAxes = Axes.Both;
}
[BackgroundDependencyLoader]
private void load(ISkinSource skin)
{
float hitPosition = skin.GetManiaSkinConfig<float>(LegacyManiaSkinConfigurationLookups.HitPosition)?.Value ?? 0;
float scorePosition = skin.GetManiaSkinConfig<float>(LegacyManiaSkinConfigurationLookups.ScorePosition)?.Value ?? 0;
private IBindable<ScrollingDirection> direction = null!;
float absoluteHitPosition = 480f * LegacyManiaSkinConfiguration.POSITION_SCALE_FACTOR - hitPosition;
Y = scorePosition - absoluteHitPosition;
[Resolved]
private ISkinSource skin { get; set; } = null!;
[BackgroundDependencyLoader]
private void load(IScrollingInfo scrollingInfo)
{
direction = scrollingInfo.Direction.GetBoundCopy();
direction.BindValueChanged(_ => onDirectionChanged(), true);
InternalChild = animation.With(d =>
{
@ -44,6 +48,17 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
});
}
private void onDirectionChanged()
{
float hitPosition = skin.GetManiaSkinConfig<float>(LegacyManiaSkinConfigurationLookups.HitPosition)?.Value ?? 0;
float scorePosition = skin.GetManiaSkinConfig<float>(LegacyManiaSkinConfigurationLookups.ScorePosition)?.Value ?? 0;
float absoluteHitPosition = 480f * LegacyManiaSkinConfiguration.POSITION_SCALE_FACTOR - hitPosition;
float finalPosition = scorePosition - absoluteHitPosition;
Y = direction.Value == ScrollingDirection.Up ? -finalPosition : finalPosition;
}
public void PlayAnimation()
{
(animation as IFramedAnimation)?.GotoFrame(0);

View File

@ -0,0 +1,75 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI.Scrolling;
using osuTK;
namespace osu.Game.Rulesets.Mania.UI
{
public partial class DefaultManiaJudgementPiece : DefaultJudgementPiece
{
private const float judgement_y_position = -180f;
private IBindable<ScrollingDirection> direction = null!;
public DefaultManiaJudgementPiece(HitResult result)
: base(result)
{
}
[BackgroundDependencyLoader]
private void load(IScrollingInfo scrollingInfo)
{
direction = scrollingInfo.Direction.GetBoundCopy();
direction.BindValueChanged(_ => onDirectionChanged(), true);
}
private void onDirectionChanged() => Y = direction.Value == ScrollingDirection.Up ? -judgement_y_position : judgement_y_position;
protected override void LoadComplete()
{
base.LoadComplete();
JudgementText.Font = JudgementText.Font.With(size: 25);
}
public override void PlayAnimation()
{
switch (Result)
{
case HitResult.None:
this.FadeOutFromOne(800);
break;
case HitResult.Miss:
this.ScaleTo(1.6f);
this.ScaleTo(1, 100, Easing.In);
this.MoveToY(direction.Value == ScrollingDirection.Up ? -judgement_y_position : judgement_y_position);
this.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint);
this.RotateTo(0);
this.RotateTo(40, 800, Easing.InQuint);
this.FadeOutFromOne(800);
break;
default:
this.ScaleTo(0.8f);
this.ScaleTo(1, 250, Easing.OutElastic);
this.Delay(50)
.ScaleTo(0.75f, 250)
.FadeOut(200);
// osu!mania uses a custom fade length, so the base call is intentionally omitted.
break;
}
}
}
}

View File

@ -3,65 +3,32 @@
#nullable disable
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
using osuTK;
using osu.Game.Rulesets.UI.Scrolling;
namespace osu.Game.Rulesets.Mania.UI
{
public partial class DrawableManiaJudgement : DrawableJudgement
{
protected override Drawable CreateDefaultJudgement(HitResult result) => new DefaultManiaJudgementPiece(result);
private IBindable<ScrollingDirection> direction;
private partial class DefaultManiaJudgementPiece : DefaultJudgementPiece
[BackgroundDependencyLoader]
private void load(IScrollingInfo scrollingInfo)
{
private const float judgement_y_position = -180f;
public DefaultManiaJudgementPiece(HitResult result)
: base(result)
{
Y = judgement_y_position;
}
protected override void LoadComplete()
{
base.LoadComplete();
JudgementText.Font = JudgementText.Font.With(size: 25);
}
public override void PlayAnimation()
{
switch (Result)
{
case HitResult.None:
this.FadeOutFromOne(800);
break;
case HitResult.Miss:
this.ScaleTo(1.6f);
this.ScaleTo(1, 100, Easing.In);
this.MoveToY(judgement_y_position);
this.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint);
this.RotateTo(0);
this.RotateTo(40, 800, Easing.InQuint);
this.FadeOutFromOne(800);
break;
default:
this.ScaleTo(0.8f);
this.ScaleTo(1, 250, Easing.OutElastic);
this.Delay(50)
.ScaleTo(0.75f, 250)
.FadeOut(200);
break;
}
}
direction = scrollingInfo.Direction.GetBoundCopy();
direction.BindValueChanged(_ => onDirectionChanged(), true);
}
private void onDirectionChanged()
{
Anchor = direction.Value == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre;
Origin = Anchor.Centre;
}
protected override Drawable CreateDefaultJudgement(HitResult result) => new DefaultManiaJudgementPiece(result);
}
}

View File

@ -216,13 +216,7 @@ namespace osu.Game.Rulesets.Mania.UI
return;
judgements.Clear(false);
judgements.Add(judgementPooler.Get(result.Type, j =>
{
j.Apply(result, judgedObject);
j.Anchor = Anchor.BottomCentre;
j.Origin = Anchor.Centre;
})!);
judgements.Add(judgementPooler.Get(result.Type, j => j.Apply(result, judgedObject))!);
}
protected override void Update()

View File

@ -34,7 +34,7 @@ using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{
public partial class PathControlPointVisualiser<T> : CompositeDrawable, IKeyBindingHandler<PlatformAction>, IHasContextMenu
where T : OsuHitObject, IHasPath
where T : OsuHitObject, IHasPath, IHasSliderVelocity
{
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; // allow context menu to appear outside the playfield.

View File

@ -434,7 +434,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
if (state == SliderPlacementState.Drawing)
HitObject.Path.ExpectedDistance.Value = (float)HitObject.Path.CalculatedDistance;
else
HitObject.Path.ExpectedDistance.Value = distanceSnapProvider?.FindSnappedDistance(HitObject, (float)HitObject.Path.CalculatedDistance, DistanceSnapTarget.Start) ?? (float)HitObject.Path.CalculatedDistance;
HitObject.Path.ExpectedDistance.Value = distanceSnapProvider?.FindSnappedDistance((float)HitObject.Path.CalculatedDistance, HitObject.StartTime, HitObject) ?? (float)HitObject.Path.CalculatedDistance;
bodyPiece.UpdateFrom(HitObject);
headCirclePiece.UpdateFrom(HitObject.HeadCircle);

View File

@ -274,9 +274,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
}
else
{
double minDistance = distanceSnapProvider?.GetBeatSnapDistanceAt(HitObject, false) * oldVelocityMultiplier ?? 1;
double minDistance = distanceSnapProvider?.GetBeatSnapDistance() * oldVelocityMultiplier ?? 1;
// Add a small amount to the proposed distance to make it easier to snap to the full length of the slider.
proposedDistance = distanceSnapProvider?.FindSnappedDistance(HitObject, (float)proposedDistance + 1, DistanceSnapTarget.Start) ?? proposedDistance;
proposedDistance = distanceSnapProvider?.FindSnappedDistance((float)proposedDistance + 1, HitObject.StartTime, HitObject) ?? proposedDistance;
proposedDistance = MathHelper.Clamp(proposedDistance, minDistance, HitObject.Path.CalculatedDistance);
}

View File

@ -5,6 +5,7 @@
using JetBrains.Annotations;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit.Compose.Components;
@ -12,8 +13,8 @@ namespace osu.Game.Rulesets.Osu.Edit
{
public partial class OsuDistanceSnapGrid : CircularDistanceSnapGrid
{
public OsuDistanceSnapGrid(OsuHitObject hitObject, [CanBeNull] OsuHitObject nextHitObject = null)
: base(hitObject, hitObject.StackedEndPosition, hitObject.GetEndTime(), nextHitObject?.StartTime - 1)
public OsuDistanceSnapGrid(OsuHitObject hitObject, [CanBeNull] OsuHitObject nextHitObject = null, [CanBeNull] IHasSliderVelocity sliderVelocitySource = null)
: base(hitObject.StackedEndPosition, hitObject.GetEndTime(), nextHitObject?.StartTime - 1, sliderVelocitySource)
{
Masking = true;
}

View File

@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Osu.Edit
{
public override double ReadCurrentDistanceSnap(HitObject before, HitObject after)
{
float expectedDistance = DurationToDistance(before, after.StartTime - before.GetEndTime());
float expectedDistance = DurationToDistance(after.StartTime - before.GetEndTime(), before.StartTime);
float actualDistance = Vector2.Distance(((OsuHitObject)before).EndPosition, ((OsuHitObject)after).Position);
return actualDistance / expectedDistance;

View File

@ -23,6 +23,7 @@ using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.UI;
@ -406,22 +407,26 @@ namespace osu.Game.Rulesets.Osu.Edit
{
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(targetOffset);
int sourceIndex = -1;
int positionSourceObjectIndex = -1;
IHasSliderVelocity sliderVelocitySource = null;
for (int i = 0; i < EditorBeatmap.HitObjects.Count; i++)
{
if (!sourceSelector(EditorBeatmap.HitObjects[i]))
break;
sourceIndex = i;
positionSourceObjectIndex = i;
if (EditorBeatmap.HitObjects[i] is IHasSliderVelocity hasSliderVelocity)
sliderVelocitySource = hasSliderVelocity;
}
if (sourceIndex == -1)
if (positionSourceObjectIndex == -1)
return null;
HitObject sourceObject = EditorBeatmap.HitObjects[sourceIndex];
HitObject sourceObject = EditorBeatmap.HitObjects[positionSourceObjectIndex];
int targetIndex = sourceIndex + targetOffset;
int targetIndex = positionSourceObjectIndex + targetOffset;
HitObject targetObject = null;
// Keep advancing the target object while its start time falls before the end time of the source object
@ -442,7 +447,7 @@ namespace osu.Game.Rulesets.Osu.Edit
if (sourceObject is Spinner)
return null;
return new OsuDistanceSnapGrid((OsuHitObject)sourceObject, (OsuHitObject)targetObject);
return new OsuDistanceSnapGrid((OsuHitObject)sourceObject, (OsuHitObject)targetObject, sliderVelocitySource);
}
}
}

View File

@ -66,8 +66,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Slider slider = drawableSlider.HitObject;
Position = slider.CurvePositionAt(completionProgress);
//0.1 / slider.Path.Distance is the additional progress needed to ensure the diff length is 0.1
var diff = slider.CurvePositionAt(completionProgress) - slider.CurvePositionAt(Math.Min(1, completionProgress + 0.1 / slider.Path.Distance));
// 0.1 / slider.Path.Distance is the additional progress needed to ensure the diff length is 0.1
double checkDistance = 0.1 / slider.Path.Distance;
var diff = slider.CurvePositionAt(Math.Min(1 - checkDistance, completionProgress)) - slider.CurvePositionAt(Math.Min(1, completionProgress + checkDistance));
// Ensure the value is substantially high enough to allow for Atan2 to get a valid angle.
// Needed for when near completion, or in case of a very short slider.

View File

@ -12,6 +12,7 @@ using osu.Framework.Utils;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Edit;
@ -67,17 +68,7 @@ namespace osu.Game.Tests.Editing
{
AddStep($"set slider multiplier = {multiplier}", () => composer.EditorBeatmap.Difficulty.SliderMultiplier = multiplier);
assertSnapDistance(100 * multiplier, null, true);
}
[TestCase(1)]
[TestCase(2)]
public void TestSpeedMultiplierDoesNotChangeDistanceSnap(float multiplier)
{
assertSnapDistance(100, new Slider
{
SliderVelocityMultiplier = multiplier
}, false);
assertSnapDistance(100 * multiplier);
}
[TestCase(1)]
@ -87,7 +78,7 @@ namespace osu.Game.Tests.Editing
assertSnapDistance(100 * multiplier, new Slider
{
SliderVelocityMultiplier = multiplier
}, true);
});
}
[TestCase(1)]
@ -96,7 +87,7 @@ namespace osu.Game.Tests.Editing
{
AddStep($"set divisor = {divisor}", () => BeatDivisor.Value = divisor);
assertSnapDistance(100f / divisor, null, true);
assertSnapDistance(100f / divisor);
}
/// <summary>
@ -114,9 +105,8 @@ namespace osu.Game.Tests.Editing
};
AddStep("add to beatmap", () => composer.EditorBeatmap.Add(referenceObject));
assertSnapDistance(base_distance * slider_velocity, referenceObject, true);
assertSnapDistance(base_distance * slider_velocity, referenceObject);
assertSnappedDistance(base_distance * slider_velocity + 10, base_distance * slider_velocity, referenceObject);
assertSnappedDuration(base_distance * slider_velocity + 10, 1000, referenceObject);
assertDistanceToDuration(base_distance * slider_velocity, 1000, referenceObject);
assertDurationToDistance(1000, base_distance * slider_velocity, referenceObject);
@ -164,39 +154,6 @@ namespace osu.Game.Tests.Editing
assertDistanceToDuration(400, 1000);
}
[Test]
public void TestGetSnappedDurationFromDistance()
{
assertSnappedDuration(0, 0);
assertSnappedDuration(50, 1000);
assertSnappedDuration(100, 1000);
assertSnappedDuration(150, 2000);
assertSnappedDuration(200, 2000);
assertSnappedDuration(250, 3000);
AddStep("set slider multiplier = 2", () => composer.EditorBeatmap.Difficulty.SliderMultiplier = 2);
assertSnappedDuration(0, 0);
assertSnappedDuration(50, 0);
assertSnappedDuration(100, 1000);
assertSnappedDuration(150, 1000);
assertSnappedDuration(200, 1000);
assertSnappedDuration(250, 1000);
AddStep("set beat length = 500", () =>
{
composer.EditorBeatmap.ControlPointInfo.Clear();
composer.EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 500 });
});
assertSnappedDuration(50, 0);
assertSnappedDuration(100, 500);
assertSnappedDuration(150, 500);
assertSnappedDuration(200, 500);
assertSnappedDuration(250, 500);
assertSnappedDuration(400, 1000);
}
[Test]
public void GetSnappedDistanceFromDistance()
{
@ -289,20 +246,17 @@ namespace osu.Game.Tests.Editing
AddUntilStep("use current snap not available", () => getCurrentSnapButton().Enabled.Value, () => Is.False);
}
private void assertSnapDistance(float expectedDistance, HitObject? referenceObject, bool includeSliderVelocity)
=> AddAssert($"distance is {expectedDistance}", () => composer.DistanceSnapProvider.GetBeatSnapDistanceAt(referenceObject ?? new HitObject(), includeSliderVelocity), () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON));
private void assertSnapDistance(float expectedDistance, IHasSliderVelocity? hasSliderVelocity = null)
=> AddAssert($"distance is {expectedDistance}", () => composer.DistanceSnapProvider.GetBeatSnapDistance(hasSliderVelocity), () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON));
private void assertDurationToDistance(double duration, float expectedDistance, HitObject? referenceObject = null)
=> AddAssert($"duration = {duration} -> distance = {expectedDistance}", () => composer.DistanceSnapProvider.DurationToDistance(referenceObject ?? new HitObject(), duration), () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON));
=> AddAssert($"duration = {duration} -> distance = {expectedDistance}", () => composer.DistanceSnapProvider.DurationToDistance(duration, referenceObject?.StartTime ?? 0, referenceObject as IHasSliderVelocity), () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON));
private void assertDistanceToDuration(float distance, double expectedDuration, HitObject? referenceObject = null)
=> AddAssert($"distance = {distance} -> duration = {expectedDuration}", () => composer.DistanceSnapProvider.DistanceToDuration(referenceObject ?? new HitObject(), distance), () => Is.EqualTo(expectedDuration).Within(Precision.FLOAT_EPSILON));
private void assertSnappedDuration(float distance, double expectedDuration, HitObject? referenceObject = null)
=> AddAssert($"distance = {distance} -> duration = {expectedDuration} (snapped)", () => composer.DistanceSnapProvider.FindSnappedDuration(referenceObject ?? new HitObject(), distance), () => Is.EqualTo(expectedDuration).Within(Precision.FLOAT_EPSILON));
=> AddAssert($"distance = {distance} -> duration = {expectedDuration}", () => composer.DistanceSnapProvider.DistanceToDuration(distance, referenceObject?.StartTime ?? 0, referenceObject as IHasSliderVelocity), () => Is.EqualTo(expectedDuration).Within(Precision.FLOAT_EPSILON));
private void assertSnappedDistance(float distance, float expectedDistance, HitObject? referenceObject = null)
=> AddAssert($"distance = {distance} -> distance = {expectedDistance} (snapped)", () => composer.DistanceSnapProvider.FindSnappedDistance(referenceObject ?? new HitObject(), distance, DistanceSnapTarget.End), () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON));
=> AddAssert($"distance = {distance} -> distance = {expectedDistance} (snapped)", () => composer.DistanceSnapProvider.FindSnappedDistance(distance, referenceObject?.GetEndTime() ?? 0, referenceObject as IHasSliderVelocity), () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON));
private partial class TestHitObjectComposer : OsuHitObjectComposer
{

View File

@ -0,0 +1,52 @@
// 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 NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Tests.Visual;
using osu.Game.Tests.Visual.Metadata;
namespace osu.Game.Tests.Online
{
[TestFixture]
[HeadlessTest]
public partial class TestSceneMetadataClient : OsuTestScene
{
private TestMetadataClient client = null!;
[SetUp]
public void Setup() => Schedule(() =>
{
Child = client = new TestMetadataClient();
});
[Test]
public void TestWatchingMultipleTimesInvokesServerMethodsOnce()
{
int countBegin = 0;
int countEnd = 0;
IDisposable token1 = null!;
IDisposable token2 = null!;
AddStep("setup", () =>
{
client.OnBeginWatchingUserPresence += () => countBegin++;
client.OnEndWatchingUserPresence += () => countEnd++;
});
AddStep("begin watching presence (1)", () => token1 = client.BeginWatchingUserPresence());
AddAssert("server method invoked once", () => countBegin, () => Is.EqualTo(1));
AddStep("begin watching presence (2)", () => token2 = client.BeginWatchingUserPresence());
AddAssert("server method not invoked a second time", () => countBegin, () => Is.EqualTo(1));
AddStep("end watching presence (1)", () => token1.Dispose());
AddAssert("server method not invoked", () => countEnd, () => Is.EqualTo(0));
AddStep("end watching presence (2)", () => token2.Dispose());
AddAssert("server method invoked once", () => countEnd, () => Is.EqualTo(1));
}
}
}

View File

@ -0,0 +1,41 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Screens.Edit.Submission;
using osu.Game.Screens.Footer;
namespace osu.Game.Tests.Visual.Editing
{
public partial class TestSceneBeatmapSubmissionOverlay : OsuTestScene
{
private ScreenFooter footer = null!;
[SetUpSteps]
public void SetUpSteps()
{
AddStep("add overlay", () =>
{
var receptor = new ScreenFooter.BackReceptor();
footer = new ScreenFooter(receptor);
Child = new DependencyProvidingContainer
{
RelativeSizeAxes = Axes.Both,
CachedDependencies = new[] { (typeof(ScreenFooter), (object)footer) },
Children = new Drawable[]
{
receptor,
new BeatmapSubmissionOverlay
{
State = { Value = Visibility.Visible, },
},
footer,
}
};
});
}
}
}

View File

@ -10,7 +10,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Screens.Edit;
@ -115,7 +115,7 @@ namespace osu.Game.Tests.Visual.Editing
public new int MaxIntervals => base.MaxIntervals;
public TestDistanceSnapGrid(double? endTime = null)
: base(new HitObject(), grid_position, 0, endTime)
: base(grid_position, 0, endTime)
{
}
@ -191,15 +191,13 @@ namespace osu.Game.Tests.Visual.Editing
Bindable<double> IDistanceSnapProvider.DistanceSpacingMultiplier => DistanceSpacingMultiplier;
public float GetBeatSnapDistanceAt(HitObject referenceObject, bool useReferenceSliderVelocity = true) => beat_snap_distance;
public float GetBeatSnapDistance(IHasSliderVelocity withVelocity = null) => beat_snap_distance;
public float DurationToDistance(HitObject referenceObject, double duration) => (float)duration;
public float DurationToDistance(double duration, double timingReference, IHasSliderVelocity withVelocity = null) => (float)duration;
public double DistanceToDuration(HitObject referenceObject, float distance) => distance;
public double DistanceToDuration(float distance, double timingReference, IHasSliderVelocity withVelocity = null) => distance;
public double FindSnappedDuration(HitObject referenceObject, float distance) => 0;
public float FindSnappedDistance(HitObject referenceObject, float distance, DistanceSnapTarget target) => 0;
public float FindSnappedDistance(float distance, double snapReferenceTime, IHasSliderVelocity withVelocity = null) => 0;
}
}
}

View File

@ -74,7 +74,6 @@ namespace osu.Game.Tests.Visual.Editing
}
[Test]
[Solo]
public void TestCommitPlacementViaRightClick()
{
Playfield playfield = null!;

View File

@ -0,0 +1,47 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Overlays;
using osu.Game.Screens.Edit.Submission;
using osuTK;
namespace osu.Game.Tests.Visual.Editing
{
public partial class TestSceneSubmissionStageProgress : OsuTestScene
{
[Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
[Test]
public void TestAppearance()
{
SubmissionStageProgress progress = null!;
AddStep("create content", () => Child = new Container
{
RelativeSizeAxes = Axes.Both,
Size = new Vector2(0.8f),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Child = progress = new SubmissionStageProgress
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
StageDescription = "Frobnicating the foobarator...",
}
});
AddStep("not started", () => progress.SetNotStarted());
AddStep("indeterminate progress", () => progress.SetInProgress());
AddStep("30% progress", () => progress.SetInProgress(0.3f));
AddStep("70% progress", () => progress.SetInProgress(0.7f));
AddStep("completed", () => progress.SetCompleted());
AddStep("failed", () => progress.SetFailed("the foobarator has defrobnicated"));
AddStep("failed with long message", () => progress.SetFailed("this is a very very very very VERY VEEEEEEEEEEEEEEEEEEEEEEEEERY long error message like you would never believe"));
AddStep("canceled", () => progress.SetCanceled());
}
}
}

View File

@ -16,6 +16,7 @@ using osu.Game.Online.API;
using osu.Game.Online.Rooms;
using osu.Game.Online.Solo;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
@ -234,6 +235,31 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("ensure no submission", () => Player.SubmittedScore == null);
}
[Test]
public void TestNoSubmissionWhenScoreZero()
{
prepareTestAPI(true);
createPlayerTest();
AddUntilStep("wait for token request", () => Player.TokenCreationRequested);
AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
AddUntilStep("wait for first result", () => Player.Results.Count > 0);
AddStep("add fake non-scoring hit", () =>
{
Player.ScoreProcessor.RevertResult(Player.Results.First());
Player.ScoreProcessor.ApplyResult(new OsuJudgementResult(Beatmap.Value.Beatmap.HitObjects.First(), new IgnoreJudgement())
{
Type = HitResult.IgnoreHit,
});
});
AddStep("exit", () => Player.Exit());
AddAssert("ensure no submission", () => Player.SubmittedScore == null);
}
[Test]
public void TestSubmissionOnExit()
{

View File

@ -8,11 +8,14 @@ using osu.Framework.Graphics;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Spectator;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Screens.Play.HUD;
using osu.Game.Tests.Visual.Multiplayer;
using osu.Game.Tests.Visual.OnlinePlay;
using osu.Game.Tests.Visual.Spectator;
namespace osu.Game.Tests.Visual.Gameplay
@ -28,20 +31,23 @@ namespace osu.Game.Tests.Visual.Gameplay
SpectatorList list = null!;
Bindable<LocalUserPlayingState> playingState = new Bindable<LocalUserPlayingState>();
GameplayState gameplayState = new GameplayState(new Beatmap(), new OsuRuleset(), healthProcessor: new OsuHealthProcessor(0), localUserPlayingState: playingState);
TestSpectatorClient client = new TestSpectatorClient();
TestSpectatorClient spectatorClient = new TestSpectatorClient();
TestMultiplayerClient multiplayerClient = new TestMultiplayerClient(new TestMultiplayerRoomManager(new TestRoomRequestsHandler()));
AddStep("create spectator list", () =>
{
Children = new Drawable[]
{
client,
spectatorClient,
multiplayerClient,
new DependencyProvidingContainer
{
RelativeSizeAxes = Axes.Both,
CachedDependencies =
[
(typeof(GameplayState), gameplayState),
(typeof(SpectatorClient), client)
(typeof(SpectatorClient), spectatorClient),
(typeof(MultiplayerClient), multiplayerClient),
],
Child = list = new SpectatorList
{
@ -57,7 +63,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddRepeatStep("add a user", () =>
{
int id = Interlocked.Increment(ref counter);
((ISpectatorClient)client).UserStartedWatching([
((ISpectatorClient)spectatorClient).UserStartedWatching([
new SpectatorUser
{
OnlineID = id,
@ -66,7 +72,8 @@ namespace osu.Game.Tests.Visual.Gameplay
]);
}, 10);
AddRepeatStep("remove random user", () => ((ISpectatorClient)client).UserEndedWatching(client.WatchingUsers[RNG.Next(client.WatchingUsers.Count)].OnlineID), 5);
AddRepeatStep("remove random user", () => ((ISpectatorClient)spectatorClient).UserEndedWatching(
spectatorClient.WatchingUsers[RNG.Next(spectatorClient.WatchingUsers.Count)].OnlineID), 5);
AddStep("change font to venera", () => list.Font.Value = Typeface.Venera);
AddStep("change font to torus", () => list.Font.Value = Typeface.Torus);

View File

@ -80,7 +80,7 @@ namespace osu.Game.Tests.Visual.Menus
new APIMenuImage
{
Image = @"https://assets.ppy.sh/main-menu/project-loved-2@2x.png",
Url = $@"{API.WebsiteRootUrl}/home/news/2023-12-21-project-loved-december-2023",
Url = $@"{API.Endpoints.WebsiteUrl}/home/news/2023-12-21-project-loved-december-2023",
}
}
});

View File

@ -165,7 +165,6 @@ namespace osu.Game.Tests.Visual.Navigation
}
[Test]
[Solo]
public void TestEditorGameplayTestAlwaysUsesOriginalRuleset()
{
prepareBeatmap();

View File

@ -65,7 +65,9 @@ namespace osu.Game.Tests.Visual.Online
[Test]
public void TestBasicDisplay()
{
AddStep("Begin watching user presence", () => metadataClient.BeginWatchingUserPresence());
IDisposable token = null!;
AddStep("Begin watching user presence", () => token = metadataClient.BeginWatchingUserPresence());
AddStep("Add online user", () => metadataClient.UserPresenceUpdated(streamingUser.Id, new UserPresence { Status = UserStatus.Online, Activity = new UserActivity.ChoosingBeatmap() }));
AddUntilStep("Panel loaded", () => currentlyOnline.ChildrenOfType<UserGridPanel>().FirstOrDefault()?.User.Id == 2);
AddAssert("Spectate button disabled", () => currentlyOnline.ChildrenOfType<PurpleRoundedButton>().First().Enabled.Value, () => Is.False);
@ -78,14 +80,16 @@ namespace osu.Game.Tests.Visual.Online
AddStep("Remove playing user", () => metadataClient.UserPresenceUpdated(streamingUser.Id, null));
AddUntilStep("Panel no longer present", () => !currentlyOnline.ChildrenOfType<UserGridPanel>().Any());
AddStep("End watching user presence", () => metadataClient.EndWatchingUserPresence());
AddStep("End watching user presence", () => token.Dispose());
}
[Test]
public void TestUserWasPlayingBeforeWatchingUserPresence()
{
IDisposable token = null!;
AddStep("User began playing", () => spectatorClient.SendStartPlay(streamingUser.Id, 0));
AddStep("Begin watching user presence", () => metadataClient.BeginWatchingUserPresence());
AddStep("Begin watching user presence", () => token = metadataClient.BeginWatchingUserPresence());
AddStep("Add online user", () => metadataClient.UserPresenceUpdated(streamingUser.Id, new UserPresence { Status = UserStatus.Online, Activity = new UserActivity.ChoosingBeatmap() }));
AddUntilStep("Panel loaded", () => currentlyOnline.ChildrenOfType<UserGridPanel>().FirstOrDefault()?.User.Id == 2);
AddAssert("Spectate button enabled", () => currentlyOnline.ChildrenOfType<PurpleRoundedButton>().First().Enabled.Value, () => Is.True);
@ -93,7 +97,7 @@ namespace osu.Game.Tests.Visual.Online
AddStep("User finished playing", () => spectatorClient.SendEndPlay(streamingUser.Id));
AddAssert("Spectate button disabled", () => currentlyOnline.ChildrenOfType<PurpleRoundedButton>().First().Enabled.Value, () => Is.False);
AddStep("Remove playing user", () => metadataClient.UserPresenceUpdated(streamingUser.Id, null));
AddStep("End watching user presence", () => metadataClient.EndWatchingUserPresence());
AddStep("End watching user presence", () => token.Dispose());
}
internal partial class TestUserLookupCache : UserLookupCache

View File

@ -4,17 +4,18 @@
using System;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Online;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Metadata;
using osu.Game.Overlays;
using osu.Game.Rulesets;
using osu.Game.Scoring;
using osu.Game.Tests.Beatmaps;
using osu.Game.Tests.Visual.Metadata;
using osu.Game.Users;
using osuTK;
@ -23,144 +24,142 @@ namespace osu.Game.Tests.Visual.Online
[TestFixture]
public partial class TestSceneUserPanel : OsuTestScene
{
private readonly Bindable<UserActivity?> activity = new Bindable<UserActivity?>();
private readonly Bindable<UserStatus?> status = new Bindable<UserStatus?>();
private UserGridPanel boundPanel1 = null!;
private TestUserListPanel boundPanel2 = null!;
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple);
[Cached(typeof(LocalUserStatisticsProvider))]
private readonly TestUserStatisticsProvider statisticsProvider = new TestUserStatisticsProvider();
[Resolved]
private IRulesetStore rulesetStore { get; set; } = null!;
private TestUserStatisticsProvider statisticsProvider = null!;
private TestMetadataClient metadataClient = null!;
private TestUserListPanel panel = null!;
[SetUp]
public void SetUp() => Schedule(() =>
{
activity.Value = null;
status.Value = null;
Remove(statisticsProvider, false);
Clear();
Add(statisticsProvider);
Add(new FillFlowContainer
Child = new DependencyProvidingContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
Spacing = new Vector2(10f),
RelativeSizeAxes = Axes.Both,
CachedDependencies =
[
(typeof(LocalUserStatisticsProvider), statisticsProvider = new TestUserStatisticsProvider()),
(typeof(MetadataClient), metadataClient = new TestMetadataClient())
],
Children = new Drawable[]
{
new UserBrickPanel(new APIUser
statisticsProvider,
metadataClient,
new FillFlowContainer
{
Username = @"flyte",
Id = 3103765,
CoverUrl = @"https://assets.ppy.sh/user-cover-presets/1/df28696b58541a9e67f6755918951d542d93bdf1da41720fcca2fd2c1ea8cf51.jpeg",
}),
new UserBrickPanel(new APIUser
{
Username = @"peppy",
Id = 2,
Colour = "99EB47",
CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg",
}),
new UserGridPanel(new APIUser
{
Username = @"flyte",
Id = 3103765,
CountryCode = CountryCode.JP,
CoverUrl = @"https://assets.ppy.sh/user-cover-presets/1/df28696b58541a9e67f6755918951d542d93bdf1da41720fcca2fd2c1ea8cf51.jpeg",
IsOnline = true
}) { Width = 300 },
boundPanel1 = new UserGridPanel(new APIUser
{
Username = @"peppy",
Id = 2,
CountryCode = CountryCode.AU,
CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg",
IsSupporter = true,
SupportLevel = 3,
}) { Width = 300 },
boundPanel2 = new TestUserListPanel(new APIUser
{
Username = @"Evast",
Id = 8195163,
CountryCode = CountryCode.BY,
CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg",
IsOnline = false,
LastVisit = DateTimeOffset.Now
}),
new UserRankPanel(new APIUser
{
Username = @"flyte",
Id = 3103765,
CountryCode = CountryCode.JP,
CoverUrl = @"https://assets.ppy.sh/user-cover-presets/1/df28696b58541a9e67f6755918951d542d93bdf1da41720fcca2fd2c1ea8cf51.jpeg",
Statistics = new UserStatistics { GlobalRank = 12345, CountryRank = 1234 }
}) { Width = 300 },
new UserRankPanel(new APIUser
{
Username = @"peppy",
Id = 2,
Colour = "99EB47",
CountryCode = CountryCode.AU,
CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg",
Statistics = new UserStatistics { GlobalRank = null, CountryRank = null }
}) { Width = 300 }
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
Spacing = new Vector2(10f),
Children = new Drawable[]
{
new UserBrickPanel(new APIUser
{
Username = @"flyte",
Id = 3103765,
CoverUrl = @"https://assets.ppy.sh/user-cover-presets/1/df28696b58541a9e67f6755918951d542d93bdf1da41720fcca2fd2c1ea8cf51.jpeg",
}),
new UserBrickPanel(new APIUser
{
Username = @"peppy",
Id = 2,
Colour = "99EB47",
CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg",
}),
new UserGridPanel(new APIUser
{
Username = @"flyte",
Id = 3103765,
CountryCode = CountryCode.JP,
CoverUrl = @"https://assets.ppy.sh/user-cover-presets/1/df28696b58541a9e67f6755918951d542d93bdf1da41720fcca2fd2c1ea8cf51.jpeg",
IsOnline = true
}) { Width = 300 },
new UserGridPanel(new APIUser
{
Username = @"peppy",
Id = 2,
CountryCode = CountryCode.AU,
CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg",
IsSupporter = true,
SupportLevel = 3,
}) { Width = 300 },
panel = new TestUserListPanel(new APIUser
{
Username = @"peppy",
Id = 2,
CountryCode = CountryCode.AU,
CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg",
LastVisit = DateTimeOffset.Now
}),
new UserRankPanel(new APIUser
{
Username = @"flyte",
Id = 3103765,
CountryCode = CountryCode.JP,
CoverUrl = @"https://assets.ppy.sh/user-cover-presets/1/df28696b58541a9e67f6755918951d542d93bdf1da41720fcca2fd2c1ea8cf51.jpeg",
Statistics = new UserStatistics { GlobalRank = 12345, CountryRank = 1234 }
}) { Width = 300 },
new UserRankPanel(new APIUser
{
Username = @"peppy",
Id = 2,
Colour = "99EB47",
CountryCode = CountryCode.AU,
CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg",
Statistics = new UserStatistics { GlobalRank = null, CountryRank = null }
}) { Width = 300 },
new UserGridPanel(API.LocalUser.Value)
{
Width = 300
}
}
}
}
});
};
boundPanel1.Status.BindTo(status);
boundPanel1.Activity.BindTo(activity);
boundPanel2.Status.BindTo(status);
boundPanel2.Activity.BindTo(activity);
metadataClient.BeginWatchingUserPresence();
});
[Test]
public void TestUserStatus()
{
AddStep("online", () => status.Value = UserStatus.Online);
AddStep("do not disturb", () => status.Value = UserStatus.DoNotDisturb);
AddStep("offline", () => status.Value = UserStatus.Offline);
AddStep("null status", () => status.Value = null);
AddStep("online", () => setPresence(UserStatus.Online, null));
AddStep("do not disturb", () => setPresence(UserStatus.DoNotDisturb, null));
AddStep("offline", () => setPresence(UserStatus.Offline, null));
}
[Test]
public void TestUserActivity()
{
AddStep("set online status", () => status.Value = UserStatus.Online);
AddStep("idle", () => activity.Value = null);
AddStep("watching replay", () => activity.Value = new UserActivity.WatchingReplay(createScore(@"nats")));
AddStep("spectating user", () => activity.Value = new UserActivity.SpectatingUser(createScore(@"mrekk")));
AddStep("solo (osu!)", () => activity.Value = soloGameStatusForRuleset(0));
AddStep("solo (osu!taiko)", () => activity.Value = soloGameStatusForRuleset(1));
AddStep("solo (osu!catch)", () => activity.Value = soloGameStatusForRuleset(2));
AddStep("solo (osu!mania)", () => activity.Value = soloGameStatusForRuleset(3));
AddStep("choosing", () => activity.Value = new UserActivity.ChoosingBeatmap());
AddStep("editing beatmap", () => activity.Value = new UserActivity.EditingBeatmap(new BeatmapInfo()));
AddStep("modding beatmap", () => activity.Value = new UserActivity.ModdingBeatmap(new BeatmapInfo()));
AddStep("testing beatmap", () => activity.Value = new UserActivity.TestingBeatmap(new BeatmapInfo()));
AddStep("idle", () => setPresence(UserStatus.Online, null));
AddStep("watching replay", () => setPresence(UserStatus.Online, new UserActivity.WatchingReplay(createScore(@"nats"))));
AddStep("spectating user", () => setPresence(UserStatus.Online, new UserActivity.SpectatingUser(createScore(@"mrekk"))));
AddStep("solo (osu!)", () => setPresence(UserStatus.Online, soloGameStatusForRuleset(0)));
AddStep("solo (osu!taiko)", () => setPresence(UserStatus.Online, soloGameStatusForRuleset(1)));
AddStep("solo (osu!catch)", () => setPresence(UserStatus.Online, soloGameStatusForRuleset(2)));
AddStep("solo (osu!mania)", () => setPresence(UserStatus.Online, soloGameStatusForRuleset(3)));
AddStep("choosing", () => setPresence(UserStatus.Online, new UserActivity.ChoosingBeatmap()));
AddStep("editing beatmap", () => setPresence(UserStatus.Online, new UserActivity.EditingBeatmap(new BeatmapInfo())));
AddStep("modding beatmap", () => setPresence(UserStatus.Online, new UserActivity.ModdingBeatmap(new BeatmapInfo())));
AddStep("testing beatmap", () => setPresence(UserStatus.Online, new UserActivity.TestingBeatmap(new BeatmapInfo())));
}
[Test]
public void TestUserActivityChange()
{
AddAssert("visit message is visible", () => boundPanel2.LastVisitMessage.IsPresent);
AddStep("set online status", () => status.Value = UserStatus.Online);
AddAssert("visit message is not visible", () => !boundPanel2.LastVisitMessage.IsPresent);
AddStep("set choosing activity", () => activity.Value = new UserActivity.ChoosingBeatmap());
AddStep("set offline status", () => status.Value = UserStatus.Offline);
AddAssert("visit message is visible", () => boundPanel2.LastVisitMessage.IsPresent);
AddStep("set online status", () => status.Value = UserStatus.Online);
AddAssert("visit message is not visible", () => !boundPanel2.LastVisitMessage.IsPresent);
AddAssert("visit message is visible", () => panel.LastVisitMessage.IsPresent);
AddStep("set online status", () => setPresence(UserStatus.Online, null));
AddAssert("visit message is not visible", () => !panel.LastVisitMessage.IsPresent);
AddStep("set choosing activity", () => setPresence(UserStatus.Online, new UserActivity.ChoosingBeatmap()));
AddStep("set offline status", () => setPresence(UserStatus.Offline, null));
AddAssert("visit message is visible", () => panel.LastVisitMessage.IsPresent);
AddStep("set online status", () => setPresence(UserStatus.Online, null));
AddAssert("visit message is not visible", () => !panel.LastVisitMessage.IsPresent);
}
[Test]
@ -185,6 +184,31 @@ namespace osu.Game.Tests.Visual.Online
AddStep("set statistics to empty", () => statisticsProvider.UpdateStatistics(new UserStatistics(), Ruleset.Value));
}
[Test]
public void TestLocalUserActivity()
{
AddStep("idle", () => setPresence(UserStatus.Online, null, API.LocalUser.Value.OnlineID));
AddStep("watching replay", () => setPresence(UserStatus.Online, new UserActivity.WatchingReplay(createScore(@"nats")), API.LocalUser.Value.OnlineID));
AddStep("spectating user", () => setPresence(UserStatus.Online, new UserActivity.SpectatingUser(createScore(@"mrekk")), API.LocalUser.Value.OnlineID));
AddStep("solo (osu!)", () => setPresence(UserStatus.Online, soloGameStatusForRuleset(0), API.LocalUser.Value.OnlineID));
AddStep("solo (osu!taiko)", () => setPresence(UserStatus.Online, soloGameStatusForRuleset(1), API.LocalUser.Value.OnlineID));
AddStep("solo (osu!catch)", () => setPresence(UserStatus.Online, soloGameStatusForRuleset(2), API.LocalUser.Value.OnlineID));
AddStep("solo (osu!mania)", () => setPresence(UserStatus.Online, soloGameStatusForRuleset(3), API.LocalUser.Value.OnlineID));
AddStep("choosing", () => setPresence(UserStatus.Online, new UserActivity.ChoosingBeatmap(), API.LocalUser.Value.OnlineID));
AddStep("editing beatmap", () => setPresence(UserStatus.Online, new UserActivity.EditingBeatmap(new BeatmapInfo()), API.LocalUser.Value.OnlineID));
AddStep("modding beatmap", () => setPresence(UserStatus.Online, new UserActivity.ModdingBeatmap(new BeatmapInfo()), API.LocalUser.Value.OnlineID));
AddStep("testing beatmap", () => setPresence(UserStatus.Online, new UserActivity.TestingBeatmap(new BeatmapInfo()), API.LocalUser.Value.OnlineID));
AddStep("set offline status", () => setPresence(UserStatus.Offline, null, API.LocalUser.Value.OnlineID));
}
private void setPresence(UserStatus status, UserActivity? activity, int? userId = null)
{
if (status == UserStatus.Offline)
metadataClient.UserPresenceUpdated(userId ?? panel.User.OnlineID, null);
else
metadataClient.UserPresenceUpdated(userId ?? panel.User.OnlineID, new UserPresence { Status = status, Activity = activity });
}
private UserActivity soloGameStatusForRuleset(int rulesetId) => new UserActivity.InSoloGame(new BeatmapInfo(), rulesetStore.GetRuleset(rulesetId)!);
private ScoreInfo createScore(string name) => new ScoreInfo(new TestBeatmap(Ruleset.Value).BeatmapInfo)

View File

@ -67,19 +67,19 @@ namespace osu.Game.Tests.Visual.Online
[Test]
public void TestLink()
{
AddStep("set current path", () => markdownContainer.CurrentPath = $"{API.WebsiteRootUrl}/wiki/Article_styling_criteria/");
AddStep("set current path", () => markdownContainer.CurrentPath = $"{API.Endpoints.WebsiteUrl}/wiki/Article_styling_criteria/");
AddStep("set '/wiki/Main_page''", () => markdownContainer.Text = "[wiki main page](/wiki/Main_page)");
AddAssert("check url", () => markdownContainer.Link.Url == $"{API.WebsiteRootUrl}/wiki/Main_page");
AddAssert("check url", () => markdownContainer.Link.Url == $"{API.Endpoints.WebsiteUrl}/wiki/Main_page");
AddStep("set '../FAQ''", () => markdownContainer.Text = "[FAQ](../FAQ)");
AddAssert("check url", () => markdownContainer.Link.Url == $"{API.WebsiteRootUrl}/wiki/FAQ");
AddAssert("check url", () => markdownContainer.Link.Url == $"{API.Endpoints.WebsiteUrl}/wiki/FAQ");
AddStep("set './Writing''", () => markdownContainer.Text = "[wiki writing guidline](./Writing)");
AddAssert("check url", () => markdownContainer.Link.Url == $"{API.WebsiteRootUrl}/wiki/Article_styling_criteria/Writing");
AddAssert("check url", () => markdownContainer.Link.Url == $"{API.Endpoints.WebsiteUrl}/wiki/Article_styling_criteria/Writing");
AddStep("set 'Formatting''", () => markdownContainer.Text = "[wiki formatting guidline](Formatting)");
AddAssert("check url", () => markdownContainer.Link.Url == $"{API.WebsiteRootUrl}/wiki/Article_styling_criteria/Formatting");
AddAssert("check url", () => markdownContainer.Link.Url == $"{API.Endpoints.WebsiteUrl}/wiki/Article_styling_criteria/Formatting");
}
[Test]

View File

@ -62,12 +62,6 @@ namespace osu.Game.Tests.Visual.Ranking
if (beatmapInfo != null)
Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmapInfo);
});
AddToggleStep("toggle legacy classic skin", v =>
{
if (skins != null)
skins.CurrentSkinInfo.Value = v ? skins.DefaultClassicSkin.SkinInfo : skins.CurrentSkinInfo.Default;
});
}
[SetUp]
@ -84,6 +78,16 @@ namespace osu.Game.Tests.Visual.Ranking
}));
}
[Test]
public void TestLegacySkin()
{
AddToggleStep("toggle legacy classic skin", v =>
{
if (skins != null)
skins.CurrentSkinInfo.Value = v ? skins.DefaultClassicSkin.SkinInfo : skins.CurrentSkinInfo.Default;
});
}
private int onlineScoreID = 1;
[TestCase(1, ScoreRank.X, 0)]

View File

@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
@ -16,10 +18,10 @@ using osu.Game.Database;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Filter;
using osu.Game.Screens.SelectV2;
using osu.Game.Tests.Beatmaps;
using osu.Game.Tests.Resources;
using osuTK;
using osuTK.Graphics;
using osuTK.Input;
using BeatmapCarousel = osu.Game.Screens.SelectV2.BeatmapCarousel;
@ -53,16 +55,6 @@ namespace osu.Game.Tests.Visual.SongSelect
Scheduler.AddDelayed(updateStats, 100, true);
}
[SetUpSteps]
public virtual void SetUpSteps()
{
RemoveAllBeatmaps();
CreateCarousel();
SortBy(new FilterCriteria { Sort = SortMode.Title });
}
protected void CreateCarousel()
{
AddStep("create components", () =>
@ -130,7 +122,7 @@ namespace osu.Game.Tests.Visual.SongSelect
});
}
protected void SortBy(FilterCriteria criteria) => AddStep($"sort {criteria.Sort} group {criteria.Group}", () => Carousel.Filter(criteria));
protected void SortBy(FilterCriteria criteria) => AddStep($"sort:{criteria.Sort} group:{criteria.Group}", () => Carousel.Filter(criteria));
protected void WaitForDrawablePanels() => AddUntilStep("drawable panels loaded", () => Carousel.ChildrenOfType<ICarouselPanel>().Count(), () => Is.GreaterThan(0));
protected void WaitForSorting() => AddUntilStep("sorting finished", () => Carousel.IsFiltering, () => Is.False);
@ -146,6 +138,9 @@ namespace osu.Game.Tests.Visual.SongSelect
protected void CheckNoSelection() => AddAssert("has no selection", () => Carousel.CurrentSelection, () => Is.Null);
protected void CheckHasSelection() => AddAssert("has selection", () => Carousel.CurrentSelection, () => Is.Not.Null);
protected ICarouselPanel? GetSelectedPanel() => Carousel.ChildrenOfType<ICarouselPanel>().SingleOrDefault(p => p.Selected.Value);
protected ICarouselPanel? GetKeyboardSelectedPanel() => Carousel.ChildrenOfType<ICarouselPanel>().SingleOrDefault(p => p.KeyboardSelected.Value);
protected void WaitForGroupSelection(int group, int panel)
{
AddUntilStep($"selected is group{group} panel{panel}", () =>
@ -171,6 +166,15 @@ namespace osu.Game.Tests.Visual.SongSelect
});
}
protected IEnumerable<T> GetVisiblePanels<T>()
where T : Drawable
{
return Carousel.ChildrenOfType<UserTrackingScrollContainer>().Single()
.ChildrenOfType<T>()
.Where(p => ((ICarouselPanel)p).Item?.IsVisible == true)
.OrderBy(p => p.Y);
}
protected void ClickVisiblePanel<T>(int index)
where T : Drawable
{
@ -185,17 +189,66 @@ namespace osu.Game.Tests.Visual.SongSelect
});
}
protected void ClickVisiblePanelWithOffset<T>(int index, Vector2 positionOffsetFromCentre)
where T : Drawable
{
AddStep($"move mouse to panel {index} with offset {positionOffsetFromCentre}", () =>
{
var panel = Carousel.ChildrenOfType<UserTrackingScrollContainer>().Single()
.ChildrenOfType<T>()
.Where(p => ((ICarouselPanel)p).Item?.IsVisible == true)
.OrderBy(p => p.Y)
.ElementAt(index);
InputManager.MoveMouseTo(panel.ScreenSpaceDrawQuad.Centre + panel.ToScreenSpace(positionOffsetFromCentre) - panel.ToScreenSpace(Vector2.Zero));
});
AddStep("click", () => InputManager.Click(MouseButton.Left));
}
/// <summary>
/// Add requested beatmap sets count to list.
/// </summary>
/// <param name="count">The count of beatmap sets to add.</param>
/// <param name="fixedDifficultiesPerSet">If not null, the number of difficulties per set. If null, randomised difficulty count will be used.</param>
protected void AddBeatmaps(int count, int? fixedDifficultiesPerSet = null) => AddStep($"add {count} beatmaps", () =>
/// <param name="randomMetadata">Whether to randomise the metadata to make groupings more uniform.</param>
protected void AddBeatmaps(int count, int? fixedDifficultiesPerSet = null, bool randomMetadata = false) => AddStep($"add {count} beatmaps{(randomMetadata ? " with random data" : "")}", () =>
{
for (int i = 0; i < count; i++)
BeatmapSets.Add(TestResources.CreateTestBeatmapSetInfo(fixedDifficultiesPerSet ?? RNG.Next(1, 4)));
BeatmapSets.Add(CreateTestBeatmapSetInfo(fixedDifficultiesPerSet, randomMetadata));
});
protected static BeatmapSetInfo CreateTestBeatmapSetInfo(int? fixedDifficultiesPerSet, bool randomMetadata)
{
var beatmapSetInfo = TestResources.CreateTestBeatmapSetInfo(fixedDifficultiesPerSet ?? RNG.Next(1, 4));
if (randomMetadata)
{
char randomCharacter = getRandomCharacter();
var metadata = new BeatmapMetadata
{
// Create random metadata, then we can check if sorting works based on these
Artist = $"{randomCharacter}ome Artist " + RNG.Next(0, 9),
Title = $"{randomCharacter}ome Song (set id {beatmapSetInfo.OnlineID:000}) {Guid.NewGuid()}",
Author = { Username = $"{randomCharacter}ome Guy " + RNG.Next(0, 9) },
};
foreach (var beatmap in beatmapSetInfo.Beatmaps)
beatmap.Metadata = metadata.DeepClone();
}
return beatmapSetInfo;
}
private static long randomCharPointer;
private static char getRandomCharacter()
{
const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz*";
return chars[(int)((randomCharPointer++ / 2) % chars.Length)];
}
protected void RemoveAllBeatmaps() => AddStep("clear all beatmaps", () => BeatmapSets.Clear());
protected void RemoveFirstBeatmap() =>

View File

@ -0,0 +1,91 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Threading.Tasks;
using NUnit.Framework;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Filter;
using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Visual.SongSelect
{
/// <summary>
/// Covers common steps which can be used for manual testing.
/// </summary>
[TestFixture]
public partial class TestSceneBeatmapCarouselV2 : BeatmapCarouselV2TestScene
{
[Test]
[Explicit]
public void TestBasics()
{
CreateCarousel();
RemoveAllBeatmaps();
AddBeatmaps(10, randomMetadata: true);
AddBeatmaps(10);
AddBeatmaps(1);
}
[Test]
[Explicit]
public void TestSorting()
{
SortBy(new FilterCriteria { Sort = SortMode.Artist });
SortBy(new FilterCriteria { Group = GroupMode.Difficulty, Sort = SortMode.Difficulty });
SortBy(new FilterCriteria { Group = GroupMode.Artist, Sort = SortMode.Artist });
}
[Test]
[Explicit]
public void TestRemovals()
{
RemoveFirstBeatmap();
RemoveAllBeatmaps();
}
[Test]
[Explicit]
public void TestAddRemoveRepeatedOps()
{
AddRepeatStep("add beatmaps", () => BeatmapSets.Add(TestResources.CreateTestBeatmapSetInfo(RNG.Next(1, 4))), 20);
AddRepeatStep("remove beatmaps", () => BeatmapSets.RemoveAt(RNG.Next(0, BeatmapSets.Count)), 20);
}
[Test]
[Explicit]
public void TestMasking()
{
AddStep("disable masking", () => Scroll.Masking = false);
AddStep("enable masking", () => Scroll.Masking = true);
}
[Test]
[Explicit]
public void TestPerformanceWithManyBeatmaps()
{
const int count = 200000;
List<BeatmapSetInfo> generated = new List<BeatmapSetInfo>();
AddStep($"populate {count} test beatmaps", () =>
{
generated.Clear();
Task.Run(() =>
{
for (int j = 0; j < count; j++)
generated.Add(CreateTestBeatmapSetInfo(3, true));
}).ConfigureAwait(true);
});
AddUntilStep("wait for beatmaps populated", () => generated.Count, () => Is.GreaterThan(count / 3));
AddUntilStep("this takes a while", () => generated.Count, () => Is.GreaterThan(count / 3 * 2));
AddUntilStep("maybe they are done now", () => generated.Count, () => Is.EqualTo(count));
AddStep("add all beatmaps", () => BeatmapSets.AddRange(generated));
}
}
}

View File

@ -0,0 +1,177 @@
// 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.Testing;
using osu.Game.Beatmaps;
using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Filter;
using osu.Game.Screens.SelectV2;
namespace osu.Game.Tests.Visual.SongSelect
{
[TestFixture]
public partial class TestSceneBeatmapCarouselV2ArtistGrouping : BeatmapCarouselV2TestScene
{
[SetUpSteps]
public void SetUpSteps()
{
RemoveAllBeatmaps();
CreateCarousel();
SortBy(new FilterCriteria { Group = GroupMode.Artist, Sort = SortMode.Artist });
AddBeatmaps(10, 3, true);
WaitForDrawablePanels();
}
[Test]
public void TestOpenCloseGroupWithNoSelectionMouse()
{
AddAssert("no beatmaps visible", () => Carousel.ChildrenOfType<BeatmapPanel>().Count(p => p.Alpha > 0), () => Is.Zero);
AddUntilStep("no sets visible", () => Carousel.ChildrenOfType<BeatmapSetPanel>().Count(p => p.Alpha > 0), () => Is.Zero);
CheckNoSelection();
ClickVisiblePanel<GroupPanel>(0);
AddUntilStep("some sets visible", () => Carousel.ChildrenOfType<BeatmapSetPanel>().Count(p => p.Alpha > 0), () => Is.GreaterThan(0));
AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType<BeatmapPanel>().Count(p => p.Alpha > 0), () => Is.Zero);
CheckNoSelection();
ClickVisiblePanel<GroupPanel>(0);
AddUntilStep("no sets visible", () => Carousel.ChildrenOfType<BeatmapSetPanel>().Count(p => p.Alpha > 0), () => Is.Zero);
AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType<BeatmapPanel>().Count(p => p.Alpha > 0), () => Is.Zero);
CheckNoSelection();
}
[Test]
public void TestOpenCloseGroupWithNoSelectionKeyboard()
{
AddAssert("no beatmaps visible", () => Carousel.ChildrenOfType<BeatmapPanel>().Count(p => p.Alpha > 0), () => Is.Zero);
AddUntilStep("no sets visible", () => Carousel.ChildrenOfType<BeatmapSetPanel>().Count(p => p.Alpha > 0), () => Is.Zero);
CheckNoSelection();
SelectNextPanel();
Select();
AddUntilStep("some sets visible", () => Carousel.ChildrenOfType<BeatmapSetPanel>().Count(p => p.Alpha > 0), () => Is.GreaterThan(0));
AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType<BeatmapPanel>().Count(p => p.Alpha > 0), () => Is.Zero);
AddAssert("keyboard selected is expanded", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.True);
CheckNoSelection();
Select();
AddUntilStep("no sets visible", () => Carousel.ChildrenOfType<BeatmapSetPanel>().Count(p => p.Alpha > 0), () => Is.Zero);
AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType<BeatmapPanel>().Count(p => p.Alpha > 0), () => Is.Zero);
AddAssert("keyboard selected is collapsed", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.False);
CheckNoSelection();
}
[Test]
public void TestCarouselRemembersSelection()
{
SelectNextGroup();
object? selection = null;
AddStep("store drawable selection", () => selection = GetSelectedPanel()?.Item?.Model);
CheckHasSelection();
AddAssert("drawable selection non-null", () => selection, () => Is.Not.Null);
AddAssert("drawable selection matches carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection));
RemoveAllBeatmaps();
AddUntilStep("no drawable selection", GetSelectedPanel, () => Is.Null);
AddBeatmaps(10);
WaitForDrawablePanels();
CheckHasSelection();
AddAssert("no drawable selection", GetSelectedPanel, () => Is.Null);
AddStep("add previous selection", () => BeatmapSets.Add(((BeatmapInfo)selection!).BeatmapSet!));
AddAssert("selection matches original carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection));
AddUntilStep("drawable selection restored", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(selection));
AddAssert("carousel item is visible", () => GetSelectedPanel()?.Item?.IsVisible, () => Is.True);
ClickVisiblePanel<GroupPanel>(0);
AddUntilStep("carousel item not visible", GetSelectedPanel, () => Is.Null);
ClickVisiblePanel<GroupPanel>(0);
AddUntilStep("carousel item is visible", () => GetSelectedPanel()?.Item?.IsVisible, () => Is.True);
}
[Test]
public void TestGroupSelectionOnHeader()
{
SelectNextGroup();
WaitForGroupSelection(0, 1);
SelectPrevPanel();
SelectPrevPanel();
AddAssert("keyboard selected panel is expanded", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.True);
SelectPrevGroup();
WaitForGroupSelection(0, 1);
AddAssert("keyboard selected panel is contracted", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.False);
SelectPrevGroup();
WaitForGroupSelection(0, 1);
AddAssert("keyboard selected panel is expanded", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.True);
}
[Test]
public void TestKeyboardSelection()
{
SelectNextPanel();
SelectNextPanel();
SelectNextPanel();
SelectNextPanel();
CheckNoSelection();
// open first group
Select();
CheckNoSelection();
AddUntilStep("some beatmaps visible", () => Carousel.ChildrenOfType<BeatmapSetPanel>().Count(p => p.Alpha > 0), () => Is.GreaterThan(0));
SelectNextPanel();
Select();
WaitForGroupSelection(3, 1);
SelectNextGroup();
WaitForGroupSelection(3, 5);
SelectNextGroup();
WaitForGroupSelection(4, 1);
SelectPrevGroup();
WaitForGroupSelection(3, 5);
SelectNextGroup();
WaitForGroupSelection(4, 1);
SelectNextGroup();
WaitForGroupSelection(4, 5);
SelectNextGroup();
WaitForGroupSelection(0, 1);
SelectNextPanel();
SelectNextPanel();
SelectNextPanel();
SelectNextPanel();
SelectNextGroup();
WaitForGroupSelection(0, 1);
SelectNextPanel();
SelectNextGroup();
WaitForGroupSelection(1, 1);
}
}
}

View File

@ -1,126 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NUnit.Framework;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Filter;
using osu.Game.Screens.SelectV2;
using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Visual.SongSelect
{
/// <summary>
/// Currently covers adding and removing of items and scrolling.
/// If we add more tests here, these two categories can likely be split out into separate scenes.
/// </summary>
[TestFixture]
public partial class TestSceneBeatmapCarouselV2Basics : BeatmapCarouselV2TestScene
{
[Test]
public void TestBasics()
{
AddBeatmaps(1);
AddBeatmaps(10);
RemoveFirstBeatmap();
RemoveAllBeatmaps();
}
[Test]
public void TestOffScreenLoading()
{
AddStep("disable masking", () => Scroll.Masking = false);
AddStep("enable masking", () => Scroll.Masking = true);
}
[Test]
public void TestAddRemoveOneByOne()
{
AddRepeatStep("add beatmaps", () => BeatmapSets.Add(TestResources.CreateTestBeatmapSetInfo(RNG.Next(1, 4))), 20);
AddRepeatStep("remove beatmaps", () => BeatmapSets.RemoveAt(RNG.Next(0, BeatmapSets.Count)), 20);
}
[Test]
public void TestSorting()
{
AddBeatmaps(10);
SortBy(new FilterCriteria { Group = GroupMode.Difficulty, Sort = SortMode.Difficulty });
SortBy(new FilterCriteria { Sort = SortMode.Artist });
}
[Test]
public void TestScrollPositionMaintainedOnAddSecondSelected()
{
Quad positionBefore = default;
AddBeatmaps(10);
WaitForDrawablePanels();
AddStep("select middle beatmap", () => Carousel.CurrentSelection = BeatmapSets.ElementAt(BeatmapSets.Count - 2));
AddStep("scroll to selected item", () => Scroll.ScrollTo(Scroll.ChildrenOfType<BeatmapPanel>().Single(p => p.Selected.Value)));
WaitForScrolling();
AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType<BeatmapPanel>().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad);
RemoveFirstBeatmap();
WaitForSorting();
AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType<BeatmapPanel>().Single(p => p.Selected.Value).ScreenSpaceDrawQuad,
() => Is.EqualTo(positionBefore));
}
[Test]
public void TestScrollPositionMaintainedOnAddLastSelected()
{
Quad positionBefore = default;
AddBeatmaps(10);
WaitForDrawablePanels();
AddStep("scroll to last item", () => Scroll.ScrollToEnd(false));
AddStep("select last beatmap", () => Carousel.CurrentSelection = BeatmapSets.Last());
WaitForScrolling();
AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType<BeatmapPanel>().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad);
RemoveFirstBeatmap();
WaitForSorting();
AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType<BeatmapPanel>().Single(p => p.Selected.Value).ScreenSpaceDrawQuad,
() => Is.EqualTo(positionBefore));
}
[Test]
[Explicit]
public void TestPerformanceWithManyBeatmaps()
{
const int count = 200000;
List<BeatmapSetInfo> generated = new List<BeatmapSetInfo>();
AddStep($"populate {count} test beatmaps", () =>
{
generated.Clear();
Task.Run(() =>
{
for (int j = 0; j < count; j++)
generated.Add(TestResources.CreateTestBeatmapSetInfo(RNG.Next(1, 4)));
}).ConfigureAwait(true);
});
AddUntilStep("wait for beatmaps populated", () => generated.Count, () => Is.GreaterThan(count / 3));
AddUntilStep("this takes a while", () => generated.Count, () => Is.GreaterThan(count / 3 * 2));
AddUntilStep("maybe they are done now", () => generated.Count, () => Is.EqualTo(count));
AddStep("add all beatmaps", () => BeatmapSets.AddRange(generated));
}
}
}

View File

@ -8,27 +8,27 @@ using osu.Game.Beatmaps;
using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Filter;
using osu.Game.Screens.SelectV2;
using osuTK;
namespace osu.Game.Tests.Visual.SongSelect
{
[TestFixture]
public partial class TestSceneBeatmapCarouselV2GroupSelection : BeatmapCarouselV2TestScene
public partial class TestSceneBeatmapCarouselV2DifficultyGrouping : BeatmapCarouselV2TestScene
{
public override void SetUpSteps()
[SetUpSteps]
public void SetUpSteps()
{
RemoveAllBeatmaps();
CreateCarousel();
SortBy(new FilterCriteria { Group = GroupMode.Difficulty, Sort = SortMode.Difficulty });
AddBeatmaps(10, 3);
WaitForDrawablePanels();
}
[Test]
public void TestOpenCloseGroupWithNoSelectionMouse()
{
AddBeatmaps(10, 5);
WaitForDrawablePanels();
AddAssert("no beatmaps visible", () => Carousel.ChildrenOfType<BeatmapPanel>().Count(p => p.Alpha > 0), () => Is.Zero);
CheckNoSelection();
@ -44,86 +44,99 @@ namespace osu.Game.Tests.Visual.SongSelect
[Test]
public void TestOpenCloseGroupWithNoSelectionKeyboard()
{
AddBeatmaps(10, 5);
WaitForDrawablePanels();
AddAssert("no beatmaps visible", () => Carousel.ChildrenOfType<BeatmapPanel>().Count(p => p.Alpha > 0), () => Is.Zero);
CheckNoSelection();
SelectNextPanel();
Select();
AddUntilStep("some beatmaps visible", () => Carousel.ChildrenOfType<BeatmapPanel>().Count(p => p.Alpha > 0), () => Is.GreaterThan(0));
AddAssert("keyboard selected is expanded", () => getKeyboardSelectedPanel()?.Expanded.Value, () => Is.True);
AddAssert("keyboard selected is expanded", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.True);
CheckNoSelection();
Select();
AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType<BeatmapPanel>().Count(p => p.Alpha > 0), () => Is.Zero);
AddAssert("keyboard selected is collapsed", () => getKeyboardSelectedPanel()?.Expanded.Value, () => Is.False);
AddAssert("keyboard selected is collapsed", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.False);
CheckNoSelection();
GroupPanel? getKeyboardSelectedPanel() => Carousel.ChildrenOfType<GroupPanel>().SingleOrDefault(p => p.KeyboardSelected.Value);
}
[Test]
public void TestCarouselRemembersSelection()
{
AddBeatmaps(10);
WaitForDrawablePanels();
SelectNextGroup();
object? selection = null;
AddStep("store drawable selection", () => selection = getSelectedPanel()?.Item?.Model);
AddStep("store drawable selection", () => selection = GetSelectedPanel()?.Item?.Model);
CheckHasSelection();
AddAssert("drawable selection non-null", () => selection, () => Is.Not.Null);
AddAssert("drawable selection matches carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection));
RemoveAllBeatmaps();
AddUntilStep("no drawable selection", getSelectedPanel, () => Is.Null);
AddUntilStep("no drawable selection", GetSelectedPanel, () => Is.Null);
AddBeatmaps(10);
AddBeatmaps(3);
WaitForDrawablePanels();
CheckHasSelection();
AddAssert("no drawable selection", getSelectedPanel, () => Is.Null);
AddAssert("no drawable selection", GetSelectedPanel, () => Is.Null);
AddStep("add previous selection", () => BeatmapSets.Add(((BeatmapInfo)selection!).BeatmapSet!));
AddAssert("selection matches original carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection));
AddUntilStep("drawable selection restored", () => getSelectedPanel()?.Item?.Model, () => Is.EqualTo(selection));
AddAssert("carousel item is visible", () => getSelectedPanel()?.Item?.IsVisible, () => Is.True);
AddUntilStep("drawable selection restored", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(selection));
AddAssert("carousel item is visible", () => GetSelectedPanel()?.Item?.IsVisible, () => Is.True);
ClickVisiblePanel<GroupPanel>(0);
AddUntilStep("carousel item not visible", getSelectedPanel, () => Is.Null);
AddUntilStep("carousel item not visible", GetSelectedPanel, () => Is.Null);
ClickVisiblePanel<GroupPanel>(0);
AddUntilStep("carousel item is visible", () => getSelectedPanel()?.Item?.IsVisible, () => Is.True);
BeatmapPanel? getSelectedPanel() => Carousel.ChildrenOfType<BeatmapPanel>().SingleOrDefault(p => p.Selected.Value);
AddUntilStep("carousel item is visible", () => GetSelectedPanel()?.Item?.IsVisible, () => Is.True);
}
[Test]
public void TestGroupSelectionOnHeader()
public void TestGroupSelectionOnHeaderKeyboard()
{
AddBeatmaps(10, 3);
WaitForDrawablePanels();
SelectNextGroup();
WaitForGroupSelection(0, 0);
SelectPrevPanel();
AddAssert("keyboard selected panel is expanded", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.True);
SelectPrevGroup();
WaitForGroupSelection(2, 9);
WaitForGroupSelection(0, 0);
AddAssert("keyboard selected panel is contracted", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.False);
SelectPrevGroup();
WaitForGroupSelection(0, 0);
AddAssert("keyboard selected panel is expanded", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.True);
}
[Test]
public void TestGroupSelectionOnHeaderMouse()
{
SelectNextGroup();
WaitForGroupSelection(0, 0);
AddAssert("keyboard selected panel is beatmap", GetKeyboardSelectedPanel, Is.TypeOf<BeatmapPanel>);
AddAssert("selected panel is beatmap", GetSelectedPanel, Is.TypeOf<BeatmapPanel>);
ClickVisiblePanel<GroupPanel>(0);
AddAssert("keyboard selected panel is group", GetKeyboardSelectedPanel, Is.TypeOf<GroupPanel>);
AddAssert("keyboard selected panel is contracted", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.False);
ClickVisiblePanel<GroupPanel>(0);
AddAssert("keyboard selected panel is group", GetKeyboardSelectedPanel, Is.TypeOf<GroupPanel>);
AddAssert("keyboard selected panel is expanded", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.True);
AddAssert("selected panel is still beatmap", GetSelectedPanel, Is.TypeOf<BeatmapPanel>);
}
[Test]
public void TestKeyboardSelection()
{
AddBeatmaps(10, 3);
WaitForDrawablePanels();
SelectNextPanel();
SelectNextPanel();
SelectNextPanel();
@ -154,5 +167,28 @@ namespace osu.Game.Tests.Visual.SongSelect
SelectPrevGroup();
WaitForGroupSelection(2, 9);
}
[Test]
public void TestInputHandlingWithinGaps()
{
AddAssert("no beatmaps visible", () => !GetVisiblePanels<BeatmapPanel>().Any());
// Clicks just above the first group panel should not actuate any action.
ClickVisiblePanelWithOffset<GroupPanel>(0, new Vector2(0, -(GroupPanel.HEIGHT / 2 + 1)));
AddAssert("no beatmaps visible", () => !GetVisiblePanels<BeatmapPanel>().Any());
ClickVisiblePanelWithOffset<GroupPanel>(0, new Vector2(0, -(GroupPanel.HEIGHT / 2)));
AddUntilStep("wait for beatmaps visible", () => GetVisiblePanels<BeatmapPanel>().Any());
CheckNoSelection();
// Beatmap panels expand their selection area to cover holes from spacing.
ClickVisiblePanelWithOffset<BeatmapPanel>(0, new Vector2(0, -(CarouselItem.DEFAULT_HEIGHT / 2 + 1)));
WaitForGroupSelection(0, 0);
ClickVisiblePanelWithOffset<BeatmapPanel>(1, new Vector2(0, (CarouselItem.DEFAULT_HEIGHT / 2 + 1)));
WaitForGroupSelection(0, 1);
}
}
}

View File

@ -5,14 +5,25 @@ using System.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Filter;
using osu.Game.Screens.SelectV2;
using osuTK;
using osuTK.Input;
namespace osu.Game.Tests.Visual.SongSelect
{
[TestFixture]
public partial class TestSceneBeatmapCarouselV2Selection : BeatmapCarouselV2TestScene
public partial class TestSceneBeatmapCarouselV2NoGrouping : BeatmapCarouselV2TestScene
{
[SetUpSteps]
public void SetUpSteps()
{
RemoveAllBeatmaps();
CreateCarousel();
SortBy(new FilterCriteria { Sort = SortMode.Title });
}
/// <summary>
/// Keyboard selection via up and down arrows doesn't actually change the selection until
/// the select key is pressed.
@ -77,28 +88,26 @@ namespace osu.Game.Tests.Visual.SongSelect
object? selection = null;
AddStep("store drawable selection", () => selection = getSelectedPanel()?.Item?.Model);
AddStep("store drawable selection", () => selection = GetSelectedPanel()?.Item?.Model);
CheckHasSelection();
AddAssert("drawable selection non-null", () => selection, () => Is.Not.Null);
AddAssert("drawable selection matches carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection));
RemoveAllBeatmaps();
AddUntilStep("no drawable selection", getSelectedPanel, () => Is.Null);
AddUntilStep("no drawable selection", GetSelectedPanel, () => Is.Null);
AddBeatmaps(10);
WaitForDrawablePanels();
CheckHasSelection();
AddAssert("no drawable selection", getSelectedPanel, () => Is.Null);
AddAssert("no drawable selection", GetSelectedPanel, () => Is.Null);
AddStep("add previous selection", () => BeatmapSets.Add(((BeatmapInfo)selection!).BeatmapSet!));
AddAssert("selection matches original carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection));
AddUntilStep("drawable selection restored", () => getSelectedPanel()?.Item?.Model, () => Is.EqualTo(selection));
AddAssert("carousel item is visible", () => getSelectedPanel()?.Item?.IsVisible, () => Is.True);
BeatmapPanel? getSelectedPanel() => Carousel.ChildrenOfType<BeatmapPanel>().SingleOrDefault(p => p.Selected.Value);
AddUntilStep("drawable selection restored", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(selection));
AddAssert("carousel item is visible", () => GetSelectedPanel()?.Item?.IsVisible, () => Is.True);
}
[Test]
@ -141,7 +150,11 @@ namespace osu.Game.Tests.Visual.SongSelect
SelectPrevPanel();
SelectPrevGroup();
WaitForSelection(0, 0);
WaitForSelection(1, 0);
SelectPrevPanel();
SelectNextGroup();
WaitForSelection(1, 0);
}
[Test]
@ -194,6 +207,36 @@ namespace osu.Game.Tests.Visual.SongSelect
CheckNoSelection();
}
[Test]
public void TestInputHandlingWithinGaps()
{
AddBeatmaps(2, 5);
WaitForDrawablePanels();
AddAssert("no beatmaps visible", () => !GetVisiblePanels<BeatmapPanel>().Any());
// Clicks just above the first group panel should not actuate any action.
ClickVisiblePanelWithOffset<BeatmapSetPanel>(0, new Vector2(0, -(BeatmapSetPanel.HEIGHT / 2 + 1)));
AddAssert("no beatmaps visible", () => !GetVisiblePanels<BeatmapPanel>().Any());
ClickVisiblePanelWithOffset<BeatmapSetPanel>(0, new Vector2(0, -(BeatmapSetPanel.HEIGHT / 2)));
AddUntilStep("wait for beatmaps visible", () => GetVisiblePanels<BeatmapPanel>().Any());
WaitForSelection(0, 0);
// Beatmap panels expand their selection area to cover holes from spacing.
ClickVisiblePanelWithOffset<BeatmapPanel>(1, new Vector2(0, -(CarouselItem.DEFAULT_HEIGHT / 2 + 1)));
WaitForSelection(0, 0);
// Panels with higher depth will handle clicks in the gutters for simplicity.
ClickVisiblePanelWithOffset<BeatmapPanel>(2, new Vector2(0, (CarouselItem.DEFAULT_HEIGHT / 2 + 1)));
WaitForSelection(0, 2);
ClickVisiblePanelWithOffset<BeatmapPanel>(3, new Vector2(0, (CarouselItem.DEFAULT_HEIGHT / 2 + 1)));
WaitForSelection(0, 3);
}
private void checkSelectionIterating(bool isIterating)
{
object? selection = null;

View File

@ -0,0 +1,65 @@
// 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.Primitives;
using osu.Framework.Testing;
using osu.Game.Screens.Select;
using osu.Game.Screens.SelectV2;
namespace osu.Game.Tests.Visual.SongSelect
{
[TestFixture]
public partial class TestSceneBeatmapCarouselV2Scrolling : BeatmapCarouselV2TestScene
{
[SetUpSteps]
public void SetUpSteps()
{
RemoveAllBeatmaps();
CreateCarousel();
SortBy(new FilterCriteria());
AddBeatmaps(10);
WaitForDrawablePanels();
}
[Test]
public void TestScrollPositionMaintainedOnAddSecondSelected()
{
Quad positionBefore = default;
AddStep("select middle beatmap", () => Carousel.CurrentSelection = BeatmapSets.ElementAt(BeatmapSets.Count - 2).Beatmaps.First());
AddStep("scroll to selected item", () => Scroll.ScrollTo(Scroll.ChildrenOfType<BeatmapPanel>().Single(p => p.Selected.Value)));
WaitForScrolling();
AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType<BeatmapPanel>().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad);
RemoveFirstBeatmap();
WaitForSorting();
AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType<BeatmapPanel>().Single(p => p.Selected.Value).ScreenSpaceDrawQuad,
() => Is.EqualTo(positionBefore));
}
[Test]
public void TestScrollPositionMaintainedOnAddLastSelected()
{
Quad positionBefore = default;
AddStep("scroll to last item", () => Scroll.ScrollToEnd(false));
AddStep("select last beatmap", () => Carousel.CurrentSelection = BeatmapSets.Last().Beatmaps.Last());
WaitForScrolling();
AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType<BeatmapPanel>().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad);
RemoveFirstBeatmap();
WaitForSorting();
AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType<BeatmapPanel>().Single(p => p.Selected.Value).ScreenSpaceDrawQuad,
() => Is.EqualTo(positionBefore));
}
}
}

View File

@ -1239,7 +1239,6 @@ namespace osu.Game.Tests.Visual.SongSelect
}
[Test]
[Solo]
public void TestHardDeleteHandledCorrectly()
{
createSongSelect();

View File

@ -59,7 +59,7 @@ namespace osu.Game.Beatmaps
if (beatmapInfo.OnlineID <= 0 || beatmapInfo.BeatmapSet == null)
return null;
return $@"{api.WebsiteRootUrl}/beatmapsets/{beatmapInfo.BeatmapSet.OnlineID}#{ruleset?.ShortName ?? beatmapInfo.Ruleset.ShortName}/{beatmapInfo.OnlineID}";
return $@"{api.Endpoints.WebsiteUrl}/beatmapsets/{beatmapInfo.BeatmapSet.OnlineID}#{ruleset?.ShortName ?? beatmapInfo.Ruleset.ShortName}/{beatmapInfo.OnlineID}";
}
}
}

View File

@ -41,9 +41,9 @@ namespace osu.Game.Beatmaps
return null;
if (ruleset != null)
return $@"{api.WebsiteRootUrl}/beatmapsets/{beatmapSetInfo.OnlineID}#{ruleset.ShortName}";
return $@"{api.Endpoints.WebsiteUrl}/beatmapsets/{beatmapSetInfo.OnlineID}#{ruleset.ShortName}";
return $@"{api.WebsiteRootUrl}/beatmapsets/{beatmapSetInfo.OnlineID}";
return $@"{api.Endpoints.WebsiteUrl}/beatmapsets/{beatmapSetInfo.OnlineID}";
}
}
}

View File

@ -30,8 +30,8 @@ namespace osu.Game.Beatmaps
{
Metadata = new BeatmapMetadata
{
Artist = "please load a beatmap!",
Title = "no beatmaps available!"
Artist = "please select or load a beatmap!",
Title = "no beatmap selected!"
},
BeatmapSet = new BeatmapSetInfo(),
Difficulty = new BeatmapDifficulty

View File

@ -96,7 +96,6 @@ namespace osu.Game.Collections
lastCreated = collections[changes.InsertedIndices[0]].ID;
foreach (int i in changes.NewModifiedIndices)
{
var updatedItem = collections[i];

View File

@ -15,6 +15,7 @@ using osu.Framework.Localisation;
using osu.Game.Database;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
using osuTK;
@ -27,7 +28,7 @@ namespace osu.Game.Collections
/// </summary>
public partial class DrawableCollectionListItem : OsuRearrangeableListItem<Live<BeatmapCollection>>, IFilterable
{
private const float item_height = 35;
private const float item_height = 45;
private const float button_width = item_height * 0.75f;
protected TextBox TextBox => content.TextBox;
@ -92,13 +93,11 @@ namespace osu.Game.Collections
Padding = new MarginPadding { Right = collection.IsManaged ? button_width : 0 },
Children = new Drawable[]
{
TextBox = new ItemTextBox
TextBox = new ItemTextBox(collection)
{
RelativeSizeAxes = Axes.Both,
Size = Vector2.One,
CornerRadius = item_height / 2,
RelativeSizeAxes = Axes.X,
Height = item_height,
CommitOnFocusLost = true,
PlaceholderText = collection.IsManaged ? string.Empty : "Create a new collection"
},
}
},
@ -125,11 +124,57 @@ namespace osu.Game.Collections
{
protected override float LeftRightPadding => item_height / 2;
private const float count_text_size = 12;
private readonly Live<BeatmapCollection> collection;
private OsuSpriteText countText = null!;
public ItemTextBox(Live<BeatmapCollection> collection)
{
this.collection = collection;
CornerRadius = item_height / 2;
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
BackgroundUnfocused = colours.GreySeaFoamDarker.Darken(0.5f);
BackgroundFocused = colours.GreySeaFoam;
if (collection.IsManaged)
{
TextContainer.Height *= (Height - count_text_size) / Height;
TextContainer.Margin = new MarginPadding { Bottom = count_text_size };
TextContainer.Add(countText = new OsuSpriteText
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.TopLeft,
Depth = float.MinValue,
Font = OsuFont.Default.With(size: count_text_size, weight: FontWeight.SemiBold),
Margin = new MarginPadding { Top = 2, Left = 2 },
Colour = colours.Yellow
});
// interestingly, it is not required to subscribe to change notifications on this collection at all for this to work correctly.
// the reasoning for this is that `DrawableCollectionList` already takes out a subscription on the set of all `BeatmapCollection`s -
// but that subscription does not only cover *changes to the set of collections* (i.e. addition/removal/rearrangement of collections),
// but also covers *changes to the properties of collections*, which `BeatmapMD5Hashes` is one.
// when a collection item changes due to `BeatmapMD5Hashes` changing, the list item is deleted and re-inserted, thus guaranteeing this to work correctly.
int count = collection.PerformRead(c => c.BeatmapMD5Hashes.Count);
countText.Text = count == 1
// Intentionally not localised until we have proper support for this (see https://github.com/ppy/osu-framework/pull/4918
// but also in this case we want support for formatting a number within a string).
? $"{count:#,0} item"
: $"{count:#,0} items";
}
else
{
PlaceholderText = "Create a new collection";
}
}
}
@ -210,7 +255,7 @@ namespace osu.Game.Collections
private void deleteCollection() => collection.PerformWrite(c => c.Realm!.Remove(c));
}
public IEnumerable<LocalisableString> FilterTerms => [(LocalisableString)Model.Value.Name];
public IEnumerable<LocalisableString> FilterTerms => Model.PerformRead(m => m.IsValid ? new[] { (LocalisableString)m.Name } : []);
private bool matchingFilter = true;

View File

@ -220,6 +220,9 @@ namespace osu.Game.Configuration
SetDefault(OsuSetting.AlwaysShowHoldForMenuButton, false);
SetDefault(OsuSetting.AlwaysRequireHoldingForPause, false);
SetDefault(OsuSetting.EditorShowStoryboard, true);
SetDefault(OsuSetting.EditorSubmissionNotifyOnDiscussionReplies, true);
SetDefault(OsuSetting.EditorSubmissionLoadInBrowserAfterSubmission, true);
}
protected override bool CheckLookupContainsPrivateInformation(OsuSetting lookup)
@ -461,5 +464,7 @@ namespace osu.Game.Configuration
BeatmapListingFeaturedArtistFilter,
ShowMobileDisclaimer,
EditorShowStoryboard,
EditorSubmissionNotifyOnDiscussionReplies,
EditorSubmissionLoadInBrowserAfterSubmission,
}
}

View File

@ -95,6 +95,9 @@ namespace osu.Game.Configuration
/// </summary>
DailyChallengeIntroPlayed,
/// <summary>
/// The activity for the current user to broadcast to other players.
/// </summary>
UserOnlineActivity,
}
}

View File

@ -12,17 +12,11 @@ namespace osu.Game.Graphics.UserInterface
{
public partial class OsuContextMenu : OsuMenu
{
private const int fade_duration = 250;
[Resolved]
private OsuMenuSamples menuSamples { get; set; } = null!;
// todo: this shouldn't be required after https://github.com/ppy/osu-framework/issues/4519 is fixed.
private bool wasOpened;
private readonly bool playClickSample;
public OsuContextMenu(bool playClickSample = false)
: base(Direction.Vertical)
public OsuContextMenu(bool playSamples)
: base(Direction.Vertical, topLevelMenu: false, playSamples)
{
MaskingContainer.CornerRadius = 5;
MaskingContainer.EdgeEffect = new EdgeEffectParameters
@ -35,8 +29,6 @@ namespace osu.Game.Graphics.UserInterface
ItemsContainer.Padding = new MarginPadding { Vertical = DrawableOsuMenuItem.MARGIN_VERTICAL };
MaxHeight = 250;
this.playClickSample = playClickSample;
}
[BackgroundDependencyLoader]
@ -47,26 +39,12 @@ namespace osu.Game.Graphics.UserInterface
protected override void AnimateOpen()
{
wasOpened = true;
this.FadeIn(fade_duration, Easing.OutQuint);
if (PlaySamples && !WasOpened)
menuSamples.PlayClickSample();
if (!playClickSample)
return;
menuSamples.PlayClickSample();
menuSamples.PlayOpenSample();
base.AnimateOpen();
}
protected override void AnimateClose()
{
this.FadeOut(fade_duration, Easing.OutQuint);
if (wasOpened)
menuSamples.PlayCloseSample();
wasOpened = false;
}
protected override Menu CreateSubMenu() => new OsuContextMenu();
protected override Menu CreateSubMenu() => new OsuContextMenu(false); // sub menu samples are handled by OsuMenu.OnSubmenuOpen.
}
}

View File

@ -18,21 +18,32 @@ namespace osu.Game.Graphics.UserInterface
{
public partial class OsuMenu : Menu
{
protected const double DELAY_BEFORE_FADE_OUT = 50;
protected const double FADE_DURATION = 280;
// todo: this shouldn't be required after https://github.com/ppy/osu-framework/issues/4519 is fixed.
private bool wasOpened;
protected bool WasOpened { get; private set; }
public bool PlaySamples { get; }
[Resolved]
private OsuMenuSamples menuSamples { get; set; } = null!;
public OsuMenu(Direction direction, bool topLevelMenu = false)
: this(direction, topLevelMenu, playSamples: !topLevelMenu)
{
}
protected OsuMenu(Direction direction, bool topLevelMenu, bool playSamples)
: base(direction, topLevelMenu)
{
PlaySamples = playSamples;
BackgroundColour = Color4.Black.Opacity(0.5f);
MaskingContainer.CornerRadius = 4;
ItemsContainer.Padding = new MarginPadding(5);
OnSubmenuOpen += _ => { menuSamples?.PlaySubOpenSample(); };
OnSubmenuOpen += _ => menuSamples?.PlaySubOpenSample();
}
protected override void Update()
@ -56,20 +67,22 @@ namespace osu.Game.Graphics.UserInterface
protected override void AnimateOpen()
{
if (!TopLevelMenu && !wasOpened)
if (PlaySamples && !WasOpened)
menuSamples?.PlayOpenSample();
this.FadeIn(300, Easing.OutQuint);
wasOpened = true;
WasOpened = true;
this.FadeIn(FADE_DURATION, Easing.OutQuint);
}
protected override void AnimateClose()
{
if (!TopLevelMenu && wasOpened)
if (PlaySamples && WasOpened)
menuSamples?.PlayCloseSample();
this.FadeOut(300, Easing.OutQuint);
wasOpened = false;
this.Delay(DELAY_BEFORE_FADE_OUT)
.FadeOut(FADE_DURATION, Easing.OutQuint);
WasOpened = false;
}
protected override void UpdateSize(Vector2 newSize)
@ -77,12 +90,21 @@ namespace osu.Game.Graphics.UserInterface
if (Direction == Direction.Vertical)
{
Width = newSize.X;
this.ResizeHeightTo(newSize.Y, 300, Easing.OutQuint);
if (newSize.Y > 0)
this.ResizeHeightTo(newSize.Y, 300, Easing.OutQuint);
else
// Delay until the fade out finishes from AnimateClose.
this.Delay(DELAY_BEFORE_FADE_OUT + FADE_DURATION).ResizeHeightTo(0);
}
else
{
Height = newSize.Y;
this.ResizeWidthTo(newSize.X, 300, Easing.OutQuint);
if (newSize.X > 0)
this.ResizeWidthTo(newSize.X, 300, Easing.OutQuint);
else
// Delay until the fade out finishes from AnimateClose.
this.Delay(DELAY_BEFORE_FADE_OUT + FADE_DURATION).ResizeWidthTo(0);
}
}

View File

@ -148,7 +148,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
{
base.LoadComplete();
Content.CornerRadius = 2;
Content.CornerRadius = 4;
Add(triangles = new TrianglesV2
{

View File

@ -227,6 +227,10 @@ namespace osu.Game.Input.Bindings
};
}
/// <remarks>
/// IMPORTANT: New entries should always be added at the end of the enum, as key bindings are stored using the enum's numeric value and
/// changes in order would cause key bindings to get associated with the wrong action.
/// </remarks>
public enum GlobalAction
{
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ToggleChat))]

View File

@ -0,0 +1,124 @@
// 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 BeatmapSubmissionStrings
{
private const string prefix = @"osu.Game.Resources.Localisation.BeatmapSubmission";
/// <summary>
/// "Beatmap submission"
/// </summary>
public static LocalisableString BeatmapSubmissionTitle => new TranslatableString(getKey(@"beatmap_submission_title"), @"Beatmap submission");
/// <summary>
/// "Share your beatmap with the world!"
/// </summary>
public static LocalisableString BeatmapSubmissionDescription => new TranslatableString(getKey(@"beatmap_submission_description"), @"Share your beatmap with the world!");
/// <summary>
/// "Content permissions"
/// </summary>
public static LocalisableString ContentPermissions => new TranslatableString(getKey(@"content_permissions"), @"Content permissions");
/// <summary>
/// "I understand"
/// </summary>
public static LocalisableString ContentPermissionsAcknowledgement => new TranslatableString(getKey(@"content_permissions_acknowledgement"), @"I understand");
/// <summary>
/// "Frequently asked questions"
/// </summary>
public static LocalisableString FrequentlyAskedQuestions => new TranslatableString(getKey(@"frequently_asked_questions"), @"Frequently asked questions");
/// <summary>
/// "Submission settings"
/// </summary>
public static LocalisableString SubmissionSettings => new TranslatableString(getKey(@"submission_settings"), @"Submission settings");
/// <summary>
/// "Before you continue, we ask you to check whether the content you are uploading has been cleared for upload. Please understand that you are responsible for the content you upload to the platform and if in doubt, should ask permission from the creators before uploading!"
/// </summary>
public static LocalisableString ContentPermissionsDisclaimer => new TranslatableString(getKey(@"content_permissions_disclaimer"), @"Before you continue, we ask you to check whether the content you are uploading has been cleared for upload. Please understand that you are responsible for the content you upload to the platform and if in doubt, should ask permission from the creators before uploading!");
/// <summary>
/// "Check the content usage guidelines for more information"
/// </summary>
public static LocalisableString CheckContentUsageGuidelines => new TranslatableString(getKey(@"check_content_usage_guidelines"), @"Check the content usage guidelines for more information");
/// <summary>
/// "Beatmap ranking criteria"
/// </summary>
public static LocalisableString BeatmapRankingCriteria => new TranslatableString(getKey(@"beatmap_ranking_criteria"), @"Beatmap ranking criteria");
/// <summary>
/// "Not sure you meet the guidelines? Check the list and speed up the ranking process!"
/// </summary>
public static LocalisableString BeatmapRankingCriteriaDescription => new TranslatableString(getKey(@"beatmap_ranking_criteria_description"), @"Not sure you meet the guidelines? Check the list and speed up the ranking process!");
/// <summary>
/// "Submission process"
/// </summary>
public static LocalisableString SubmissionProcess => new TranslatableString(getKey(@"submission_process"), @"Submission process");
/// <summary>
/// "Unsure about the submission process? Check out the wiki entry!"
/// </summary>
public static LocalisableString SubmissionProcessDescription => new TranslatableString(getKey(@"submission_process_description"), @"Unsure about the submission process? Check out the wiki entry!");
/// <summary>
/// "Mapping help forum"
/// </summary>
public static LocalisableString MappingHelpForum => new TranslatableString(getKey(@"mapping_help_forum"), @"Mapping help forum");
/// <summary>
/// "Got some questions about mapping and submission? Ask them in the forums!"
/// </summary>
public static LocalisableString MappingHelpForumDescription => new TranslatableString(getKey(@"mapping_help_forum_description"), @"Got some questions about mapping and submission? Ask them in the forums!");
/// <summary>
/// "Modding queues forum"
/// </summary>
public static LocalisableString ModdingQueuesForum => new TranslatableString(getKey(@"modding_queues_forum"), @"Modding queues forum");
/// <summary>
/// "Having trouble getting feedback? Why not ask in a mod queue!"
/// </summary>
public static LocalisableString ModdingQueuesForumDescription => new TranslatableString(getKey(@"modding_queues_forum_description"), @"Having trouble getting feedback? Why not ask in a mod queue!");
/// <summary>
/// "Where would you like to post your beatmap?"
/// </summary>
public static LocalisableString BeatmapSubmissionTargetCaption => new TranslatableString(getKey(@"beatmap_submission_target_caption"), @"Where would you like to post your beatmap?");
/// <summary>
/// "Works in Progress / Help (incomplete, not ready for ranking)"
/// </summary>
public static LocalisableString BeatmapSubmissionTargetWIP => new TranslatableString(getKey(@"beatmap_submission_target_wip"), @"Works in Progress / Help (incomplete, not ready for ranking)");
/// <summary>
/// "Pending (complete, ready for ranking)"
/// </summary>
public static LocalisableString BeatmapSubmissionTargetPending => new TranslatableString(getKey(@"beatmap_submission_target_pending"), @"Pending (complete, ready for ranking)");
/// <summary>
/// "Receive notifications for discussion replies"
/// </summary>
public static LocalisableString NotifyOnDiscussionReplies => new TranslatableString(getKey(@"notify_for_discussion_replies"), @"Receive notifications for discussion replies");
/// <summary>
/// "Load in browser after submission"
/// </summary>
public static LocalisableString LoadInBrowserAfterSubmission => new TranslatableString(getKey(@"load_in_browser_after_submission"), @"Load in browser after submission");
/// <summary>
/// "Note: In order to make it possible for users of all osu! versions to enjoy your beatmap, it will be exported in a backwards-compatible format. While we have made efforts to ensure that that process keeps the beatmap playable in its intended form, some data related to features that previous versions of osu! do not support may be lost."
/// </summary>
public static LocalisableString LegacyExportDisclaimer => new TranslatableString(getKey(@"legacy_export_disclaimer"), @"Note: In order to make it possible for users of all osu! versions to enjoy your beatmap, it will be exported in a backwards-compatible format. While we have made efforts to ensure that that process keeps the beatmap playable in its intended form, some data related to features that previous versions of osu! do not support may be lost.");
private static string getKey(string key) => $@"{prefix}:{key}";
}
}

View File

@ -184,6 +184,16 @@ namespace osu.Game.Localisation
/// </summary>
public static LocalisableString ResetBookmarks => new TranslatableString(getKey(@"reset_bookmarks"), @"Reset bookmarks");
/// <summary>
/// "Open beatmap info page"
/// </summary>
public static LocalisableString OpenInfoPage => new TranslatableString(getKey(@"open_info_page"), @"Open beatmap info page");
/// <summary>
/// "Open beatmap discussion page"
/// </summary>
public static LocalisableString OpenDiscussionPage => new TranslatableString(getKey(@"open_discussion_page"), @"Open beatmap discussion page");
private static string getKey(string key) => $@"{prefix}:{key}";
}
}
}

View File

@ -40,9 +40,7 @@ namespace osu.Game.Online.API
private readonly Queue<APIRequest> queue = new Queue<APIRequest>();
public string APIEndpointUrl { get; }
public string WebsiteRootUrl { get; }
public EndpointConfiguration Endpoints { get; }
/// <summary>
/// The API response version.
@ -75,7 +73,7 @@ namespace osu.Game.Online.API
private readonly CancellationTokenSource cancellationToken = new CancellationTokenSource();
private readonly Logger log;
public APIAccess(OsuGameBase game, OsuConfigManager config, EndpointConfiguration endpointConfiguration, string versionHash)
public APIAccess(OsuGameBase game, OsuConfigManager config, EndpointConfiguration endpoints, string versionHash)
{
this.game = game;
this.config = config;
@ -89,14 +87,13 @@ namespace osu.Game.Online.API
APIVersion = now.Year * 10000 + now.Month * 100 + now.Day;
}
APIEndpointUrl = endpointConfiguration.APIEndpointUrl;
WebsiteRootUrl = endpointConfiguration.WebsiteRootUrl;
Endpoints = endpoints;
NotificationsClient = setUpNotificationsClient();
authentication = new OAuth(endpointConfiguration.APIClientID, endpointConfiguration.APIClientSecret, APIEndpointUrl);
authentication = new OAuth(endpoints.APIClientID, endpoints.APIClientSecret, Endpoints.APIUrl);
log = Logger.GetLogger(LoggingTarget.Network);
log.Add($@"API endpoint root: {APIEndpointUrl}");
log.Add($@"API endpoint root: {Endpoints.APIUrl}");
log.Add($@"API request version: {APIVersion}");
ProvidedUsername = config.Get<string>(OsuSetting.Username);
@ -408,7 +405,7 @@ namespace osu.Game.Online.API
var req = new RegistrationRequest
{
Url = $@"{APIEndpointUrl}/users",
Url = $@"{Endpoints.APIUrl}/users",
Method = HttpMethod.Post,
Username = username,
Email = email,

View File

@ -71,7 +71,7 @@ namespace osu.Game.Online.API
protected virtual WebRequest CreateWebRequest() => new OsuWebRequest(Uri);
protected virtual string Uri => $@"{API!.APIEndpointUrl}/api/v2/{Target}";
protected virtual string Uri => $@"{API!.Endpoints.APIUrl}/api/v2/{Target}";
protected IAPIProvider? API;

View File

@ -41,9 +41,11 @@ namespace osu.Game.Online.API
public string ProvidedUsername => LocalUser.Value.Username;
public string APIEndpointUrl => "http://localhost";
public string WebsiteRootUrl => "http://localhost";
public EndpointConfiguration Endpoints { get; } = new EndpointConfiguration
{
APIUrl = "http://localhost",
WebsiteUrl = "http://localhost",
};
public int APIVersion => int.Parse(DateTime.Now.ToString("yyyyMMdd"));

View File

@ -51,14 +51,9 @@ namespace osu.Game.Online.API
string ProvidedUsername { get; }
/// <summary>
/// The URL endpoint for this API. Does not include a trailing slash.
/// Holds configuration for online endpoints.
/// </summary>
string APIEndpointUrl { get; }
/// <summary>
/// The root URL of the website, excluding the trailing slash.
/// </summary>
string WebsiteRootUrl { get; }
EndpointConfiguration Endpoints { get; }
/// <summary>
/// The version of the API.

View File

@ -0,0 +1,26 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Diagnostics;
using osu.Framework.IO.Network;
namespace osu.Game.Online.API.Requests
{
public abstract class APIUploadRequest : APIRequest
{
protected override WebRequest CreateWebRequest()
{
var request = base.CreateWebRequest();
request.UploadProgress += onUploadProgress;
return request;
}
private void onUploadProgress(long current, long total)
{
Debug.Assert(API != null);
API.Schedule(() => Progressed?.Invoke(current, total));
}
public event APIProgressHandler? Progressed;
}
}

View File

@ -0,0 +1,55 @@
// 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.Net.Http;
using osu.Framework.IO.Network;
namespace osu.Game.Online.API.Requests
{
public class PatchBeatmapPackageRequest : APIUploadRequest
{
protected override string Uri
{
get
{
// can be removed once the service has been successfully deployed to production
if (API!.Endpoints.BeatmapSubmissionServiceUrl == null)
throw new NotSupportedException("Beatmap submission not supported in this configuration!");
return $@"{API!.Endpoints.BeatmapSubmissionServiceUrl!}/beatmapsets/{BeatmapSetID}";
}
}
protected override string Target => throw new NotSupportedException();
public uint BeatmapSetID { get; }
// ReSharper disable once CollectionNeverUpdated.Global
public Dictionary<string, byte[]> FilesChanged { get; } = new Dictionary<string, byte[]>();
// ReSharper disable once CollectionNeverUpdated.Global
public HashSet<string> FilesDeleted { get; } = new HashSet<string>();
public PatchBeatmapPackageRequest(uint beatmapSetId)
{
BeatmapSetID = beatmapSetId;
}
protected override WebRequest CreateWebRequest()
{
var request = base.CreateWebRequest();
request.Method = HttpMethod.Patch;
foreach ((string filename, byte[] content) in FilesChanged)
request.AddFile(@"filesChanged", content, filename);
foreach (string filename in FilesDeleted)
request.AddParameter(@"filesDeleted", filename, RequestParameterType.Form);
request.Timeout = 60_000;
return request;
}
}
}

View File

@ -0,0 +1,82 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using osu.Framework.IO.Network;
using osu.Framework.Localisation;
using osu.Game.Localisation;
using osu.Game.Online.API.Requests.Responses;
namespace osu.Game.Online.API.Requests
{
public class PutBeatmapSetRequest : APIRequest<PutBeatmapSetResponse>
{
protected override string Uri
{
get
{
// can be removed once the service has been successfully deployed to production
if (API!.Endpoints.BeatmapSubmissionServiceUrl == null)
throw new NotSupportedException("Beatmap submission not supported in this configuration!");
return $@"{API!.Endpoints.BeatmapSubmissionServiceUrl}/beatmapsets";
}
}
protected override string Target => throw new NotSupportedException();
[JsonProperty("beatmapset_id")]
public uint? BeatmapSetID { get; init; }
[JsonProperty("beatmaps_to_create")]
public uint BeatmapsToCreate { get; init; }
[JsonProperty("beatmaps_to_keep")]
public uint[] BeatmapsToKeep { get; init; } = [];
[JsonProperty("target")]
public BeatmapSubmissionTarget SubmissionTarget { get; init; }
private PutBeatmapSetRequest()
{
}
public static PutBeatmapSetRequest CreateNew(uint beatmapCount, BeatmapSubmissionTarget target) => new PutBeatmapSetRequest
{
BeatmapsToCreate = beatmapCount,
SubmissionTarget = target,
};
public static PutBeatmapSetRequest UpdateExisting(uint beatmapSetId, IEnumerable<uint> beatmapsToKeep, uint beatmapsToCreate, BeatmapSubmissionTarget target) => new PutBeatmapSetRequest
{
BeatmapSetID = beatmapSetId,
BeatmapsToKeep = beatmapsToKeep.ToArray(),
BeatmapsToCreate = beatmapsToCreate,
SubmissionTarget = target,
};
protected override WebRequest CreateWebRequest()
{
var req = base.CreateWebRequest();
req.Method = HttpMethod.Put;
req.ContentType = @"application/json";
req.AddRaw(JsonConvert.SerializeObject(this));
return req;
}
}
[JsonConverter(typeof(StringEnumConverter))]
public enum BeatmapSubmissionTarget
{
[LocalisableDescription(typeof(BeatmapSubmissionStrings), nameof(BeatmapSubmissionStrings.BeatmapSubmissionTargetWIP))]
WIP,
[LocalisableDescription(typeof(BeatmapSubmissionStrings), nameof(BeatmapSubmissionStrings.BeatmapSubmissionTargetPending))]
Pending,
}
}

View File

@ -0,0 +1,45 @@
// 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.Net.Http;
using osu.Framework.IO.Network;
namespace osu.Game.Online.API.Requests
{
public class ReplaceBeatmapPackageRequest : APIUploadRequest
{
protected override string Uri
{
get
{
// can be removed once the service has been successfully deployed to production
if (API!.Endpoints.BeatmapSubmissionServiceUrl == null)
throw new NotSupportedException("Beatmap submission not supported in this configuration!");
return $@"{API!.Endpoints.BeatmapSubmissionServiceUrl}/beatmapsets/{BeatmapSetID}";
}
}
protected override string Target => throw new NotSupportedException();
public uint BeatmapSetID { get; }
private readonly byte[] oszPackage;
public ReplaceBeatmapPackageRequest(uint beatmapSetID, byte[] oszPackage)
{
this.oszPackage = oszPackage;
BeatmapSetID = beatmapSetID;
}
protected override WebRequest CreateWebRequest()
{
var request = base.CreateWebRequest();
request.AddFile(@"beatmapArchive", oszPackage);
request.Method = HttpMethod.Put;
request.Timeout = 60_000;
return request;
}
}
}

View File

@ -0,0 +1,30 @@
// 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 Newtonsoft.Json;
namespace osu.Game.Online.API.Requests.Responses
{
public class PutBeatmapSetResponse
{
[JsonProperty("beatmapset_id")]
public uint BeatmapSetId { get; set; }
[JsonProperty("beatmap_ids")]
public ICollection<uint> BeatmapIds { get; set; } = Array.Empty<uint>();
[JsonProperty("files")]
public ICollection<BeatmapSetFile> Files { get; set; } = Array.Empty<BeatmapSetFile>();
}
public struct BeatmapSetFile
{
[JsonProperty("filename")]
public string Filename { get; set; }
[JsonProperty("sha2_hash")]
public string SHA2Hash { get; set; }
}
}

View File

@ -49,12 +49,12 @@ namespace osu.Game.Online.Chat
if (url.StartsWith('/'))
{
url = $"{api.WebsiteRootUrl}{url}";
url = $"{api.Endpoints.WebsiteUrl}{url}";
isTrustedDomain = true;
}
else
{
isTrustedDomain = url.StartsWith(api.WebsiteRootUrl, StringComparison.Ordinal);
isTrustedDomain = url.StartsWith(api.Endpoints.WebsiteUrl, StringComparison.Ordinal);
}
if (!url.CheckIsValidUrl())

View File

@ -95,7 +95,7 @@ namespace osu.Game.Online.Chat
string getBeatmapPart()
{
return beatmapOnlineID > 0 ? $"[{api.WebsiteRootUrl}/b/{beatmapOnlineID} {beatmapDisplayTitle}]" : beatmapDisplayTitle;
return beatmapOnlineID > 0 ? $"[{api.Endpoints.WebsiteUrl}/b/{beatmapOnlineID} {beatmapDisplayTitle}]" : beatmapDisplayTitle;
}
string getRulesetPart()

View File

@ -7,12 +7,12 @@ namespace osu.Game.Online
{
public DevelopmentEndpointConfiguration()
{
WebsiteRootUrl = APIEndpointUrl = @"https://dev.ppy.sh";
WebsiteUrl = APIUrl = @"https://dev.ppy.sh";
APIClientSecret = @"3LP2mhUrV89xxzD1YKNndXHEhWWCRLPNKioZ9ymT";
APIClientID = "5";
SpectatorEndpointUrl = $@"{APIEndpointUrl}/signalr/spectator";
MultiplayerEndpointUrl = $@"{APIEndpointUrl}/signalr/multiplayer";
MetadataEndpointUrl = $@"{APIEndpointUrl}/signalr/metadata";
SpectatorUrl = $@"{APIUrl}/signalr/spectator";
MultiplayerUrl = $@"{APIUrl}/signalr/multiplayer";
MetadataUrl = $@"{APIUrl}/signalr/metadata";
}
}
}

View File

@ -8,16 +8,6 @@ namespace osu.Game.Online
/// </summary>
public class EndpointConfiguration
{
/// <summary>
/// The base URL for the website.
/// </summary>
public string WebsiteRootUrl { get; set; } = string.Empty;
/// <summary>
/// The endpoint for the main (osu-web) API.
/// </summary>
public string APIEndpointUrl { get; set; } = string.Empty;
/// <summary>
/// The OAuth client secret.
/// </summary>
@ -28,19 +18,34 @@ namespace osu.Game.Online
/// </summary>
public string APIClientID { get; set; } = string.Empty;
/// <summary>
/// The base URL for the website. Does not include a trailing slash.
/// </summary>
public string WebsiteUrl { get; set; } = string.Empty;
/// <summary>
/// The endpoint for the main (osu-web) API. Does not include a trailing slash.
/// </summary>
public string APIUrl { get; set; } = string.Empty;
/// <summary>
/// The root URL for the service handling beatmap submission. Does not include a trailing slash.
/// </summary>
public string? BeatmapSubmissionServiceUrl { get; set; }
/// <summary>
/// The endpoint for the SignalR spectator server.
/// </summary>
public string SpectatorEndpointUrl { get; set; } = string.Empty;
public string SpectatorUrl { get; set; } = string.Empty;
/// <summary>
/// The endpoint for the SignalR multiplayer server.
/// </summary>
public string MultiplayerEndpointUrl { get; set; } = string.Empty;
public string MultiplayerUrl { get; set; } = string.Empty;
/// <summary>
/// The endpoint for the SignalR metadata server.
/// </summary>
public string MetadataEndpointUrl { get; set; } = string.Empty;
public string MetadataUrl { get; set; } = string.Empty;
}
}

View File

@ -436,7 +436,7 @@ namespace osu.Game.Online.Leaderboards
items.Add(new OsuMenuItem("Use these mods", MenuItemType.Highlighted, () => songSelect.Mods.Value = Score.Mods));
if (Score.OnlineID > 0)
items.Add(new OsuMenuItem(CommonStrings.CopyLink, MenuItemType.Standard, () => clipboard?.SetText($@"{api.WebsiteRootUrl}/scores/{Score.OnlineID}")));
items.Add(new OsuMenuItem(CommonStrings.CopyLink, MenuItemType.Standard, () => clipboard?.SetText($@"{api.Endpoints.WebsiteUrl}/scores/{Score.OnlineID}")));
if (Score.Files.Count > 0)
{

View File

@ -3,9 +3,13 @@
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Online.API;
using osu.Game.Online.Multiplayer;
using osu.Game.Users;
namespace osu.Game.Online.Metadata
@ -14,6 +18,9 @@ namespace osu.Game.Online.Metadata
{
public abstract IBindable<bool> IsConnected { get; }
[Resolved]
private IAPIProvider api { get; set; } = null!;
#region Beatmap metadata updates
public abstract Task<BeatmapUpdates> GetChangesSince(int queueId);
@ -32,11 +39,6 @@ namespace osu.Game.Online.Metadata
#region User presence updates
/// <summary>
/// Whether the client is currently receiving user presence updates from the server.
/// </summary>
public abstract IBindable<bool> IsWatchingUserPresence { get; }
/// <summary>
/// The <see cref="UserPresence"/> information about the current user.
/// </summary>
@ -52,31 +54,91 @@ namespace osu.Game.Online.Metadata
/// </summary>
public abstract IBindableDictionary<int, UserPresence> FriendPresences { get; }
/// <inheritdoc/>
/// <summary>
/// Attempts to retrieve the presence of a user.
/// </summary>
/// <param name="userId">The user ID.</param>
/// <returns>The user presence, or null if not available or the user's offline.</returns>
public UserPresence? GetPresence(int userId)
{
if (userId == api.LocalUser.Value.OnlineID)
return LocalUserPresence;
if (FriendPresences.TryGetValue(userId, out UserPresence presence))
return presence;
if (UserPresences.TryGetValue(userId, out presence))
return presence;
return null;
}
public abstract Task UpdateActivity(UserActivity? activity);
/// <inheritdoc/>
public abstract Task UpdateStatus(UserStatus? status);
/// <inheritdoc/>
public abstract Task BeginWatchingUserPresence();
private int userPresenceWatchCount;
/// <inheritdoc/>
public abstract Task EndWatchingUserPresence();
protected bool IsWatchingUserPresence
=> Interlocked.CompareExchange(ref userPresenceWatchCount, userPresenceWatchCount, userPresenceWatchCount) > 0;
/// <summary>
/// Signals to the server that we want to begin receiving status updates for all users.
/// </summary>
/// <returns>An <see cref="IDisposable"/> which will end the session when disposed.</returns>
public IDisposable BeginWatchingUserPresence() => new UserPresenceWatchToken(this);
Task IMetadataServer.BeginWatchingUserPresence()
{
if (Interlocked.Increment(ref userPresenceWatchCount) == 1)
return BeginWatchingUserPresenceInternal();
return Task.CompletedTask;
}
Task IMetadataServer.EndWatchingUserPresence()
{
if (Interlocked.Decrement(ref userPresenceWatchCount) == 0)
return EndWatchingUserPresenceInternal();
return Task.CompletedTask;
}
protected abstract Task BeginWatchingUserPresenceInternal();
protected abstract Task EndWatchingUserPresenceInternal();
/// <inheritdoc/>
public abstract Task UserPresenceUpdated(int userId, UserPresence? presence);
/// <inheritdoc/>
public abstract Task FriendPresenceUpdated(int userId, UserPresence? presence);
private class UserPresenceWatchToken : IDisposable
{
private readonly IMetadataServer server;
private bool isDisposed;
public UserPresenceWatchToken(IMetadataServer server)
{
this.server = server;
server.BeginWatchingUserPresence().FireAndForget();
}
public void Dispose()
{
if (isDisposed)
return;
server.EndWatchingUserPresence().FireAndForget();
isDisposed = true;
}
}
#endregion
#region Daily Challenge
public abstract IBindable<DailyChallengeInfo?> DailyChallengeInfo { get; }
/// <inheritdoc/>
public abstract Task DailyChallengeUpdated(DailyChallengeInfo? info);
#endregion

View File

@ -20,9 +20,6 @@ namespace osu.Game.Online.Metadata
{
public override IBindable<bool> IsConnected { get; } = new Bindable<bool>();
public override IBindable<bool> IsWatchingUserPresence => isWatchingUserPresence;
private readonly BindableBool isWatchingUserPresence = new BindableBool();
public override UserPresence LocalUserPresence => localUserPresence;
private UserPresence localUserPresence;
@ -50,7 +47,7 @@ namespace osu.Game.Online.Metadata
public OnlineMetadataClient(EndpointConfiguration endpoints)
{
endpoint = endpoints.MetadataEndpointUrl;
endpoint = endpoints.MetadataUrl;
}
[BackgroundDependencyLoader]
@ -109,15 +106,18 @@ namespace osu.Game.Online.Metadata
{
Schedule(() =>
{
isWatchingUserPresence.Value = false;
userPresences.Clear();
friendPresences.Clear();
dailyChallengeInfo.Value = null;
localUserPresence = default;
});
return;
}
if (IsWatchingUserPresence)
BeginWatchingUserPresenceInternal();
if (localUser.Value is not GuestUser)
{
UpdateActivity(userActivity.Value);
@ -201,6 +201,31 @@ namespace osu.Game.Online.Metadata
return connection.InvokeAsync(nameof(IMetadataServer.UpdateStatus), status);
}
protected override Task BeginWatchingUserPresenceInternal()
{
if (connector?.IsConnected.Value != true)
return Task.CompletedTask;
Logger.Log($@"{nameof(OnlineMetadataClient)} began watching user presence", LoggingTarget.Network);
Debug.Assert(connection != null);
return connection.InvokeAsync(nameof(IMetadataServer.BeginWatchingUserPresence));
}
protected override Task EndWatchingUserPresenceInternal()
{
if (connector?.IsConnected.Value != true)
return Task.CompletedTask;
Logger.Log($@"{nameof(OnlineMetadataClient)} stopped watching user presence", LoggingTarget.Network);
// must be scheduled before any remote calls to avoid mis-ordering.
Schedule(() => userPresences.Clear());
Debug.Assert(connection != null);
return connection.InvokeAsync(nameof(IMetadataServer.EndWatchingUserPresence));
}
public override Task UserPresenceUpdated(int userId, UserPresence? presence)
{
Schedule(() =>
@ -237,36 +262,6 @@ namespace osu.Game.Online.Metadata
return Task.CompletedTask;
}
public override async Task BeginWatchingUserPresence()
{
if (connector?.IsConnected.Value != true)
throw new OperationCanceledException();
Debug.Assert(connection != null);
await connection.InvokeAsync(nameof(IMetadataServer.BeginWatchingUserPresence)).ConfigureAwait(false);
Schedule(() => isWatchingUserPresence.Value = true);
Logger.Log($@"{nameof(OnlineMetadataClient)} began watching user presence", LoggingTarget.Network);
}
public override async Task EndWatchingUserPresence()
{
try
{
if (connector?.IsConnected.Value != true)
throw new OperationCanceledException();
// must be scheduled before any remote calls to avoid mis-ordering.
Schedule(() => userPresences.Clear());
Debug.Assert(connection != null);
await connection.InvokeAsync(nameof(IMetadataServer.EndWatchingUserPresence)).ConfigureAwait(false);
Logger.Log($@"{nameof(OnlineMetadataClient)} stopped watching user presence", LoggingTarget.Network);
}
finally
{
Schedule(() => isWatchingUserPresence.Value = false);
}
}
public override Task DailyChallengeUpdated(DailyChallengeInfo? info)
{
Schedule(() => dailyChallengeInfo.Value = info);

View File

@ -32,7 +32,7 @@ namespace osu.Game.Online.Multiplayer
public OnlineMultiplayerClient(EndpointConfiguration endpoints)
{
endpoint = endpoints.MultiplayerEndpointUrl;
endpoint = endpoints.MultiplayerUrl;
}
[BackgroundDependencyLoader]

View File

@ -7,12 +7,12 @@ namespace osu.Game.Online
{
public ProductionEndpointConfiguration()
{
WebsiteRootUrl = APIEndpointUrl = @"https://osu.ppy.sh";
WebsiteUrl = APIUrl = @"https://osu.ppy.sh";
APIClientSecret = @"FGc9GAtyHzeQDshWP5Ah7dega8hJACAJpQtw6OXk";
APIClientID = "5";
SpectatorEndpointUrl = "https://spectator.ppy.sh/spectator";
MultiplayerEndpointUrl = "https://spectator.ppy.sh/multiplayer";
MetadataEndpointUrl = "https://spectator.ppy.sh/metadata";
SpectatorUrl = "https://spectator.ppy.sh/spectator";
MultiplayerUrl = "https://spectator.ppy.sh/multiplayer";
MetadataUrl = "https://spectator.ppy.sh/metadata";
}
}
}

View File

@ -24,7 +24,7 @@ namespace osu.Game.Online.Spectator
public OnlineSpectatorClient(EndpointConfiguration endpoints)
{
endpoint = endpoints.SpectatorEndpointUrl;
endpoint = endpoints.SpectatorUrl;
}
[BackgroundDependencyLoader]

View File

@ -295,7 +295,7 @@ namespace osu.Game
EndpointConfiguration endpoints = CreateEndpoints();
MessageFormatter.WebsiteRootUrl = endpoints.WebsiteRootUrl;
MessageFormatter.WebsiteRootUrl = endpoints.WebsiteUrl;
frameworkLocale = frameworkConfig.GetBindable<string>(FrameworkSetting.Locale);
frameworkLocale.BindValueChanged(_ => updateLanguage());

View File

@ -419,7 +419,7 @@ namespace osu.Game.Overlays.Comments
private void copyUrl()
{
clipboard.SetText($@"{api.APIEndpointUrl}/comments/{Comment.Id}");
clipboard.SetText($@"{api.Endpoints.APIUrl}/comments/{Comment.Id}");
onScreenDisplay?.Display(new CopyUrlToast());
}

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics;
@ -40,17 +38,20 @@ namespace osu.Game.Overlays.Dashboard
private readonly IBindableDictionary<int, UserPresence> onlineUserPresences = new BindableDictionary<int, UserPresence>();
private readonly Dictionary<int, OnlineUserPanel> userPanels = new Dictionary<int, OnlineUserPanel>();
private SearchContainer<OnlineUserPanel> userFlow;
private BasicSearchTextBox searchTextBox;
private SearchContainer<OnlineUserPanel> userFlow = null!;
private BasicSearchTextBox searchTextBox = null!;
[Resolved]
private IAPIProvider api { get; set; }
private IAPIProvider api { get; set; } = null!;
[Resolved]
private SpectatorClient spectatorClient { get; set; }
private SpectatorClient spectatorClient { get; set; } = null!;
[Resolved]
private MetadataClient metadataClient { get; set; }
private MetadataClient metadataClient { get; set; } = null!;
[Resolved]
private UserLookupCache users { get; set; } = null!;
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
@ -99,9 +100,6 @@ namespace osu.Game.Overlays.Dashboard
searchTextBox.Current.ValueChanged += text => userFlow.SearchTerm = text.NewValue;
}
[Resolved]
private UserLookupCache users { get; set; }
protected override void LoadComplete()
{
base.LoadComplete();
@ -120,7 +118,7 @@ namespace osu.Game.Overlays.Dashboard
searchTextBox.TakeFocus();
}
private void onUserPresenceUpdated(object sender, NotifyDictionaryChangedEventArgs<int, UserPresence> e) => Schedule(() =>
private void onUserPresenceUpdated(object? sender, NotifyDictionaryChangedEventArgs<int, UserPresence> e) => Schedule(() =>
{
switch (e.Action)
{
@ -133,40 +131,13 @@ namespace osu.Game.Overlays.Dashboard
users.GetUserAsync(userId).ContinueWith(task =>
{
APIUser user = task.GetResultSafely();
if (user == null)
return;
Schedule(() =>
{
userFlow.Add(userPanels[userId] = createUserPanel(user).With(p =>
{
var presence = onlineUserPresences.GetValueOrDefault(userId);
p.Status.Value = presence.Status;
p.Activity.Value = presence.Activity;
}));
});
if (task.GetResultSafely() is APIUser user)
Schedule(() => userFlow.Add(userPanels[userId] = createUserPanel(user)));
});
}
break;
case NotifyDictionaryChangedAction.Replace:
Debug.Assert(e.NewItems != null);
foreach (var kvp in e.NewItems)
{
if (userPanels.TryGetValue(kvp.Key, out var panel))
{
panel.Activity.Value = kvp.Value.Activity;
panel.Status.Value = kvp.Value.Status;
}
}
break;
case NotifyDictionaryChangedAction.Remove:
Debug.Assert(e.OldItems != null);
@ -181,7 +152,7 @@ namespace osu.Game.Overlays.Dashboard
}
});
private void onPlayingUsersChanged(object sender, NotifyCollectionChangedEventArgs e)
private void onPlayingUsersChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
switch (e.Action)
{
@ -221,15 +192,12 @@ namespace osu.Game.Overlays.Dashboard
{
public readonly APIUser User;
public readonly Bindable<UserStatus?> Status = new Bindable<UserStatus?>();
public readonly Bindable<UserActivity> Activity = new Bindable<UserActivity>();
public BindableBool CanSpectate { get; } = new BindableBool();
public IEnumerable<LocalisableString> FilterTerms { get; }
[Resolved(canBeNull: true)]
private IPerformFromScreenRunner performer { get; set; }
[Resolved]
private IPerformFromScreenRunner? performer { get; set; }
public bool FilteringActive { set; get; }
@ -270,10 +238,7 @@ namespace osu.Game.Overlays.Dashboard
{
RelativeSizeAxes = Axes.X,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
// this is SHOCKING
Activity = { BindTarget = Activity },
Status = { BindTarget = Status },
Origin = Anchor.TopCentre
},
new PurpleRoundedButton
{

View File

@ -6,7 +6,6 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Containers;
using osu.Game.Online.Metadata;
using osu.Game.Online.Multiplayer;
using osu.Game.Overlays.Dashboard;
using osu.Game.Overlays.Dashboard.Friends;
@ -18,6 +17,7 @@ namespace osu.Game.Overlays
private MetadataClient metadataClient { get; set; } = null!;
private IBindable<bool> metadataConnected = null!;
private IDisposable? userPresenceWatchToken;
public DashboardOverlay()
: base(OverlayColourScheme.Purple)
@ -61,9 +61,12 @@ namespace osu.Game.Overlays
return;
if (State.Value == Visibility.Visible)
metadataClient.BeginWatchingUserPresence().FireAndForget();
userPresenceWatchToken ??= metadataClient.BeginWatchingUserPresence();
else
metadataClient.EndWatchingUserPresence().FireAndForget();
{
userPresenceWatchToken?.Dispose();
userPresenceWatchToken = null;
}
}
}
}

View File

@ -65,6 +65,8 @@ namespace osu.Game.Overlays.FirstRunSetup
};
}
public override LocalisableString? NextStepText => FirstRunSetupOverlayStrings.GetStarted;
private partial class LanguageSelectionFlow : FillFlowContainer
{
private Bindable<Language> language = null!;

View File

@ -129,7 +129,7 @@ namespace osu.Game.Overlays.Login
}
};
forgottenPasswordLink.AddLink(LayoutStrings.PopupLoginLoginForgot, $"{api.WebsiteRootUrl}/home/password-reset");
forgottenPasswordLink.AddLink(LayoutStrings.PopupLoginLoginForgot, $"{api.Endpoints.WebsiteUrl}/home/password-reset");
password.OnCommit += (_, _) => performLogin();

View File

@ -98,7 +98,7 @@ namespace osu.Game.Overlays.Login
explainText.AddParagraph(UserVerificationStrings.BoxInfoCheckSpam);
// We can't support localisable strings with nested links yet. Not sure if we even can (probably need to allow markdown link formatting or something).
explainText.AddParagraph("If you can't access your email or have forgotten what you used, please follow the ");
explainText.AddLink(UserVerificationStrings.BoxInfoRecoverLink, $"{api.WebsiteRootUrl}/home/password-reset");
explainText.AddLink(UserVerificationStrings.BoxInfoRecoverLink, $"{api.Endpoints.WebsiteUrl}/home/password-reset");
explainText.AddText(". You can also ");
explainText.AddLink(UserVerificationStrings.BoxInfoReissueLink, () =>
{

View File

@ -124,12 +124,12 @@ namespace osu.Game.Overlays.Profile.Header
}
topLinkContainer.AddText("Contributed ");
topLinkContainer.AddLink("forum post".ToQuantity(user.PostCount, "#,##0"), $"{api.WebsiteRootUrl}/users/{user.Id}/posts", creationParameters: embolden);
topLinkContainer.AddLink("forum post".ToQuantity(user.PostCount, "#,##0"), $"{api.Endpoints.WebsiteUrl}/users/{user.Id}/posts", creationParameters: embolden);
addSpacer(topLinkContainer);
topLinkContainer.AddText("Posted ");
topLinkContainer.AddLink("comment".ToQuantity(user.CommentsCount, "#,##0"), $"{api.WebsiteRootUrl}/comments?user_id={user.Id}", creationParameters: embolden);
topLinkContainer.AddLink("comment".ToQuantity(user.CommentsCount, "#,##0"), $"{api.Endpoints.WebsiteUrl}/comments?user_id={user.Id}", creationParameters: embolden);
string websiteWithoutProtocol = user.Website;

View File

@ -33,7 +33,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
Texture = textures.Get(banner.Image),
};
Action = () => game?.OpenUrlExternally($@"{api.WebsiteRootUrl}/community/tournaments/{banner.TournamentId}");
Action = () => game?.OpenUrlExternally($@"{api.Endpoints.WebsiteUrl}/community/tournaments/{banner.TournamentId}");
}
protected override void LoadComplete()

View File

@ -213,7 +213,7 @@ namespace osu.Game.Overlays.Profile.Header
cover.User = user;
avatar.User = user;
usernameText.Text = user?.Username ?? string.Empty;
openUserExternally.Link = $@"{api.WebsiteRootUrl}/users/{user?.Id ?? 0}";
openUserExternally.Link = $@"{api.Endpoints.WebsiteUrl}/users/{user?.Id ?? 0}";
userFlag.CountryCode = user?.CountryCode ?? default;
userCountryText.Text = (user?.CountryCode ?? default).GetDescription();
userCountryContainer.Action = () => rankingsOverlay?.ShowCountry(user?.CountryCode ?? default);

View File

@ -223,7 +223,7 @@ namespace osu.Game.Overlays.Profile.Sections.Recent
private void addBeatmapsetLink()
=> content.AddLink(activity.Beatmapset.AsNonNull().Title, LinkAction.OpenBeatmapSet, getLinkArgument(activity.Beatmapset.AsNonNull().Url), creationParameters: t => t.Font = getLinkFont());
private object getLinkArgument(string url) => MessageFormatter.GetLinkDetails($"{api.WebsiteRootUrl}{url}").Argument.AsNonNull();
private object getLinkArgument(string url) => MessageFormatter.GetLinkDetails($"{api.Endpoints.WebsiteUrl}{url}").Argument.AsNonNull();
private FontUsage getLinkFont(FontWeight fontWeight = FontWeight.Regular)
=> OsuFont.GetFont(size: font_size, weight: fontWeight, italics: true);

View File

@ -54,7 +54,7 @@ namespace osu.Game.Overlays.Wiki
Padding = new MarginPadding(padding),
Child = new WikiPanelMarkdownContainer(isFullWidth)
{
CurrentPath = $@"{api.WebsiteRootUrl}/wiki/",
CurrentPath = $@"{api.Endpoints.WebsiteUrl}/wiki/",
Text = text,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y

View File

@ -167,7 +167,7 @@ namespace osu.Game.Overlays
}
else
{
LoadDisplay(articlePage = new WikiArticlePage($@"{api.WebsiteRootUrl}/wiki/{path.Value}/", response.Markdown));
LoadDisplay(articlePage = new WikiArticlePage($@"{api.Endpoints.WebsiteUrl}/wiki/{path.Value}/", response.Markdown));
}
}
@ -176,7 +176,7 @@ namespace osu.Game.Overlays
wikiData.Value = null;
path.Value = "error";
LoadDisplay(articlePage = new WikiArticlePage($@"{api.WebsiteRootUrl}/wiki/",
LoadDisplay(articlePage = new WikiArticlePage($@"{api.Endpoints.WebsiteUrl}/wiki/",
$"Something went wrong when trying to fetch page \"{originalPath}\".\n\n[Return to the main page]({INDEX_PATH})."));
}

View File

@ -227,7 +227,7 @@ namespace osu.Game.Overlays
updateButtons();
}
private void updateButtons() => DisplayedFooterContent?.UpdateButtons(CurrentStepIndex, steps);
private void updateButtons() => DisplayedFooterContent?.UpdateButtons(CurrentStepIndex, CurrentScreen, steps);
public partial class WizardFooterContent : VisibilityContainer
{
@ -248,24 +248,23 @@ namespace osu.Game.Overlays
RelativeSizeAxes = Axes.X,
Width = 1,
Text = FirstRunSetupOverlayStrings.GetStarted,
DarkerColour = colourProvider.Colour2,
LighterColour = colourProvider.Colour1,
DarkerColour = colourProvider.Colour3,
LighterColour = colourProvider.Colour2,
Action = () => ShowNextStep?.Invoke(),
};
}
public void UpdateButtons(int? currentStep, IReadOnlyList<Type> steps)
public void UpdateButtons(int? currentStep, WizardScreen? currentScreen, IReadOnlyList<Type> steps)
{
NextButton.Enabled.Value = currentStep != null;
if (currentStep == null)
return;
bool isFirstStep = currentStep == 0;
bool isLastStep = currentStep == steps.Count - 1;
if (isFirstStep)
NextButton.Text = FirstRunSetupOverlayStrings.GetStarted;
if (currentScreen?.NextStepText != null)
NextButton.Text = currentScreen.NextStepText.Value;
else
{
NextButton.Text = isLastStep

View File

@ -7,6 +7,7 @@ using osu.Framework.Allocation;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Localisation;
using osu.Framework.Screens;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
@ -102,5 +103,7 @@ namespace osu.Game.Overlays
base.OnSuspending(e);
}
public virtual LocalisableString? NextStepText => null;
}
}

View File

@ -265,57 +265,38 @@ namespace osu.Game.Rulesets.Edit
#region IDistanceSnapProvider
public virtual float GetBeatSnapDistanceAt(HitObject referenceObject, bool useReferenceSliderVelocity = true)
public virtual float GetBeatSnapDistance(IHasSliderVelocity? withVelocity = null)
{
return (float)(100 * (useReferenceSliderVelocity && referenceObject is IHasSliderVelocity hasSliderVelocity ? hasSliderVelocity.SliderVelocityMultiplier : 1) * editorBeatmap.Difficulty.SliderMultiplier * 1
return (float)(100 * (withVelocity?.SliderVelocityMultiplier ?? 1) * editorBeatmap.Difficulty.SliderMultiplier * 1
/ beatSnapProvider.BeatDivisor);
}
public virtual float DurationToDistance(HitObject referenceObject, double duration)
public virtual float DurationToDistance(double duration, double timingReference, IHasSliderVelocity? withVelocity = null)
{
double beatLength = beatSnapProvider.GetBeatLengthAtTime(referenceObject.StartTime);
return (float)(duration / beatLength * GetBeatSnapDistanceAt(referenceObject));
double beatLength = beatSnapProvider.GetBeatLengthAtTime(timingReference);
return (float)(duration / beatLength * GetBeatSnapDistance(withVelocity));
}
public virtual double DistanceToDuration(HitObject referenceObject, float distance)
public virtual double DistanceToDuration(float distance, double timingReference, IHasSliderVelocity? withVelocity = null)
{
double beatLength = beatSnapProvider.GetBeatLengthAtTime(referenceObject.StartTime);
return distance / GetBeatSnapDistanceAt(referenceObject) * beatLength;
double beatLength = beatSnapProvider.GetBeatLengthAtTime(timingReference);
return distance / GetBeatSnapDistance(withVelocity) * beatLength;
}
public virtual double FindSnappedDuration(HitObject referenceObject, float distance)
=> beatSnapProvider.SnapTime(referenceObject.StartTime + DistanceToDuration(referenceObject, distance), referenceObject.StartTime) - referenceObject.StartTime;
public virtual float FindSnappedDistance(HitObject referenceObject, float distance, DistanceSnapTarget target)
public virtual float FindSnappedDistance(float distance, double snapReferenceTime, IHasSliderVelocity? withVelocity = null)
{
double referenceTime;
double actualDuration = snapReferenceTime + DistanceToDuration(distance, snapReferenceTime, withVelocity);
switch (target)
{
case DistanceSnapTarget.Start:
referenceTime = referenceObject.StartTime;
break;
double snappedTime = beatSnapProvider.SnapTime(actualDuration, snapReferenceTime);
case DistanceSnapTarget.End:
referenceTime = referenceObject.GetEndTime();
break;
default:
throw new ArgumentOutOfRangeException(nameof(target), target, $"Unknown {nameof(DistanceSnapTarget)} value");
}
double actualDuration = referenceTime + DistanceToDuration(referenceObject, distance);
double snappedTime = beatSnapProvider.SnapTime(actualDuration, referenceTime);
double beatLength = beatSnapProvider.GetBeatLengthAtTime(referenceTime);
double beatLength = beatSnapProvider.GetBeatLengthAtTime(snapReferenceTime);
// we don't want to exceed the actual duration and snap to a point in the future.
// as we are snapping to beat length via SnapTime (which will round-to-nearest), check for snapping in the forward direction and reverse it.
if (snappedTime > actualDuration + 1)
snappedTime -= beatLength;
return DurationToDistance(referenceObject, snappedTime - referenceTime);
return DurationToDistance(snappedTime - snapReferenceTime, snapReferenceTime, withVelocity);
}
#endregion

View File

@ -4,7 +4,7 @@
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
namespace osu.Game.Rulesets.Edit
{
@ -22,53 +22,63 @@ namespace osu.Game.Rulesets.Edit
Bindable<double> DistanceSpacingMultiplier { get; }
/// <summary>
/// Retrieves the distance between two points within a timing point that are one beat length apart.
/// Returns the spatial distance between objects which are temporally one beat apart.
/// Depends on:
/// <list type="bullet">
/// <item>the slider velocity taken from <paramref name="withVelocity"/>,</item>
/// <item>the beatmap's <see cref="IBeatmapDifficultyInfo.SliderMultiplier"/>,</item>,
/// <item>the current beat divisor.</item>
/// </list>
/// Note that the returned value does <b>NOT</b> depend on <see cref="DistanceSpacingMultiplier"/>;
/// consumers are expected to include that multiplier as they see fit.
/// </summary>
/// <param name="referenceObject">An object to be used as a reference point for this operation.</param>
/// <param name="useReferenceSliderVelocity">Whether the <paramref name="referenceObject"/>'s slider velocity should be factored into the returned distance.</param>
/// <returns>The distance between two points residing in the timing point that are one beat length apart.</returns>
float GetBeatSnapDistanceAt(HitObject referenceObject, bool useReferenceSliderVelocity = true);
float GetBeatSnapDistance(IHasSliderVelocity? withVelocity = null);
/// <summary>
/// Converts a duration to a distance without applying any snapping.
/// Converts a temporal duration into a spatial distance.
/// Does not perform any snapping.
/// Depends on:
/// <list type="bullet">
/// <item>the <paramref name="duration"/> provided,</item>
/// <item>a <paramref name="timingReference"/> used to retrieve the beat length of the beatmap at that time,</item>
/// <item>the slider velocity taken from <paramref name="withVelocity"/>,</item>
/// <item>the beatmap's <see cref="IBeatmapDifficultyInfo.SliderMultiplier"/>,</item>,
/// <item>the current beat divisor.</item>
/// </list>
/// Note that the returned value does <b>NOT</b> depend on <see cref="DistanceSpacingMultiplier"/>;
/// consumers are expected to include that multiplier as they see fit.
/// </summary>
/// <param name="referenceObject">An object to be used as a reference point for this operation.</param>
/// <param name="duration">The duration to convert.</param>
/// <returns>A value that represents <paramref name="duration"/> as a distance in the timing point.</returns>
float DurationToDistance(HitObject referenceObject, double duration);
float DurationToDistance(double duration, double timingReference, IHasSliderVelocity? withVelocity = null);
/// <summary>
/// Converts a distance to a duration without applying any snapping.
/// Converts a spatial distance into a temporal duration.
/// Does not perform any snapping.
/// Depends on:
/// <list type="bullet">
/// <item>the <paramref name="distance"/> provided,</item>
/// <item>a <paramref name="timingReference"/> used to retrieve the beat length of the beatmap at that time,</item>
/// <item>the slider velocity taken from <paramref name="withVelocity"/>,</item>
/// <item>the beatmap's <see cref="IBeatmapDifficultyInfo.SliderMultiplier"/>,</item>,
/// <item>the current beat divisor.</item>
/// </list>
/// Note that the returned value does <b>NOT</b> depend on <see cref="DistanceSpacingMultiplier"/>;
/// consumers are expected to include that multiplier as they see fit.
/// </summary>
/// <param name="referenceObject">An object to be used as a reference point for this operation.</param>
/// <param name="distance">The distance to convert.</param>
/// <returns>A value that represents <paramref name="distance"/> as a duration in the timing point.</returns>
double DistanceToDuration(HitObject referenceObject, float distance);
double DistanceToDuration(float distance, double timingReference, IHasSliderVelocity? withVelocity = null);
/// <summary>
/// Given a distance from the provided hit object, find the valid snapped duration.
/// Snaps a spatial distance to the beat, relative to <paramref name="snapReferenceTime"/>.
/// Depends on:
/// <list type="bullet">
/// <item>the <paramref name="distance"/> provided,</item>
/// <item>a <paramref name="snapReferenceTime"/> used to retrieve the beat length of the beatmap at that time,</item>
/// <item>the slider velocity taken from <paramref name="withVelocity"/>,</item>
/// <item>the beatmap's <see cref="IBeatmapDifficultyInfo.SliderMultiplier"/>,</item>,
/// <item>the current beat divisor.</item>
/// </list>
/// Note that the returned value does <b>NOT</b> depend on <see cref="DistanceSpacingMultiplier"/>;
/// consumers are expected to include that multiplier as they see fit.
/// </summary>
/// <param name="referenceObject">An object to be used as a reference point for this operation.</param>
/// <param name="distance">The distance to convert.</param>
/// <returns>A value that represents <paramref name="distance"/> as a duration snapped to the closest beat of the timing point.</returns>
double FindSnappedDuration(HitObject referenceObject, float distance);
/// <summary>
/// Given a distance from the provided hit object, find the valid snapped distance.
/// </summary>
/// <param name="referenceObject">An object to be used as a reference point for this operation.</param>
/// <param name="distance">The distance to convert.</param>
/// <param name="target">Whether the distance measured should be from the start or the end of <paramref name="referenceObject"/>.</param>
/// <returns>
/// A value that represents <paramref name="distance"/> snapped to the closest beat of the timing point.
/// The distance will always be less than or equal to the provided <paramref name="distance"/>.
/// </returns>
float FindSnappedDistance(HitObject referenceObject, float distance, DistanceSnapTarget target);
}
public enum DistanceSnapTarget
{
Start,
End,
float FindSnappedDistance(float distance, double snapReferenceTime, IHasSliderVelocity? withVelocity = null);
}
}

View File

@ -15,9 +15,9 @@ namespace osu.Game.Rulesets.Objects
/// Snaps the provided <paramref name="hitObject"/>'s duration using the <paramref name="snapProvider"/>.
/// </summary>
public static void SnapTo<THitObject>(this THitObject hitObject, IDistanceSnapProvider? snapProvider)
where THitObject : HitObject, IHasPath
where THitObject : HitObject, IHasPath, IHasSliderVelocity
{
hitObject.Path.ExpectedDistance.Value = snapProvider?.FindSnappedDistance(hitObject, (float)hitObject.Path.CalculatedDistance, DistanceSnapTarget.Start) ?? hitObject.Path.CalculatedDistance;
hitObject.Path.ExpectedDistance.Value = snapProvider?.FindSnappedDistance((float)hitObject.Path.CalculatedDistance, hitObject.StartTime, hitObject) ?? hitObject.Path.CalculatedDistance;
}
/// <summary>

View File

@ -0,0 +1,151 @@
// 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.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings;
using osu.Game.Localisation;
using osu.Game.Overlays;
using osu.Game.Screens.Edit.Components.Menus;
namespace osu.Game.Screens.Edit
{
public partial class BookmarkController : Component, IKeyBindingHandler<GlobalAction>
{
/// <summary>
/// Bookmarks menu item (with submenu containing options). Should be added to the <see cref="Editor"/>'s global menu.
/// </summary>
public EditorMenuItem Menu { get; private set; }
[Resolved]
private EditorClock clock { get; set; } = null!;
[Resolved]
private EditorBeatmap editorBeatmap { get; set; } = null!;
[Resolved]
private IDialogOverlay? dialogOverlay { get; set; }
private readonly BindableList<int> bookmarks = new BindableList<int>();
private readonly EditorMenuItem removeBookmarkMenuItem;
private readonly EditorMenuItem seekToPreviousBookmarkMenuItem;
private readonly EditorMenuItem seekToNextBookmarkMenuItem;
private readonly EditorMenuItem resetBookmarkMenuItem;
public BookmarkController()
{
Menu = new EditorMenuItem(EditorStrings.Bookmarks)
{
Items = new MenuItem[]
{
new EditorMenuItem(EditorStrings.AddBookmark, MenuItemType.Standard, addBookmarkAtCurrentTime)
{
Hotkey = new Hotkey(GlobalAction.EditorAddBookmark),
},
removeBookmarkMenuItem = new EditorMenuItem(EditorStrings.RemoveClosestBookmark, MenuItemType.Destructive, removeClosestBookmark)
{
Hotkey = new Hotkey(GlobalAction.EditorRemoveClosestBookmark)
},
seekToPreviousBookmarkMenuItem = new EditorMenuItem(EditorStrings.SeekToPreviousBookmark, MenuItemType.Standard, () => seekBookmark(-1))
{
Hotkey = new Hotkey(GlobalAction.EditorSeekToPreviousBookmark)
},
seekToNextBookmarkMenuItem = new EditorMenuItem(EditorStrings.SeekToNextBookmark, MenuItemType.Standard, () => seekBookmark(1))
{
Hotkey = new Hotkey(GlobalAction.EditorSeekToNextBookmark)
},
resetBookmarkMenuItem = new EditorMenuItem(EditorStrings.ResetBookmarks, MenuItemType.Destructive, () => dialogOverlay?.Push(new BookmarkResetDialog(editorBeatmap)))
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
bookmarks.BindTo(editorBeatmap.Bookmarks);
}
protected override void Update()
{
base.Update();
bool hasAnyBookmark = bookmarks.Count > 0;
bool hasBookmarkCloseEnoughForDeletion = bookmarks.Any(b => Math.Abs(b - clock.CurrentTimeAccurate) < 2000);
removeBookmarkMenuItem.Action.Disabled = !hasBookmarkCloseEnoughForDeletion;
seekToPreviousBookmarkMenuItem.Action.Disabled = !hasAnyBookmark;
seekToNextBookmarkMenuItem.Action.Disabled = !hasAnyBookmark;
resetBookmarkMenuItem.Action.Disabled = !hasAnyBookmark;
}
private void addBookmarkAtCurrentTime()
{
int bookmark = (int)clock.CurrentTimeAccurate;
int idx = bookmarks.BinarySearch(bookmark);
if (idx < 0)
bookmarks.Insert(~idx, bookmark);
}
private void removeClosestBookmark()
{
if (removeBookmarkMenuItem.Action.Disabled)
return;
int closestBookmark = bookmarks.MinBy(b => Math.Abs(b - clock.CurrentTimeAccurate));
bookmarks.Remove(closestBookmark);
}
private void seekBookmark(int direction)
{
int? targetBookmark = direction < 1
? bookmarks.Cast<int?>().LastOrDefault(b => b < clock.CurrentTimeAccurate)
: bookmarks.Cast<int?>().FirstOrDefault(b => b > clock.CurrentTimeAccurate);
if (targetBookmark != null)
clock.SeekSmoothlyTo(targetBookmark.Value);
}
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{
switch (e.Action)
{
case GlobalAction.EditorSeekToPreviousBookmark:
seekBookmark(-1);
return true;
case GlobalAction.EditorSeekToNextBookmark:
seekBookmark(1);
return true;
}
if (e.Repeat)
return false;
switch (e.Action)
{
case GlobalAction.EditorAddBookmark:
addBookmarkAtCurrentTime();
return true;
case GlobalAction.EditorRemoveClosestBookmark:
removeClosestBookmark();
return true;
}
return false;
}
public void OnReleased(KeyBindingReleaseEvent<GlobalAction> e)
{
}
}
}

View File

@ -0,0 +1,26 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Game.Overlays.Dialog;
namespace osu.Game.Screens.Edit
{
public partial class BookmarkResetDialog : DeletionDialog
{
private readonly EditorBeatmap editor;
public BookmarkResetDialog(EditorBeatmap editorBeatmap)
{
editor = editorBeatmap;
BodyText = "All Bookmarks";
}
[BackgroundDependencyLoader]
private void load()
{
DangerousAction = () => editor.Bookmarks.Clear();
}
}
}

View File

@ -18,6 +18,7 @@ namespace osu.Game.Screens.Edit.Components
public partial class TimeInfoContainer : BottomBarContainer
{
private OsuSpriteText bpm = null!;
private OsuSpriteText progress = null!;
[Resolved]
private EditorBeatmap editorBeatmap { get; set; } = null!;
@ -36,26 +37,44 @@ namespace osu.Game.Screens.Edit.Components
bpm = new OsuSpriteText
{
Colour = colours.Orange1,
Font = OsuFont.Torus.With(size: 14, weight: FontWeight.SemiBold, fixedWidth: true),
Spacing = new Vector2(-1, 0),
Position = new Vector2(0, 4),
Anchor = Anchor.CentreRight,
Origin = Anchor.TopRight,
},
progress = new OsuSpriteText
{
Colour = colours.Purple1,
Font = OsuFont.Torus.With(size: 14, weight: FontWeight.SemiBold, fixedWidth: true),
Spacing = new Vector2(-1, 0),
Anchor = Anchor.CentreLeft,
Font = OsuFont.Torus.With(size: 14, weight: FontWeight.SemiBold),
Position = new Vector2(2, 4),
}
};
}
private double? lastBPM;
private double? lastProgress;
protected override void Update()
{
base.Update();
double newBPM = editorBeatmap.ControlPointInfo.TimingPointAt(editorClock.CurrentTime).BPM;
double newProgress = (int)(editorClock.CurrentTime / editorClock.TrackLength * 100);
if (lastBPM != newBPM)
{
lastBPM = newBPM;
bpm.Text = @$"{newBPM:0} BPM";
}
if (lastProgress != newProgress)
{
lastProgress = newProgress;
progress.Text = @$"{newProgress:0}%";
}
}
private partial class TimestampControl : OsuClickableContainer

View File

@ -4,6 +4,7 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
@ -14,6 +15,9 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary
{
public partial class TestGameplayButton : OsuButton
{
[Resolved]
private OsuColour colours { get; set; } = null!;
protected override SpriteText CreateText() => new OsuSpriteText
{
Depth = -1,
@ -24,7 +28,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary
};
[BackgroundDependencyLoader]
private void load(OsuColour colours, OverlayColourProvider colourProvider)
private void load(OverlayColourProvider colourProvider)
{
BackgroundColour = colours.Orange1;
SpriteText.Colour = colourProvider.Background6;
@ -33,5 +37,18 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary
Text = EditorStrings.TestBeatmap;
}
protected override bool OnMouseDown(MouseDownEvent e)
{
Background.FadeColour(colours.Orange0, 500, Easing.OutQuint);
// don't call base in order to block scale animation
return false;
}
protected override void OnMouseUp(MouseUpEvent e)
{
Background.FadeColour(colours.Orange1, 300, Easing.OutQuint);
// don't call base in order to block scale animation
}
}
}

View File

@ -8,7 +8,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osuTK;
using osuTK.Graphics;
@ -16,8 +16,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
{
public abstract partial class CircularDistanceSnapGrid : DistanceSnapGrid
{
protected CircularDistanceSnapGrid(HitObject referenceObject, Vector2 startPosition, double startTime, double? endTime = null)
: base(referenceObject, startPosition, startTime, endTime)
protected CircularDistanceSnapGrid(Vector2 startPosition, double startTime, double? endTime = null, IHasSliderVelocity? sliderVelocitySource = null)
: base(startPosition, startTime, endTime, sliderVelocitySource)
{
}
@ -56,14 +56,14 @@ namespace osu.Game.Screens.Edit.Compose.Components
// Picture the scenario where the user has just placed an object on a 1/2 snap, then changes to
// 1/3 snap and expects to be able to place the next object on a valid 1/3 snap, regardless of the
// fact that the 1/2 snap reference object is not valid for 1/3 snapping.
float offset = SnapProvider.FindSnappedDistance(ReferenceObject, 0, DistanceSnapTarget.End);
float offset = (float)(SnapProvider.FindSnappedDistance(0, StartTime, SliderVelocitySource) * DistanceSpacingMultiplier.Value);
for (int i = 0; i < requiredCircles; i++)
{
const float thickness = 4;
float diameter = (offset + (i + 1) * DistanceBetweenTicks + thickness / 2) * 2;
AddInternal(new Ring(ReferenceObject, GetColourForIndexFromPlacement(i))
AddInternal(new Ring(StartTime, GetColourForIndexFromPlacement(i))
{
Position = StartPosition,
Origin = Anchor.Centre,
@ -98,19 +98,19 @@ namespace osu.Game.Screens.Edit.Compose.Components
travelLength = DistanceBetweenTicks;
float snappedDistance = fixedTime != null
? SnapProvider.DurationToDistance(ReferenceObject, fixedTime.Value - ReferenceObject.GetEndTime())
? SnapProvider.DurationToDistance(fixedTime.Value - StartTime, StartTime, SliderVelocitySource)
// When interacting with the resolved snap provider, the distance spacing multiplier should first be removed
// to allow for snapping at a non-multiplied ratio.
: SnapProvider.FindSnappedDistance(ReferenceObject, travelLength / distanceSpacingMultiplier, DistanceSnapTarget.End);
: SnapProvider.FindSnappedDistance(travelLength / distanceSpacingMultiplier, StartTime, SliderVelocitySource);
double snappedTime = StartTime + SnapProvider.DistanceToDuration(ReferenceObject, snappedDistance);
double snappedTime = StartTime + SnapProvider.DistanceToDuration(snappedDistance, StartTime, SliderVelocitySource);
if (snappedTime > LatestEndTime)
{
double tickLength = Beatmap.GetBeatLengthAtTime(StartTime);
snappedDistance = SnapProvider.DurationToDistance(ReferenceObject, MaxIntervals * tickLength);
snappedTime = StartTime + SnapProvider.DistanceToDuration(ReferenceObject, snappedDistance);
snappedDistance = SnapProvider.DurationToDistance(MaxIntervals * tickLength, StartTime, SliderVelocitySource);
snappedTime = StartTime + SnapProvider.DistanceToDuration(snappedDistance, StartTime, SliderVelocitySource);
}
// The multiplier can then be reapplied to the final position.
@ -127,13 +127,13 @@ namespace osu.Game.Screens.Edit.Compose.Components
[Resolved]
private EditorClock? editorClock { get; set; }
private readonly HitObject referenceObject;
private readonly double startTime;
private readonly Color4 baseColour;
public Ring(HitObject referenceObject, Color4 baseColour)
public Ring(double startTime, Color4 baseColour)
{
this.referenceObject = referenceObject;
this.startTime = startTime;
Colour = this.baseColour = baseColour;
@ -148,9 +148,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
return;
float distanceSpacingMultiplier = (float)snapProvider.DistanceSpacingMultiplier.Value;
double timeFromReferencePoint = editorClock.CurrentTime - referenceObject.GetEndTime();
double timeFromReferencePoint = editorClock.CurrentTime - startTime;
float distanceForCurrentTime = snapProvider.DurationToDistance(referenceObject, timeFromReferencePoint)
float distanceForCurrentTime = snapProvider.DurationToDistance(timeFromReferencePoint, startTime)
* distanceSpacingMultiplier;
float timeBasedAlpha = 1 - Math.Clamp(Math.Abs(distanceForCurrentTime - Size.X / 2) / 30, 0, 1);

View File

@ -4,6 +4,7 @@
#nullable disable
using System;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
@ -12,7 +13,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Layout;
using osu.Game.Graphics;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osuTK;
using osuTK.Graphics;
@ -48,6 +49,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
protected readonly double? LatestEndTime;
[CanBeNull]
protected readonly IHasSliderVelocity SliderVelocitySource;
[Resolved]
protected OsuColour Colours { get; private set; }
@ -62,19 +66,17 @@ namespace osu.Game.Screens.Edit.Compose.Components
private readonly LayoutValue gridCache = new LayoutValue(Invalidation.RequiredParentSizeToFit);
protected readonly HitObject ReferenceObject;
/// <summary>
/// Creates a new <see cref="DistanceSnapGrid"/>.
/// </summary>
/// <param name="referenceObject">A reference object to gather relevant difficulty values from.</param>
/// <param name="startPosition">The position at which the grid should start. The first tick is located one distance spacing length away from this point.</param>
/// <param name="startTime">The snapping time at <see cref="StartPosition"/>.</param>
/// <param name="endTime">The time at which the snapping grid should end. If null, the grid will continue until the bounds of the screen are exceeded.</param>
protected DistanceSnapGrid(HitObject referenceObject, Vector2 startPosition, double startTime, double? endTime = null)
/// <param name="sliderVelocitySource">The reference object with slider velocity to include in the calculations for distance snapping.</param>
protected DistanceSnapGrid(Vector2 startPosition, double startTime, double? endTime = null, [CanBeNull] IHasSliderVelocity sliderVelocitySource = null)
{
ReferenceObject = referenceObject;
LatestEndTime = endTime;
SliderVelocitySource = sliderVelocitySource;
StartPosition = startPosition;
StartTime = startTime;
@ -97,14 +99,14 @@ namespace osu.Game.Screens.Edit.Compose.Components
private void updateSpacing()
{
float distanceSpacingMultiplier = (float)DistanceSpacingMultiplier.Value;
float beatSnapDistance = SnapProvider.GetBeatSnapDistanceAt(ReferenceObject, false);
float beatSnapDistance = SnapProvider.GetBeatSnapDistance(SliderVelocitySource);
DistanceBetweenTicks = beatSnapDistance * distanceSpacingMultiplier;
if (LatestEndTime == null)
MaxIntervals = int.MaxValue;
else
MaxIntervals = (int)((LatestEndTime.Value - StartTime) / SnapProvider.DistanceToDuration(ReferenceObject, beatSnapDistance));
MaxIntervals = (int)((LatestEndTime.Value - StartTime) / SnapProvider.DistanceToDuration(beatSnapDistance, StartTime, SliderVelocitySource));
gridCache.Invalidate();
}
@ -132,7 +134,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// <param name="position">The original position in coordinate space local to this <see cref="DistanceSnapGrid"/>.</param>
/// <param name="fixedTime">
/// Whether the snap operation should be temporally constrained to a particular time instant,
/// thus fixing the possible positions to a set distance from the <see cref="ReferenceObject"/>.
/// thus fixing the possible positions to a set distance relative from the <see cref="StartTime"/>.
/// </param>
/// <returns>A tuple containing the snapped position in coordinate space local to this <see cref="DistanceSnapGrid"/> and the respective time value.</returns>
public abstract (Vector2 position, double time) GetSnappedPosition(Vector2 position, double? fixedTime = null);

View File

@ -317,6 +317,9 @@ namespace osu.Game.Screens.Edit
workingBeatmapUpdated = true;
});
var bookmarkController = new BookmarkController();
AddInternal(bookmarkController);
OsuMenuItem undoMenuItem;
OsuMenuItem redoMenuItem;
@ -442,29 +445,7 @@ namespace osu.Game.Screens.Edit
Items = new MenuItem[]
{
new EditorMenuItem(EditorStrings.SetPreviewPointToCurrent, MenuItemType.Standard, SetPreviewPointToCurrentTime),
new EditorMenuItem(EditorStrings.Bookmarks)
{
Items = new MenuItem[]
{
new EditorMenuItem(EditorStrings.AddBookmark, MenuItemType.Standard, addBookmarkAtCurrentTime)
{
Hotkey = new Hotkey(GlobalAction.EditorAddBookmark),
},
new EditorMenuItem(EditorStrings.RemoveClosestBookmark, MenuItemType.Destructive, removeBookmarksInProximityToCurrentTime)
{
Hotkey = new Hotkey(GlobalAction.EditorRemoveClosestBookmark)
},
new EditorMenuItem(EditorStrings.SeekToPreviousBookmark, MenuItemType.Standard, () => seekBookmark(-1))
{
Hotkey = new Hotkey(GlobalAction.EditorSeekToPreviousBookmark)
},
new EditorMenuItem(EditorStrings.SeekToNextBookmark, MenuItemType.Standard, () => seekBookmark(1))
{
Hotkey = new Hotkey(GlobalAction.EditorSeekToNextBookmark)
},
new EditorMenuItem(EditorStrings.ResetBookmarks, MenuItemType.Destructive, () => editorBeatmap.Bookmarks.Clear())
}
}
bookmarkController.Menu,
}
}
}
@ -800,14 +781,6 @@ namespace osu.Game.Screens.Edit
case GlobalAction.EditorSeekToNextSamplePoint:
seekSamplePoint(1);
return true;
case GlobalAction.EditorSeekToPreviousBookmark:
seekBookmark(-1);
return true;
case GlobalAction.EditorSeekToNextBookmark:
seekBookmark(1);
return true;
}
if (e.Repeat)
@ -815,14 +788,6 @@ namespace osu.Game.Screens.Edit
switch (e.Action)
{
case GlobalAction.EditorAddBookmark:
addBookmarkAtCurrentTime();
return true;
case GlobalAction.EditorRemoveClosestBookmark:
removeBookmarksInProximityToCurrentTime();
return true;
case GlobalAction.EditorCloneSelection:
Clone();
return true;
@ -855,19 +820,6 @@ namespace osu.Game.Screens.Edit
return false;
}
private void addBookmarkAtCurrentTime()
{
int bookmark = (int)clock.CurrentTimeAccurate;
int idx = editorBeatmap.Bookmarks.BinarySearch(bookmark);
if (idx < 0)
editorBeatmap.Bookmarks.Insert(~idx, bookmark);
}
private void removeBookmarksInProximityToCurrentTime()
{
editorBeatmap.Bookmarks.RemoveAll(b => Math.Abs(b - clock.CurrentTimeAccurate) < 2000);
}
public void OnReleased(KeyBindingReleaseEvent<GlobalAction> e)
{
}
@ -1202,16 +1154,6 @@ namespace osu.Game.Screens.Edit
clock.SeekSmoothlyTo(found.StartTime);
}
private void seekBookmark(int direction)
{
int? targetBookmark = direction < 1
? editorBeatmap.Bookmarks.Cast<int?>().LastOrDefault(b => b < clock.CurrentTimeAccurate)
: editorBeatmap.Bookmarks.Cast<int?>().FirstOrDefault(b => b > clock.CurrentTimeAccurate);
if (targetBookmark != null)
clock.SeekSmoothlyTo(targetBookmark.Value);
}
private void seekSamplePoint(int direction)
{
double currentTime = clock.CurrentTimeAccurate;
@ -1314,6 +1256,15 @@ namespace osu.Game.Screens.Edit
yield return externalEdit;
}
if (editorBeatmap.BeatmapInfo.OnlineID > 0)
{
yield return new OsuMenuItemSpacer();
yield return new EditorMenuItem(EditorStrings.OpenInfoPage, MenuItemType.Standard,
() => (Game as OsuGame)?.OpenUrlExternally(editorBeatmap.BeatmapInfo.GetOnlineURL(api, editorBeatmap.BeatmapInfo.Ruleset)));
yield return new EditorMenuItem(EditorStrings.OpenDiscussionPage, MenuItemType.Standard,
() => (Game as OsuGame)?.OpenUrlExternally($@"{api.Endpoints.WebsiteUrl}/beatmapsets/{editorBeatmap.BeatmapInfo.BeatmapSet!.OnlineID}/discussion/{editorBeatmap.BeatmapInfo.OnlineID}"));
}
yield return new OsuMenuItemSpacer();
yield return new EditorMenuItem(CommonStrings.Exit, MenuItemType.Standard, this.Exit);
}

View File

@ -115,7 +115,9 @@ namespace osu.Game.Screens.Edit
if (editorBeatmap.Bookmarks.Contains(newBookmark))
continue;
editorBeatmap.Bookmarks.Add(newBookmark);
int idx = editorBeatmap.Bookmarks.BinarySearch(newBookmark);
if (idx < 0)
editorBeatmap.Bookmarks.Insert(~idx, newBookmark);
}
}

View File

@ -0,0 +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.Allocation;
using osu.Game.Overlays;
using osu.Game.Localisation;
namespace osu.Game.Screens.Edit.Submission
{
public partial class BeatmapSubmissionOverlay : WizardOverlay
{
public BeatmapSubmissionOverlay()
: base(OverlayColourScheme.Aquamarine)
{
}
[BackgroundDependencyLoader]
private void load()
{
AddStep<ScreenContentPermissions>();
AddStep<ScreenFrequentlyAskedQuestions>();
AddStep<ScreenSubmissionSettings>();
Header.Title = BeatmapSubmissionStrings.BeatmapSubmissionTitle;
Header.Description = BeatmapSubmissionStrings.BeatmapSubmissionDescription;
}
}
}

View File

@ -0,0 +1,44 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Localisation;
using osu.Game.Overlays;
namespace osu.Game.Screens.Edit.Submission
{
[LocalisableDescription(typeof(BeatmapSubmissionStrings), nameof(BeatmapSubmissionStrings.ContentPermissions))]
public partial class ScreenContentPermissions : WizardScreen
{
[BackgroundDependencyLoader]
private void load(OsuGame? game)
{
Content.AddRange(new Drawable[]
{
new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: CONTENT_FONT_SIZE))
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Text = BeatmapSubmissionStrings.ContentPermissionsDisclaimer,
},
new RoundedButton
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Width = 450,
Text = BeatmapSubmissionStrings.CheckContentUsageGuidelines,
Action = () => game?.ShowWiki(@"Rules/Content_usage_permissions"),
},
});
}
public override LocalisableString? NextStepText => BeatmapSubmissionStrings.ContentPermissionsAcknowledgement;
}
}

View File

@ -0,0 +1,62 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Localisation;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Localisation;
using osu.Game.Online.API;
using osu.Game.Overlays;
using osuTK;
namespace osu.Game.Screens.Edit.Submission
{
[LocalisableDescription(typeof(BeatmapSubmissionStrings), nameof(BeatmapSubmissionStrings.FrequentlyAskedQuestions))]
public partial class ScreenFrequentlyAskedQuestions : WizardScreen
{
[BackgroundDependencyLoader]
private void load(OsuGame? game, IAPIProvider api)
{
Content.Add(new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(5),
Children = new Drawable[]
{
new FormButton
{
RelativeSizeAxes = Axes.X,
Caption = BeatmapSubmissionStrings.BeatmapRankingCriteriaDescription,
ButtonText = BeatmapSubmissionStrings.BeatmapRankingCriteria,
Action = () => game?.ShowWiki(@"Ranking_Criteria"),
},
new FormButton
{
RelativeSizeAxes = Axes.X,
Caption = BeatmapSubmissionStrings.SubmissionProcessDescription,
ButtonText = BeatmapSubmissionStrings.SubmissionProcess,
Action = () => game?.ShowWiki(@"Beatmap_ranking_procedure"),
},
new FormButton
{
RelativeSizeAxes = Axes.X,
Caption = BeatmapSubmissionStrings.MappingHelpForumDescription,
ButtonText = BeatmapSubmissionStrings.MappingHelpForum,
Action = () => game?.OpenUrlExternally($@"{api.Endpoints.WebsiteUrl}/community/forums/56"),
},
new FormButton
{
RelativeSizeAxes = Axes.X,
Caption = BeatmapSubmissionStrings.ModdingQueuesForumDescription,
ButtonText = BeatmapSubmissionStrings.ModdingQueuesForum,
Action = () => game?.OpenUrlExternally($@"{api.Endpoints.WebsiteUrl}/community/forums/60"),
},
},
});
}
}
}

View File

@ -0,0 +1,73 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Localisation;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Localisation;
using osu.Game.Overlays;
using osuTK;
namespace osu.Game.Screens.Edit.Submission
{
[LocalisableDescription(typeof(BeatmapSubmissionStrings), nameof(BeatmapSubmissionStrings.SubmissionSettings))]
public partial class ScreenSubmissionSettings : WizardScreen
{
private readonly BindableBool notifyOnDiscussionReplies = new BindableBool();
private readonly BindableBool loadInBrowserAfterSubmission = new BindableBool();
[BackgroundDependencyLoader]
private void load(OsuConfigManager configManager, OsuColour colours)
{
configManager.BindWith(OsuSetting.EditorSubmissionNotifyOnDiscussionReplies, notifyOnDiscussionReplies);
configManager.BindWith(OsuSetting.EditorSubmissionLoadInBrowserAfterSubmission, loadInBrowserAfterSubmission);
Content.Add(new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(5),
Children = new Drawable[]
{
new FormEnumDropdown<BeatmapSubmissionTarget>
{
RelativeSizeAxes = Axes.X,
Caption = BeatmapSubmissionStrings.BeatmapSubmissionTargetCaption,
},
new FormCheckBox
{
Caption = BeatmapSubmissionStrings.NotifyOnDiscussionReplies,
Current = notifyOnDiscussionReplies,
},
new FormCheckBox
{
Caption = BeatmapSubmissionStrings.LoadInBrowserAfterSubmission,
Current = loadInBrowserAfterSubmission,
},
new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: CONTENT_FONT_SIZE, weight: FontWeight.Bold))
{
RelativeSizeAxes = Axes.X,
Colour = colours.Orange1,
Text = BeatmapSubmissionStrings.LegacyExportDisclaimer,
Padding = new MarginPadding { Top = 20 }
},
}
});
}
private enum BeatmapSubmissionTarget
{
[LocalisableDescription(typeof(BeatmapSubmissionStrings), nameof(BeatmapSubmissionStrings.BeatmapSubmissionTargetWIP))]
WIP,
[LocalisableDescription(typeof(BeatmapSubmissionStrings), nameof(BeatmapSubmissionStrings.BeatmapSubmissionTargetPending))]
Pending,
}
}
}

View File

@ -0,0 +1,212 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
using osuTK;
namespace osu.Game.Screens.Edit.Submission
{
public partial class SubmissionStageProgress : CompositeDrawable
{
public LocalisableString StageDescription { get; init; }
private Bindable<StageStatusType> status { get; } = new Bindable<StageStatusType>();
private Bindable<float?> progress { get; } = new Bindable<float?>();
private Container progressBarContainer = null!;
private Box progressBar = null!;
private Container iconContainer = null!;
private OsuTextFlowContainer errorMessage = null!;
[Resolved]
private OsuColour colours { get; set; } = null!;
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
InternalChildren = new Drawable[]
{
new OsuSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Text = StageDescription,
},
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(5),
Children = new Drawable[]
{
iconContainer = new Container
{
AutoSizeAxes = Axes.Both,
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
},
new Container
{
AutoSizeAxes = Axes.Both,
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Children =
[
progressBarContainer = new Container
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Width = 150,
Height = 10,
CornerRadius = 5,
Masking = true,
Children = new[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background6,
},
progressBar = new Box
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Width = 0,
Colour = colourProvider.Highlight1,
}
}
},
errorMessage = new OsuTextFlowContainer
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
// should really be `CentreRight` too, but that's broken due to a framework bug
// (https://github.com/ppy/osu-framework/issues/5084)
TextAnchor = Anchor.BottomRight,
Width = 450,
AutoSizeAxes = Axes.Y,
Alpha = 0,
Colour = colours.Red1,
}
]
}
}
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
status.BindValueChanged(_ => Scheduler.AddOnce(updateStatus), true);
progress.BindValueChanged(_ => Scheduler.AddOnce(updateProgress), true);
}
public void SetNotStarted() => status.Value = StageStatusType.NotStarted;
public void SetInProgress(float? progress = null)
{
this.progress.Value = progress;
status.Value = StageStatusType.InProgress;
}
public void SetCompleted() => status.Value = StageStatusType.Completed;
public void SetFailed(string reason)
{
status.Value = StageStatusType.Failed;
errorMessage.Text = reason;
}
public void SetCanceled() => status.Value = StageStatusType.Canceled;
private const float transition_duration = 200;
private void updateProgress()
{
if (progress.Value != null)
progressBar.ResizeWidthTo(progress.Value.Value, transition_duration, Easing.OutQuint);
progressBarContainer.FadeTo(status.Value == StageStatusType.InProgress && progress.Value != null ? 1 : 0, transition_duration, Easing.OutQuint);
}
private void updateStatus()
{
progressBarContainer.FadeTo(status.Value == StageStatusType.InProgress && progress.Value != null ? 1 : 0, transition_duration, Easing.OutQuint);
errorMessage.FadeTo(status.Value == StageStatusType.Failed ? 1 : 0, transition_duration, Easing.OutQuint);
iconContainer.Clear();
iconContainer.ClearTransforms();
switch (status.Value)
{
case StageStatusType.InProgress:
iconContainer.Child = new LoadingSpinner
{
Size = new Vector2(16),
State = { Value = Visibility.Visible, },
};
iconContainer.Colour = colours.Orange1;
break;
case StageStatusType.Completed:
iconContainer.Child = new SpriteIcon
{
Icon = FontAwesome.Solid.CheckCircle,
Size = new Vector2(16),
};
iconContainer.Colour = colours.Green1;
iconContainer.FlashColour(Colour4.White, 1000, Easing.OutQuint);
break;
case StageStatusType.Failed:
iconContainer.Child = new SpriteIcon
{
Icon = FontAwesome.Solid.ExclamationCircle,
Size = new Vector2(16),
};
iconContainer.Colour = colours.Red1;
iconContainer.FlashColour(Colour4.White, 1000, Easing.OutQuint);
break;
case StageStatusType.Canceled:
iconContainer.Child = new SpriteIcon
{
Icon = FontAwesome.Solid.Ban,
Size = new Vector2(16),
};
iconContainer.Colour = colours.Gray8;
iconContainer.FlashColour(Colour4.White, 1000, Easing.OutQuint);
break;
}
}
public enum StageStatusType
{
NotStarted,
InProgress,
Completed,
Failed,
Canceled,
}
}
}

View File

@ -361,7 +361,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
return items.ToArray();
string formatRoomUrl(long id) => $@"{api.WebsiteRootUrl}/multiplayer/rooms/{id}";
string formatRoomUrl(long id) => $@"{api.Endpoints.WebsiteUrl}/multiplayer/rooms/{id}";
}
}

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