diff --git a/CodeAnalysis/BannedSymbols.txt b/CodeAnalysis/BannedSymbols.txt
index 022da0a2ea..03fd21829d 100644
--- a/CodeAnalysis/BannedSymbols.txt
+++ b/CodeAnalysis/BannedSymbols.txt
@@ -15,6 +15,8 @@ M:Realms.CollectionExtensions.SubscribeForNotifications`1(System.Collections.Gen
M:System.Threading.Tasks.Task.Wait();Don't use Task.Wait. Use Task.WaitSafely() to ensure we avoid deadlocks.
P:System.Threading.Tasks.Task`1.Result;Don't use Task.Result. Use Task.GetResultSafely() to ensure we avoid deadlocks.
M:System.Threading.ManualResetEventSlim.Wait();Specify a timeout to avoid waiting forever.
+M:System.Char.ToLower(System.Char);char.ToLower() changes behaviour depending on CultureInfo.CurrentCulture. Use char.ToLowerInvariant() instead. If wanting culture-sensitive behaviour, explicitly provide CultureInfo.CurrentCulture.
+M:System.Char.ToUpper(System.Char);char.ToUpper() changes behaviour depending on CultureInfo.CurrentCulture. Use char.ToUpperInvariant() instead. If wanting culture-sensitive behaviour, explicitly provide CultureInfo.CurrentCulture.
M:System.String.ToLower();string.ToLower() changes behaviour depending on CultureInfo.CurrentCulture. Use string.ToLowerInvariant() instead. If wanting culture-sensitive behaviour, explicitly provide CultureInfo.CurrentCulture or use LocalisableString.
M:System.String.ToUpper();string.ToUpper() changes behaviour depending on CultureInfo.CurrentCulture. Use string.ToUpperInvariant() instead. If wanting culture-sensitive behaviour, explicitly provide CultureInfo.CurrentCulture or use LocalisableString.
M:Humanizer.InflectorExtensions.Pascalize(System.String);Humanizer's .Pascalize() extension method changes behaviour depending on CultureInfo.CurrentCulture. Use StringDehumanizeExtensions.ToPascalCase() instead.
diff --git a/osu.Android.props b/osu.Android.props
index 3f4c8e2d24..f251e8ee71 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -51,8 +51,8 @@
-
-
+
+
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs
index 0169627867..728aa27da2 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs
@@ -14,6 +14,7 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
+using osu.Game.Rulesets.Osu.Skinning.Legacy;
using osu.Game.Skinning;
using osu.Game.Tests.Visual;
using osuTK;
@@ -68,10 +69,8 @@ namespace osu.Game.Rulesets.Osu.Tests
AddStep("create slider", () =>
{
- var tintingSkin = skinManager.GetSkin(DefaultLegacySkin.CreateInfo());
- tintingSkin.Configuration.ConfigDictionary["AllowSliderBallTint"] = "1";
-
- var provider = Ruleset.Value.CreateInstance().CreateSkinTransformer(tintingSkin, Beatmap.Value.Beatmap);
+ var skin = skinManager.GetSkin(DefaultLegacySkin.CreateInfo());
+ var provider = Ruleset.Value.CreateInstance().CreateSkinTransformer(skin, Beatmap.Value.Beatmap);
Child = new SkinProvidingContainer(provider)
{
@@ -92,10 +91,10 @@ namespace osu.Game.Rulesets.Osu.Tests
});
AddStep("set accent white", () => dho.AccentColour.Value = Color4.White);
- AddAssert("ball is white", () => dho.ChildrenOfType().Single().AccentColour == Color4.White);
+ AddAssert("ball is white", () => dho.ChildrenOfType().Single().BallColour == Color4.White);
AddStep("set accent red", () => dho.AccentColour.Value = Color4.Red);
- AddAssert("ball is red", () => dho.ChildrenOfType().Single().AccentColour == Color4.Red);
+ AddAssert("ball is red", () => dho.ChildrenOfType().Single().BallColour == Color4.Red);
}
private Slider prepareObject(Slider slider)
diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
index 282631bb1a..838f18bad5 100644
--- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
+++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
@@ -82,19 +82,7 @@ namespace osu.Game.Rulesets.Osu.Edit
placementObject = EditorBeatmap.PlacementObject.GetBoundCopy();
placementObject.ValueChanged += _ => updateDistanceSnapGrid();
- distanceSnapToggle.ValueChanged += _ =>
- {
- updateDistanceSnapGrid();
-
- if (distanceSnapToggle.Value == TernaryState.True)
- rectangularGridSnapToggle.Value = TernaryState.False;
- };
-
- rectangularGridSnapToggle.ValueChanged += _ =>
- {
- if (rectangularGridSnapToggle.Value == TernaryState.True)
- distanceSnapToggle.Value = TernaryState.False;
- };
+ distanceSnapToggle.ValueChanged += _ => updateDistanceSnapGrid();
// we may be entering the screen with a selection already active
updateDistanceSnapGrid();
@@ -136,22 +124,27 @@ namespace osu.Game.Rulesets.Osu.Edit
if (snapType.HasFlagFast(SnapType.NearbyObjects) && snapToVisibleBlueprints(screenSpacePosition, out var snapResult))
return snapResult;
+ SnapResult result = base.FindSnappedPositionAndTime(screenSpacePosition, snapType);
+
if (snapType.HasFlagFast(SnapType.Grids))
{
if (distanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null)
{
(Vector2 pos, double time) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(screenSpacePosition));
- return new SnapResult(distanceSnapGrid.ToScreenSpace(pos), time, PlayfieldAtScreenSpacePosition(screenSpacePosition));
+
+ result.ScreenSpacePosition = distanceSnapGrid.ToScreenSpace(pos);
+ result.Time = time;
}
if (rectangularGridSnapToggle.Value == TernaryState.True)
{
- Vector2 pos = rectangularPositionSnapGrid.GetSnappedPosition(rectangularPositionSnapGrid.ToLocalSpace(screenSpacePosition));
- return new SnapResult(rectangularPositionSnapGrid.ToScreenSpace(pos), null, PlayfieldAtScreenSpacePosition(screenSpacePosition));
+ Vector2 pos = rectangularPositionSnapGrid.GetSnappedPosition(rectangularPositionSnapGrid.ToLocalSpace(result.ScreenSpacePosition));
+
+ result.ScreenSpacePosition = rectangularPositionSnapGrid.ToScreenSpace(pos);
}
}
- return base.FindSnappedPositionAndTime(screenSpacePosition, snapType);
+ return result;
}
private bool snapToVisibleBlueprints(Vector2 screenSpacePosition, out SnapResult snapResult)
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
index e37f1133aa..785d15c15b 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
@@ -14,12 +14,10 @@ using osu.Game.Audio;
using osu.Game.Graphics.Containers;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
-using osu.Game.Rulesets.Osu.Skinning;
using osu.Game.Rulesets.Osu.Skinning.Default;
using osu.Game.Rulesets.Scoring;
using osu.Game.Skinning;
using osuTK;
-using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
@@ -106,7 +104,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
foreach (var drawableHitObject in NestedHitObjects)
drawableHitObject.AccentColour.Value = colour.NewValue;
- updateBallTint();
}, true);
Tracking.BindValueChanged(updateSlidingSample);
@@ -257,22 +254,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
SliderBody?.RecyclePath();
}
- protected override void ApplySkin(ISkinSource skin, bool allowFallback)
- {
- base.ApplySkin(skin, allowFallback);
-
- updateBallTint();
- }
-
- private void updateBallTint()
- {
- if (CurrentSkin == null)
- return;
-
- bool allowBallTint = CurrentSkin.GetConfig(OsuSkinConfiguration.AllowSliderBallTint)?.Value ?? false;
- Ball.AccentColour = allowBallTint ? AccentColour.Value : Color4.White;
- }
-
protected override void CheckForResult(bool userTriggered, double timeOffset)
{
if (userTriggered || Time.Current < HitObject.EndTime)
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs
index a2fe623897..de6ca7dd38 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs
@@ -11,28 +11,20 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input;
using osu.Framework.Input.Events;
-using osu.Game.Graphics;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Skinning.Default;
using osu.Game.Skinning;
using osuTK;
-using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
- public class DrawableSliderBall : CircularContainer, ISliderProgress, IRequireHighFrequencyMousePosition, IHasAccentColour
+ public class DrawableSliderBall : CircularContainer, ISliderProgress, IRequireHighFrequencyMousePosition
{
public const float FOLLOW_AREA = 2.4f;
public Func GetInitialHitAction;
- public Color4 AccentColour
- {
- get => ball.Colour;
- set => ball.Colour = value;
- }
-
private Drawable followCircleReceptor;
private DrawableSlider drawableSlider;
private Drawable ball;
diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonMainCirclePiece.cs
index ffdcba3cdb..36dc8c801d 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonMainCirclePiece.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonMainCirclePiece.cs
@@ -108,18 +108,23 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
{
base.LoadComplete();
- accentColour.BindValueChanged(colour =>
- {
- outerFill.Colour = innerFill.Colour = colour.NewValue.Darken(4);
- outerGradient.Colour = ColourInfo.GradientVertical(colour.NewValue, colour.NewValue.Darken(0.1f));
- innerGradient.Colour = ColourInfo.GradientVertical(colour.NewValue.Darken(0.5f), colour.NewValue.Darken(0.6f));
- flash.Colour = colour.NewValue;
- }, true);
-
indexInCurrentCombo.BindValueChanged(index => number.Text = (index.NewValue + 1).ToString(), true);
+ accentColour.BindValueChanged(colour =>
+ {
+ // A colour transform is applied.
+ // Without removing transforms first, when it is rewound it may apply an old colour.
+ outerGradient.ClearTransforms(targetMember: nameof(Colour));
+ outerGradient.Colour = ColourInfo.GradientVertical(colour.NewValue, colour.NewValue.Darken(0.1f));
+
+ outerFill.Colour = innerFill.Colour = colour.NewValue.Darken(4);
+ innerGradient.Colour = ColourInfo.GradientVertical(colour.NewValue.Darken(0.5f), colour.NewValue.Darken(0.6f));
+ flash.Colour = colour.NewValue;
+
+ updateStateTransforms(drawableObject, drawableObject.State.Value);
+ }, true);
+
drawableObject.ApplyCustomUpdateState += updateStateTransforms;
- updateStateTransforms(drawableObject, drawableObject.State.Value);
}
private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state)
@@ -173,11 +178,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
.FadeOut(flash_in_duration);
}
- // The flash layer starts white to give the wanted brightness, but is almost immediately
- // recoloured to the accent colour. This would more correctly be done with two layers (one for the initial flash)
- // but works well enough with the colour fade.
flash.FadeTo(1, flash_in_duration, Easing.OutQuint);
- flash.FlashColour(accentColour.Value, fade_out_time, Easing.OutQuint);
this.FadeOut(fade_out_time, Easing.OutQuad);
break;
diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs
index f9f9751b6c..a6e62b83e4 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs
@@ -68,7 +68,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
InternalChildren = new[]
{
- CircleSprite = new KiaiFlashingDrawable(() => new Sprite { Texture = skin.GetTexture(circleName) })
+ CircleSprite = new LegacyKiaiFlashingDrawable(() => new Sprite { Texture = skin.GetTexture(circleName) })
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -77,7 +77,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
- Child = OverlaySprite = new KiaiFlashingDrawable(() => skin.GetAnimation(@$"{circleName}overlay", true, true, frameLength: 1000 / 2d))
+ Child = OverlaySprite = new LegacyKiaiFlashingDrawable(() => skin.GetAnimation(@$"{circleName}overlay", true, true, frameLength: 1000 / 2d))
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySliderBall.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySliderBall.cs
index 414879f42d..60d71ae843 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySliderBall.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySliderBall.cs
@@ -2,6 +2,7 @@
// 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.Sprites;
@@ -21,6 +22,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
[Resolved(canBeNull: true)]
private DrawableHitObject? parentObject { get; set; }
+ public Color4 BallColour => animationContent.Colour;
+
private Sprite layerNd = null!;
private Sprite layerSpec = null!;
@@ -61,6 +64,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
};
}
+ private readonly IBindable accentColour = new Bindable();
+
protected override void LoadComplete()
{
base.LoadComplete();
@@ -69,6 +74,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{
parentObject.ApplyCustomUpdateState += updateStateTransforms;
updateStateTransforms(parentObject, parentObject.State.Value);
+
+ if (skin.GetConfig(SkinConfiguration.LegacySetting.AllowSliderBallTint)?.Value == true)
+ {
+ accentColour.BindTo(parentObject.AccentColour);
+ accentColour.BindValueChanged(a => animationContent.Colour = a.NewValue, true);
+ }
}
}
diff --git a/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs b/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs
index 306a1e38b9..1c0a62454b 100644
--- a/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs
@@ -9,7 +9,6 @@ namespace osu.Game.Rulesets.Osu.Skinning
{
SliderBorderSize,
SliderPathRadius,
- AllowSliderBallTint,
CursorCentre,
CursorExpand,
CursorRotate,
diff --git a/osu.Game.Rulesets.Osu/Skinning/SmokeSegment.cs b/osu.Game.Rulesets.Osu/Skinning/SmokeSegment.cs
index aba4d0ff63..46c8e7c02a 100644
--- a/osu.Game.Rulesets.Osu/Skinning/SmokeSegment.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/SmokeSegment.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
+using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
@@ -22,8 +23,6 @@ namespace osu.Game.Rulesets.Osu.Skinning
{
public abstract class SmokeSegment : Drawable, ITexturedShaderDrawable
{
- private const int max_point_count = 18_000;
-
// fade anim values
private const double initial_fade_out_duration = 4000;
@@ -85,12 +84,6 @@ namespace osu.Game.Rulesets.Osu.Skinning
totalDistance = pointInterval;
}
- private Vector2 nextPointDirection()
- {
- float angle = RNG.NextSingle(0, 2 * MathF.PI);
- return new Vector2(MathF.Sin(angle), -MathF.Cos(angle));
- }
-
public void AddPosition(Vector2 position, double time)
{
lastPosition ??= position;
@@ -107,33 +100,27 @@ namespace osu.Game.Rulesets.Osu.Skinning
Vector2 pointPos = (pointInterval - (totalDistance - delta)) * increment + (Vector2)lastPosition;
increment *= pointInterval;
- if (SmokePoints.Count > 0 && SmokePoints[^1].Time > time)
- {
- int index = ~SmokePoints.BinarySearch(new SmokePoint { Time = time }, new SmokePoint.UpperBoundComparer());
- SmokePoints.RemoveRange(index, SmokePoints.Count - index);
- }
-
totalDistance %= pointInterval;
- for (int i = 0; i < count; i++)
+ if (SmokePoints.Count == 0 || SmokePoints[^1].Time <= time)
{
- SmokePoints.Add(new SmokePoint
+ for (int i = 0; i < count; i++)
{
- Position = pointPos,
- Time = time,
- Direction = nextPointDirection(),
- });
+ SmokePoints.Add(new SmokePoint
+ {
+ Position = pointPos,
+ Time = time,
+ Angle = RNG.NextSingle(0, 2 * MathF.PI),
+ });
- pointPos += increment;
+ pointPos += increment;
+ }
}
Invalidate(Invalidation.DrawNode);
}
lastPosition = position;
-
- if (SmokePoints.Count >= max_point_count)
- FinishDrawing(time);
}
public void FinishDrawing(double time)
@@ -157,7 +144,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
{
public Vector2 Position;
public double Time;
- public Vector2 Direction;
+ public float Angle;
public struct UpperBoundComparer : IComparer
{
@@ -171,6 +158,17 @@ namespace osu.Game.Rulesets.Osu.Skinning
return x.Time > target.Time ? 1 : -1;
}
}
+
+ public struct LowerBoundComparer : IComparer
+ {
+ public int Compare(SmokePoint x, SmokePoint target)
+ {
+ // Similar logic as UpperBoundComparer, except returned index will always be
+ // the first element larger or equal
+
+ return x.Time < target.Time ? -1 : 1;
+ }
+ }
}
protected class SmokeDrawNode : TexturedShaderDrawNode
@@ -187,11 +185,11 @@ namespace osu.Game.Rulesets.Osu.Skinning
private Vector2 drawSize;
private Texture? texture;
private int rotationSeed;
- private int rotationIndex;
+ private int firstVisiblePointIndex;
// anim calculation vars (color, scale, direction)
private double initialFadeOutDurationTrunc;
- private double firstVisiblePointTime;
+ private double firstVisiblePointTimeAfterSmokeEnded;
private double initialFadeOutTime;
private double reFadeInTime;
@@ -206,9 +204,6 @@ namespace osu.Game.Rulesets.Osu.Skinning
{
base.ApplyState();
- points.Clear();
- points.AddRange(Source.SmokePoints);
-
radius = Source.radius;
drawSize = Source.DrawSize;
texture = Source.Texture;
@@ -220,11 +215,18 @@ namespace osu.Game.Rulesets.Osu.Skinning
rotationSeed = Source.rotationSeed;
initialFadeOutDurationTrunc = Math.Min(initial_fade_out_duration, SmokeEndTime - SmokeStartTime);
- firstVisiblePointTime = SmokeEndTime - initialFadeOutDurationTrunc;
+ firstVisiblePointTimeAfterSmokeEnded = SmokeEndTime - initialFadeOutDurationTrunc;
- initialFadeOutTime = CurrentTime;
- reFadeInTime = CurrentTime - initialFadeOutDurationTrunc - firstVisiblePointTime * (1 - 1 / re_fade_in_speed);
- finalFadeOutTime = CurrentTime - initialFadeOutDurationTrunc - firstVisiblePointTime * (1 - 1 / final_fade_out_speed);
+ initialFadeOutTime = Math.Min(CurrentTime, SmokeEndTime);
+ reFadeInTime = CurrentTime - initialFadeOutDurationTrunc - firstVisiblePointTimeAfterSmokeEnded * (1 - 1 / re_fade_in_speed);
+ finalFadeOutTime = CurrentTime - initialFadeOutDurationTrunc - firstVisiblePointTimeAfterSmokeEnded * (1 - 1 / final_fade_out_speed);
+
+ double firstVisiblePointTime = Math.Min(SmokeEndTime, CurrentTime) - initialFadeOutDurationTrunc;
+ firstVisiblePointIndex = ~Source.SmokePoints.BinarySearch(new SmokePoint { Time = firstVisiblePointTime }, new SmokePoint.LowerBoundComparer());
+ int futurePointIndex = ~Source.SmokePoints.BinarySearch(new SmokePoint { Time = CurrentTime }, new SmokePoint.UpperBoundComparer());
+
+ points.Clear();
+ points.AddRange(Source.SmokePoints.Skip(firstVisiblePointIndex).Take(futurePointIndex - firstVisiblePointIndex));
}
public sealed override void Draw(IRenderer renderer)
@@ -234,9 +236,14 @@ namespace osu.Game.Rulesets.Osu.Skinning
if (points.Count == 0)
return;
- rotationIndex = 0;
+ quadBatch ??= renderer.CreateQuadBatch(200, 4);
+
+ if (points.Count > quadBatch.Size && quadBatch.Size != IRenderer.MAX_QUADS)
+ {
+ int batchSize = Math.Min(quadBatch.Size * 2, IRenderer.MAX_QUADS);
+ quadBatch = renderer.CreateQuadBatch(batchSize, 4);
+ }
- quadBatch ??= renderer.CreateQuadBatch(max_point_count / 10, 10);
texture ??= renderer.WhitePixel;
RectangleF textureRect = texture.GetTextureRect();
@@ -248,8 +255,8 @@ namespace osu.Game.Rulesets.Osu.Skinning
shader.Bind();
texture.Bind();
- foreach (var point in points)
- drawPointQuad(point, textureRect);
+ for (int i = 0; i < points.Count; i++)
+ drawPointQuad(points[i], textureRect, i + firstVisiblePointIndex);
shader.Unbind();
renderer.PopLocalMatrix();
@@ -263,30 +270,34 @@ namespace osu.Game.Rulesets.Osu.Skinning
{
var color = Color4.White;
- double timeDoingInitialFadeOut = Math.Min(initialFadeOutTime, SmokeEndTime) - point.Time;
+ double timeDoingFinalFadeOut = finalFadeOutTime - point.Time / final_fade_out_speed;
- if (timeDoingInitialFadeOut > 0)
+ if (timeDoingFinalFadeOut > 0 && point.Time >= firstVisiblePointTimeAfterSmokeEnded)
{
- float fraction = Math.Clamp((float)(timeDoingInitialFadeOut / initial_fade_out_duration), 0, 1);
- color.A = (1 - fraction) * initial_alpha;
+ float fraction = Math.Clamp((float)(timeDoingFinalFadeOut / final_fade_out_duration), 0, 1);
+ fraction = MathF.Pow(fraction, 5);
+ color.A = (1 - fraction) * re_fade_in_alpha;
}
-
- if (color.A > 0)
+ else
{
- double timeDoingReFadeIn = reFadeInTime - point.Time / re_fade_in_speed;
- double timeDoingFinalFadeOut = finalFadeOutTime - point.Time / final_fade_out_speed;
+ double timeDoingInitialFadeOut = initialFadeOutTime - point.Time;
- if (timeDoingFinalFadeOut > 0)
+ if (timeDoingInitialFadeOut > 0)
{
- float fraction = Math.Clamp((float)(timeDoingFinalFadeOut / final_fade_out_duration), 0, 1);
- fraction = MathF.Pow(fraction, 5);
- color.A = (1 - fraction) * re_fade_in_alpha;
+ float fraction = Math.Clamp((float)(timeDoingInitialFadeOut / initial_fade_out_duration), 0, 1);
+ color.A = (1 - fraction) * initial_alpha;
}
- else if (timeDoingReFadeIn > 0)
+
+ if (point.Time > firstVisiblePointTimeAfterSmokeEnded)
{
- float fraction = Math.Clamp((float)(timeDoingReFadeIn / re_fade_in_duration), 0, 1);
- fraction = 1 - MathF.Pow(1 - fraction, 5);
- color.A = fraction * (re_fade_in_alpha - color.A) + color.A;
+ double timeDoingReFadeIn = reFadeInTime - point.Time / re_fade_in_speed;
+
+ if (timeDoingReFadeIn > 0)
+ {
+ float fraction = Math.Clamp((float)(timeDoingReFadeIn / re_fade_in_duration), 0, 1);
+ fraction = 1 - MathF.Pow(1 - fraction, 5);
+ color.A = fraction * (re_fade_in_alpha - color.A) + color.A;
+ }
}
}
@@ -301,33 +312,33 @@ namespace osu.Game.Rulesets.Osu.Skinning
return fraction * (final_scale - initial_scale) + initial_scale;
}
- protected virtual Vector2 PointDirection(SmokePoint point)
+ protected virtual Vector2 PointDirection(SmokePoint point, int index)
{
- float initialAngle = MathF.Atan2(point.Direction.Y, point.Direction.X);
- float finalAngle = initialAngle + nextRotation();
-
double timeDoingRotation = CurrentTime - point.Time;
float fraction = Math.Clamp((float)(timeDoingRotation / rotation_duration), 0, 1);
fraction = 1 - MathF.Pow(1 - fraction, 5);
- float angle = fraction * (finalAngle - initialAngle) + initialAngle;
+ float angle = fraction * getRotation(index) + point.Angle;
return new Vector2(MathF.Sin(angle), -MathF.Cos(angle));
}
- private float nextRotation() => max_rotation * (StatelessRNG.NextSingle(rotationSeed, rotationIndex++) * 2 - 1);
+ private float getRotation(int index) => max_rotation * (StatelessRNG.NextSingle(rotationSeed, index) * 2 - 1);
- private void drawPointQuad(SmokePoint point, RectangleF textureRect)
+ private void drawPointQuad(SmokePoint point, RectangleF textureRect, int index)
{
Debug.Assert(quadBatch != null);
var colour = PointColour(point);
- float scale = PointScale(point);
- var dir = PointDirection(point);
- var ortho = dir.PerpendicularLeft;
-
- if (colour.A == 0 || scale == 0)
+ if (colour.A == 0)
return;
+ float scale = PointScale(point);
+ if (scale == 0)
+ return;
+
+ var dir = PointDirection(point, index);
+ var ortho = dir.PerpendicularLeft;
+
var localTopLeft = point.Position + (radius * scale * (-ortho - dir));
var localTopRight = point.Position + (radius * scale * (-ortho + dir));
var localBotLeft = point.Position + (radius * scale * (ortho - dir));
diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableDrumRoll.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableDrumRoll.cs
index 2d27e0e40e..e42dc254ac 100644
--- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableDrumRoll.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableDrumRoll.cs
@@ -25,8 +25,8 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
TimeRange = { Value = 5000 },
};
- [BackgroundDependencyLoader]
- private void load()
+ [Test]
+ public void DrumrollTest()
{
AddStep("Drum roll", () => SetContents(_ =>
{
diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableDrumRollKiai.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableDrumRollKiai.cs
new file mode 100644
index 0000000000..53977150e7
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableDrumRollKiai.cs
@@ -0,0 +1,30 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Beatmaps;
+
+namespace osu.Game.Rulesets.Taiko.Tests.Skinning
+{
+ [TestFixture]
+ public class TestSceneDrawableDrumRollKiai : TestSceneDrawableDrumRoll
+ {
+ [SetUp]
+ public void SetUp() => Schedule(() =>
+ {
+ var controlPointInfo = new ControlPointInfo();
+
+ controlPointInfo.Add(0, new TimingControlPoint { BeatLength = 500 });
+ controlPointInfo.Add(0, new EffectControlPoint { KiaiMode = true });
+
+ Beatmap.Value = CreateWorkingBeatmap(new Beatmap
+ {
+ ControlPointInfo = controlPointInfo
+ });
+
+ // track needs to be playing for BeatSyncedContainer to work.
+ Beatmap.Value.Track.Start();
+ });
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableHit.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableHit.cs
index d5a97f8f88..8e9c487c2f 100644
--- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableHit.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableHit.cs
@@ -4,7 +4,6 @@
#nullable disable
using NUnit.Framework;
-using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
@@ -16,8 +15,8 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
[TestFixture]
public class TestSceneDrawableHit : TaikoSkinnableTestScene
{
- [BackgroundDependencyLoader]
- private void load()
+ [Test]
+ public void TestHits()
{
AddStep("Centre hit", () => SetContents(_ => new DrawableHit(createHitAtCurrentTime())
{
diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableHitKiai.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableHitKiai.cs
new file mode 100644
index 0000000000..fac0530749
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableHitKiai.cs
@@ -0,0 +1,30 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
+
+namespace osu.Game.Rulesets.Taiko.Tests.Skinning
+{
+ [TestFixture]
+ public class TestSceneDrawableHitKiai : TestSceneDrawableHit
+ {
+ [SetUp]
+ public void SetUp() => Schedule(() =>
+ {
+ var controlPointInfo = new ControlPointInfo();
+
+ controlPointInfo.Add(0, new TimingControlPoint { BeatLength = 500 });
+ controlPointInfo.Add(0, new EffectControlPoint { KiaiMode = true });
+
+ Beatmap.Value = CreateWorkingBeatmap(new Beatmap
+ {
+ ControlPointInfo = controlPointInfo
+ });
+
+ // track needs to be playing for BeatSyncedContainer to work.
+ Beatmap.Value.Track.Start();
+ });
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs b/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs
index df1450bf77..863a2c9eac 100644
--- a/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs
@@ -27,6 +27,12 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints
};
}
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ BeginPlacement();
+ }
+
protected override bool OnMouseDown(MouseDownEvent e)
{
switch (e.Button)
diff --git a/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs b/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs
index 23a005190a..70364cabf1 100644
--- a/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs
@@ -52,6 +52,12 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints
private double originalStartTime;
private Vector2 originalPosition;
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ BeginPlacement();
+ }
+
protected override bool OnMouseDown(MouseDownEvent e)
{
if (e.Button != MouseButton.Left)
diff --git a/osu.Game.Rulesets.Taiko/Edit/TaikoHitObjectComposer.cs b/osu.Game.Rulesets.Taiko/Edit/TaikoHitObjectComposer.cs
index 1c1a5c325f..161799c980 100644
--- a/osu.Game.Rulesets.Taiko/Edit/TaikoHitObjectComposer.cs
+++ b/osu.Game.Rulesets.Taiko/Edit/TaikoHitObjectComposer.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System.Collections.Generic;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
diff --git a/osu.Game.Rulesets.Taiko/Skinning/Default/CirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Default/CirclePiece.cs
index a7ab1bcd4a..6b5a9ae6d2 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/Default/CirclePiece.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/Default/CirclePiece.cs
@@ -3,6 +3,7 @@
#nullable disable
+using osu.Framework.Allocation;
using osu.Framework.Audio.Track;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
@@ -13,6 +14,7 @@ using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics;
using osu.Game.Graphics.Backgrounds;
using osu.Game.Graphics.Containers;
+using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Taiko.Objects;
using osuTK.Graphics;
@@ -32,6 +34,8 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default
private const double pre_beat_transition_time = 80;
+ private const float flash_opacity = 0.3f;
+
private Color4 accentColour;
///
@@ -152,11 +156,22 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default
};
}
+ [Resolved]
+ private DrawableHitObject drawableHitObject { get; set; }
+
protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes)
{
if (!effectPoint.KiaiMode)
return;
+ if (drawableHitObject.State.Value == ArmedState.Idle)
+ {
+ FlashBox
+ .FadeTo(flash_opacity)
+ .Then()
+ .FadeOut(timingPoint.BeatLength * 0.75, Easing.OutSine);
+ }
+
if (beatIndex % timingPoint.TimeSignature.Numerator != 0)
return;
diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyCirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyCirclePiece.cs
index 399bd9260d..6bbeb0ed4c 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyCirclePiece.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyCirclePiece.cs
@@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
}
// backgroundLayer is guaranteed to exist due to the pre-check in TaikoLegacySkinTransformer.
- AddInternal(backgroundLayer = getDrawableFor("circle"));
+ AddInternal(backgroundLayer = new LegacyKiaiFlashingDrawable(() => getDrawableFor("circle")));
var foregroundLayer = getDrawableFor("circleoverlay");
if (foregroundLayer != null)
diff --git a/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs
index 1e87ed27df..a98f931e7a 100644
--- a/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs
+++ b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs
@@ -71,9 +71,9 @@ namespace osu.Game.Tests.Editing
[TestCase(1)]
[TestCase(2)]
- public void TestSpeedMultiplier(float multiplier)
+ public void TestSpeedMultiplierDoesNotChangeDistanceSnap(float multiplier)
{
- assertSnapDistance(100 * multiplier, new HitObject
+ assertSnapDistance(100, new HitObject
{
DifficultyControlPoint = new DifficultyControlPoint
{
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneScoring.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneScoring.cs
new file mode 100644
index 0000000000..f8b5085a70
--- /dev/null
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneScoring.cs
@@ -0,0 +1,498 @@
+// Copyright (c) ppy Pty Ltd . 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.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Cursor;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Input.Events;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Containers;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Overlays.Settings;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Rulesets.Osu.Beatmaps;
+using osu.Game.Rulesets.Osu.Judgements;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Scoring;
+using osuTK;
+using osuTK.Graphics;
+using osuTK.Input;
+
+namespace osu.Game.Tests.Visual.Gameplay
+{
+ public class TestSceneScoring : OsuTestScene
+ {
+ private GraphContainer graphs = null!;
+ private SettingsSlider sliderMaxCombo = null!;
+
+ private FillFlowContainer legend = null!;
+
+ [Test]
+ public void TestBasic()
+ {
+ AddStep("setup tests", () =>
+ {
+ Children = new Drawable[]
+ {
+ new GridContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ RowDimensions = new[]
+ {
+ new Dimension(),
+ new Dimension(GridSizeMode.AutoSize),
+ new Dimension(GridSizeMode.AutoSize),
+ },
+ Content = new[]
+ {
+ new Drawable[]
+ {
+ graphs = new GraphContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ },
+ },
+ new Drawable[]
+ {
+ legend = new FillFlowContainer
+ {
+ Padding = new MarginPadding(20),
+ Direction = FillDirection.Full,
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ },
+ },
+ new Drawable[]
+ {
+ new FillFlowContainer
+ {
+ Padding = new MarginPadding(20),
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Direction = FillDirection.Full,
+ Children = new Drawable[]
+ {
+ sliderMaxCombo = new SettingsSlider
+ {
+ Width = 0.5f,
+ TransferValueOnCommit = true,
+ Current = new BindableInt(1024)
+ {
+ MinValue = 96,
+ MaxValue = 8192,
+ },
+ LabelText = "max combo",
+ },
+ new OsuTextFlowContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ Width = 0.5f,
+ AutoSizeAxes = Axes.Y,
+ Text = $"Left click to add miss\nRight click to add OK/{base_ok}"
+ }
+ }
+ },
+ },
+ }
+ }
+ };
+
+ sliderMaxCombo.Current.BindValueChanged(_ => rerun());
+
+ graphs.MissLocations.BindCollectionChanged((_, __) => rerun());
+ graphs.NonPerfectLocations.BindCollectionChanged((_, __) => rerun());
+
+ graphs.MaxCombo.BindTo(sliderMaxCombo.Current);
+
+ rerun();
+ });
+ }
+
+ private const int base_great = 300;
+ private const int base_ok = 100;
+
+ private void rerun()
+ {
+ graphs.Clear();
+ legend.Clear();
+
+ runForProcessor("lazer-standardised", Color4.YellowGreen, new ScoreProcessor(new OsuRuleset()) { Mode = { Value = ScoringMode.Standardised } });
+ runForProcessor("lazer-classic", Color4.MediumPurple, new ScoreProcessor(new OsuRuleset()) { Mode = { Value = ScoringMode.Classic } });
+
+ runScoreV1();
+ runScoreV2();
+ }
+
+ private void runScoreV1()
+ {
+ int totalScore = 0;
+ int currentCombo = 0;
+
+ void applyHitV1(int baseScore)
+ {
+ if (baseScore == 0)
+ {
+ currentCombo = 0;
+ return;
+ }
+
+ const float score_multiplier = 1;
+
+ totalScore += baseScore;
+
+ // combo multiplier
+ // ReSharper disable once PossibleLossOfFraction
+ totalScore += (int)(Math.Max(0, currentCombo - 1) * (baseScore / 25 * score_multiplier));
+
+ currentCombo++;
+ }
+
+ runForAlgorithm("ScoreV1 (classic)", Color4.Purple,
+ () => applyHitV1(base_great),
+ () => applyHitV1(base_ok),
+ () => applyHitV1(0),
+ () =>
+ {
+ // Arbitrary value chosen towards the upper range.
+ const double score_multiplier = 4;
+
+ return (int)(totalScore * score_multiplier);
+ });
+ }
+
+ private void runScoreV2()
+ {
+ int maxCombo = sliderMaxCombo.Current.Value;
+
+ int currentCombo = 0;
+ double comboPortion = 0;
+ double currentBaseScore = 0;
+ double maxBaseScore = 0;
+ int currentHits = 0;
+
+ for (int i = 0; i < maxCombo; i++)
+ applyHitV2(base_great);
+
+ double comboPortionMax = comboPortion;
+
+ currentCombo = 0;
+ comboPortion = 0;
+ currentBaseScore = 0;
+ maxBaseScore = 0;
+ currentHits = 0;
+
+ void applyHitV2(int baseScore)
+ {
+ maxBaseScore += base_great;
+ currentBaseScore += baseScore;
+ comboPortion += baseScore * (1 + ++currentCombo / 10.0);
+
+ currentHits++;
+ }
+
+ runForAlgorithm("ScoreV2", Color4.OrangeRed,
+ () => applyHitV2(base_great),
+ () => applyHitV2(base_ok),
+ () =>
+ {
+ currentHits++;
+ maxBaseScore += base_great;
+ currentCombo = 0;
+ }, () =>
+ {
+ double accuracy = currentBaseScore / maxBaseScore;
+
+ return (int)Math.Round
+ (
+ 700000 * comboPortion / comboPortionMax +
+ 300000 * Math.Pow(accuracy, 10) * ((double)currentHits / maxCombo)
+ );
+ });
+ }
+
+ private void runForProcessor(string name, Color4 colour, ScoreProcessor processor)
+ {
+ int maxCombo = sliderMaxCombo.Current.Value;
+
+ var beatmap = new OsuBeatmap();
+ for (int i = 0; i < maxCombo; i++)
+ beatmap.HitObjects.Add(new HitCircle());
+
+ processor.ApplyBeatmap(beatmap);
+
+ runForAlgorithm(name, colour,
+ () => processor.ApplyResult(new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Great }),
+ () => processor.ApplyResult(new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Ok }),
+ () => processor.ApplyResult(new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Miss }),
+ () => (int)processor.TotalScore.Value);
+ }
+
+ private void runForAlgorithm(string name, Color4 colour, Action applyHit, Action applyNonPerfect, Action applyMiss, Func getTotalScore)
+ {
+ int maxCombo = sliderMaxCombo.Current.Value;
+
+ List results = new List();
+
+ for (int i = 0; i < maxCombo; i++)
+ {
+ if (graphs.MissLocations.Contains(i))
+ applyMiss();
+ else if (graphs.NonPerfectLocations.Contains(i))
+ applyNonPerfect();
+ else
+ applyHit();
+
+ results.Add(getTotalScore());
+ }
+
+ graphs.Add(new LineGraph
+ {
+ Name = name,
+ RelativeSizeAxes = Axes.Both,
+ LineColour = colour,
+ Values = results
+ });
+
+ legend.Add(new OsuSpriteText
+ {
+ Colour = colour,
+ RelativeSizeAxes = Axes.X,
+ Width = 0.5f,
+ Text = $"{FontAwesome.Solid.Circle.Icon} {name}"
+ });
+
+ legend.Add(new OsuSpriteText
+ {
+ Colour = colour,
+ RelativeSizeAxes = Axes.X,
+ Width = 0.5f,
+ Text = $"final score {getTotalScore():#,0}"
+ });
+ }
+ }
+
+ public class GraphContainer : Container, IHasCustomTooltip>
+ {
+ public readonly BindableList MissLocations = new BindableList();
+ public readonly BindableList NonPerfectLocations = new BindableList();
+
+ public Bindable MaxCombo = new Bindable();
+
+ protected override Container Content { get; } = new Container { RelativeSizeAxes = Axes.Both };
+
+ private readonly Box hoverLine;
+
+ private readonly Container missLines;
+ private readonly Container verticalGridLines;
+
+ public int CurrentHoverCombo { get; private set; }
+
+ public GraphContainer()
+ {
+ InternalChild = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ Colour = OsuColour.Gray(0.1f),
+ RelativeSizeAxes = Axes.Both,
+ },
+ verticalGridLines = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ },
+ hoverLine = new Box
+ {
+ Colour = Color4.Yellow,
+ RelativeSizeAxes = Axes.Y,
+ Origin = Anchor.TopCentre,
+ Alpha = 0,
+ Width = 1,
+ },
+ missLines = new Container
+ {
+ Alpha = 0.6f,
+ RelativeSizeAxes = Axes.Both,
+ },
+ Content,
+ }
+ };
+
+ MissLocations.BindCollectionChanged((_, _) => updateMissLocations());
+ NonPerfectLocations.BindCollectionChanged((_, _) => updateMissLocations());
+
+ MaxCombo.BindValueChanged(_ =>
+ {
+ updateMissLocations();
+ updateVerticalGridLines();
+ }, true);
+ }
+
+ private void updateVerticalGridLines()
+ {
+ verticalGridLines.Clear();
+
+ for (int i = 0; i < MaxCombo.Value; i++)
+ {
+ if (i % 100 == 0)
+ {
+ verticalGridLines.AddRange(new Drawable[]
+ {
+ new Box
+ {
+ Colour = OsuColour.Gray(0.2f),
+ Origin = Anchor.TopCentre,
+ Width = 1,
+ RelativeSizeAxes = Axes.Y,
+ RelativePositionAxes = Axes.X,
+ X = (float)i / MaxCombo.Value,
+ },
+ new OsuSpriteText
+ {
+ RelativePositionAxes = Axes.X,
+ X = (float)i / MaxCombo.Value,
+ Anchor = Anchor.BottomLeft,
+ Origin = Anchor.BottomLeft,
+ Text = $"{i:#,0}",
+ Rotation = -30,
+ Y = -20,
+ }
+ });
+ }
+ }
+ }
+
+ private void updateMissLocations()
+ {
+ missLines.Clear();
+
+ foreach (int miss in MissLocations)
+ {
+ missLines.Add(new Box
+ {
+ Colour = Color4.Red,
+ Origin = Anchor.TopCentre,
+ Width = 1,
+ RelativeSizeAxes = Axes.Y,
+ RelativePositionAxes = Axes.X,
+ X = (float)miss / MaxCombo.Value,
+ });
+ }
+
+ foreach (int miss in NonPerfectLocations)
+ {
+ missLines.Add(new Box
+ {
+ Colour = Color4.Orange,
+ Origin = Anchor.TopCentre,
+ Width = 1,
+ RelativeSizeAxes = Axes.Y,
+ RelativePositionAxes = Axes.X,
+ X = (float)miss / MaxCombo.Value,
+ });
+ }
+ }
+
+ protected override bool OnHover(HoverEvent e)
+ {
+ hoverLine.Show();
+ return base.OnHover(e);
+ }
+
+ protected override void OnHoverLost(HoverLostEvent e)
+ {
+ hoverLine.Hide();
+ base.OnHoverLost(e);
+ }
+
+ protected override bool OnMouseMove(MouseMoveEvent e)
+ {
+ CurrentHoverCombo = (int)(e.MousePosition.X / DrawWidth * MaxCombo.Value);
+
+ hoverLine.X = e.MousePosition.X;
+ return base.OnMouseMove(e);
+ }
+
+ protected override bool OnMouseDown(MouseDownEvent e)
+ {
+ if (e.Button == MouseButton.Left)
+ MissLocations.Add(CurrentHoverCombo);
+ else
+ NonPerfectLocations.Add(CurrentHoverCombo);
+
+ return true;
+ }
+
+ private GraphTooltip? tooltip;
+
+ public ITooltip> GetCustomTooltip() => tooltip ??= new GraphTooltip(this);
+
+ public IEnumerable TooltipContent => Content.OfType();
+
+ public class GraphTooltip : CompositeDrawable, ITooltip>
+ {
+ private readonly GraphContainer graphContainer;
+
+ private readonly OsuTextFlowContainer textFlow;
+
+ public GraphTooltip(GraphContainer graphContainer)
+ {
+ this.graphContainer = graphContainer;
+ AutoSizeAxes = Axes.Both;
+
+ Masking = true;
+ CornerRadius = 10;
+
+ InternalChildren = new Drawable[]
+ {
+ new Box
+ {
+ Colour = OsuColour.Gray(0.15f),
+ RelativeSizeAxes = Axes.Both,
+ },
+ textFlow = new OsuTextFlowContainer
+ {
+ Colour = Color4.White,
+ AutoSizeAxes = Axes.Both,
+ Padding = new MarginPadding(10),
+ }
+ };
+ }
+
+ private int? lastContentCombo;
+
+ public void SetContent(IEnumerable content)
+ {
+ int relevantCombo = graphContainer.CurrentHoverCombo;
+
+ if (lastContentCombo == relevantCombo)
+ return;
+
+ lastContentCombo = relevantCombo;
+ textFlow.Clear();
+
+ textFlow.AddParagraph($"At combo {relevantCombo}:");
+
+ foreach (var graph in content)
+ {
+ float valueAtHover = graph.Values.ElementAt(relevantCombo);
+ float ofTotal = valueAtHover / graph.Values.Last();
+
+ textFlow.AddParagraph($"{graph.Name}: {valueAtHover:#,0} ({ofTotal * 100:N0}% of final)\n", st => st.Colour = graph.LineColour);
+ }
+ }
+
+ public void Move(Vector2 pos) => this.MoveTo(pos);
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs
index 156a1ee34a..6d036f8e9b 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs
@@ -214,7 +214,7 @@ namespace osu.Game.Tests.Visual.Gameplay
private void addControlPoints(IList controlPoints, double sequenceStartTime)
{
- controlPoints.ForEach(point => point.StartTime += sequenceStartTime);
+ controlPoints.ForEach(point => point.Time += sequenceStartTime);
scrollContainers.ForEach(container =>
{
@@ -224,7 +224,7 @@ namespace osu.Game.Tests.Visual.Gameplay
foreach (var playfield in playfields)
{
foreach (var controlPoint in controlPoints)
- playfield.Add(createDrawablePoint(playfield, controlPoint.StartTime));
+ playfield.Add(createDrawablePoint(playfield, controlPoint.Time));
}
}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs
index f31261dc1f..63677ce378 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs
@@ -97,14 +97,23 @@ namespace osu.Game.Tests.Visual.Multiplayer
}
[Test]
- public void TestCurrentItemDoesNotHaveDeleteButton()
+ public void TestSingleItemDoesNotHaveDeleteButton()
+ {
+ AddStep("set all players queue mode", () => MultiplayerClient.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers }).WaitSafely());
+ AddUntilStep("wait for queue mode change", () => MultiplayerClient.ClientAPIRoom?.QueueMode.Value == QueueMode.AllPlayers);
+
+ assertDeleteButtonVisibility(0, false);
+ }
+
+ [Test]
+ public void TestCurrentItemHasDeleteButtonIfNotSingle()
{
AddStep("set all players queue mode", () => MultiplayerClient.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers }).WaitSafely());
AddUntilStep("wait for queue mode change", () => MultiplayerClient.ClientAPIRoom?.QueueMode.Value == QueueMode.AllPlayers);
addPlaylistItem(() => API.LocalUser.Value.OnlineID);
- assertDeleteButtonVisibility(0, false);
+ assertDeleteButtonVisibility(0, true);
assertDeleteButtonVisibility(1, true);
AddStep("finish current item", () => MultiplayerClient.FinishCurrentItem().WaitSafely());
diff --git a/osu.Game.Tests/Visual/Settings/TestSceneDirectorySelector.cs b/osu.Game.Tests/Visual/Settings/TestSceneDirectorySelector.cs
index 4f05194e08..16110e5595 100644
--- a/osu.Game.Tests/Visual/Settings/TestSceneDirectorySelector.cs
+++ b/osu.Game.Tests/Visual/Settings/TestSceneDirectorySelector.cs
@@ -3,18 +3,17 @@
#nullable disable
-using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Graphics.UserInterfaceV2;
+using osu.Game.Tests.Visual.UserInterface;
namespace osu.Game.Tests.Visual.Settings
{
- public class TestSceneDirectorySelector : OsuTestScene
+ public class TestSceneDirectorySelector : ThemeComparisonTestScene
{
- [BackgroundDependencyLoader]
- private void load()
+ protected override Drawable CreateContent() => new OsuDirectorySelector
{
- Add(new OsuDirectorySelector { RelativeSizeAxes = Axes.Both });
- }
+ RelativeSizeAxes = Axes.Both
+ };
}
}
diff --git a/osu.Game.Tests/Visual/Settings/TestSceneFileSelector.cs b/osu.Game.Tests/Visual/Settings/TestSceneFileSelector.cs
index 6f25012bfa..97bf0d212a 100644
--- a/osu.Game.Tests/Visual/Settings/TestSceneFileSelector.cs
+++ b/osu.Game.Tests/Visual/Settings/TestSceneFileSelector.cs
@@ -4,23 +4,43 @@
#nullable disable
using NUnit.Framework;
+using osu.Framework.Allocation;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Graphics;
using osu.Game.Graphics.UserInterfaceV2;
+using osu.Game.Tests.Visual.UserInterface;
namespace osu.Game.Tests.Visual.Settings
{
- public class TestSceneFileSelector : OsuTestScene
+ public class TestSceneFileSelector : ThemeComparisonTestScene
{
- [Test]
- public void TestAllFiles()
- {
- AddStep("create", () => Child = new OsuFileSelector { RelativeSizeAxes = Axes.Both });
- }
+ [Resolved]
+ private OsuColour colours { get; set; }
[Test]
public void TestJpgFilesOnly()
{
- AddStep("create", () => Child = new OsuFileSelector(validFileExtensions: new[] { ".jpg" }) { RelativeSizeAxes = Axes.Both });
+ AddStep("create", () =>
+ {
+ Cell(0, 0).Children = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = colours.GreySeaFoam
+ },
+ new OsuFileSelector(validFileExtensions: new[] { ".jpg" })
+ {
+ RelativeSizeAxes = Axes.Both,
+ },
+ };
+ });
}
+
+ protected override Drawable CreateContent() => new OsuFileSelector
+ {
+ RelativeSizeAxes = Axes.Both,
+ };
}
}
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneCursors.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneCursors.cs
index 6c0191ae27..75c47f0b1b 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneCursors.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneCursors.cs
@@ -178,6 +178,7 @@ namespace osu.Game.Tests.Visual.UserInterface
[Test]
public void TestKeyboardLocalCursor([Values] bool clickToShow)
{
+ AddStep("Enable cursor hiding", () => globalCursorDisplay.MenuCursor.HideCursorOnNonMouseInput = true);
AddStep("Move to purple area", () => InputManager.MoveMouseTo(cursorBoxes[3].ScreenSpaceDrawQuad.Centre + new Vector2(10, 0)));
AddAssert("Check purple cursor visible", () => checkVisible(cursorBoxes[3].Cursor));
AddAssert("Check global cursor alpha is 1", () => globalCursorDisplay.MenuCursor.Alpha == 1);
@@ -201,6 +202,7 @@ namespace osu.Game.Tests.Visual.UserInterface
[Test]
public void TestKeyboardUserCursor([Values] bool clickToShow)
{
+ AddStep("Enable cursor hiding", () => globalCursorDisplay.MenuCursor.HideCursorOnNonMouseInput = true);
AddStep("Move to green area", () => InputManager.MoveMouseTo(cursorBoxes[0]));
AddAssert("Check green cursor visible", () => checkVisible(cursorBoxes[0].Cursor));
AddAssert("Check global cursor alpha is 0", () => !checkVisible(globalCursorDisplay.MenuCursor) && globalCursorDisplay.MenuCursor.ActiveCursor.Alpha == 0);
diff --git a/osu.Game.Tournament/JsonPointConverter.cs b/osu.Game.Tournament/JsonPointConverter.cs
index db48c36c99..d3b40a3526 100644
--- a/osu.Game.Tournament/JsonPointConverter.cs
+++ b/osu.Game.Tournament/JsonPointConverter.cs
@@ -6,6 +6,7 @@
using System;
using System.Diagnostics;
using System.Drawing;
+using System.Globalization;
using Newtonsoft.Json;
namespace osu.Game.Tournament
@@ -31,7 +32,9 @@ namespace osu.Game.Tournament
Debug.Assert(str != null);
- return new PointConverter().ConvertFromString(str) as Point? ?? new Point();
+ // Null check suppression is required due to .NET standard expecting a non-null context.
+ // Seems to work fine at a runtime level (and the parameter is nullable in .NET 6+).
+ return new PointConverter().ConvertFromString(null!, CultureInfo.InvariantCulture, str) as Point? ?? new Point();
}
var point = new Point();
diff --git a/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs
index 1bc929604d..348661e2a3 100644
--- a/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs
+++ b/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs
@@ -239,17 +239,17 @@ namespace osu.Game.Tournament.Screens.Editors
var req = new GetBeatmapRequest(new APIBeatmap { OnlineID = Model.ID });
- req.Success += res =>
+ req.Success += res => Schedule(() =>
{
Model.Beatmap = new TournamentBeatmap(res);
updatePanel();
- };
+ });
- req.Failure += _ =>
+ req.Failure += _ => Schedule(() =>
{
Model.Beatmap = null;
updatePanel();
- };
+ });
API.Queue(req);
}, true);
diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs
index 56a432aec4..0a09e6e7e6 100644
--- a/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs
+++ b/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs
@@ -9,11 +9,8 @@ using osuTK.Graphics;
namespace osu.Game.Beatmaps.ControlPoints
{
- public abstract class ControlPoint : IComparable, IDeepCloneable, IEquatable
+ public abstract class ControlPoint : IComparable, IDeepCloneable, IEquatable, IControlPoint
{
- ///
- /// The time at which the control point takes effect.
- ///
[JsonIgnore]
public double Time { get; set; }
diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs
index 4be6b5eede..422e306450 100644
--- a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs
+++ b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs
@@ -196,8 +196,8 @@ namespace osu.Game.Beatmaps.ControlPoints
/// The time to find the control point at.
/// The control point to use when is before any control points.
/// The active control point at , or a fallback if none found.
- protected T BinarySearchWithFallback(IReadOnlyList list, double time, T fallback)
- where T : ControlPoint
+ public static T BinarySearchWithFallback(IReadOnlyList list, double time, T fallback)
+ where T : class, IControlPoint
{
return BinarySearch(list, time) ?? fallback;
}
@@ -207,9 +207,9 @@ namespace osu.Game.Beatmaps.ControlPoints
///
/// The list to search.
/// The time to find the control point at.
- /// The active control point at .
- protected virtual T BinarySearch(IReadOnlyList list, double time)
- where T : ControlPoint
+ /// The active control point at . Will return null if there are no control points, or if the time is before the first control point.
+ public static T BinarySearch(IReadOnlyList list, double time)
+ where T : class, IControlPoint
{
if (list == null)
throw new ArgumentNullException(nameof(list));
diff --git a/osu.Game/Beatmaps/ControlPoints/IControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/IControlPoint.cs
new file mode 100644
index 0000000000..091e99e029
--- /dev/null
+++ b/osu.Game/Beatmaps/ControlPoints/IControlPoint.cs
@@ -0,0 +1,13 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+namespace osu.Game.Beatmaps.ControlPoints
+{
+ public interface IControlPoint
+ {
+ ///
+ /// The time at which the control point takes effect.
+ ///
+ double Time { get; }
+ }
+}
diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
index 75500fbc4e..5f5749dc73 100644
--- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
@@ -355,6 +355,14 @@ namespace osu.Game.Beatmaps.Formats
switch (type)
{
+ case LegacyEventType.Sprite:
+ // Generally, the background is the first thing defined in a beatmap file.
+ // In some older beatmaps, it is not present and replaced by a storyboard-level background instead.
+ // Allow the first sprite (by file order) to act as the background in such cases.
+ if (string.IsNullOrEmpty(beatmap.BeatmapInfo.Metadata.BackgroundFile))
+ beatmap.BeatmapInfo.Metadata.BackgroundFile = CleanFilename(split[3]);
+ break;
+
case LegacyEventType.Background:
beatmap.BeatmapInfo.Metadata.BackgroundFile = CleanFilename(split[2]);
break;
diff --git a/osu.Game/Graphics/Cursor/MenuCursorContainer.cs b/osu.Game/Graphics/Cursor/MenuCursorContainer.cs
index 33b0c308cc..af542989ff 100644
--- a/osu.Game/Graphics/Cursor/MenuCursorContainer.cs
+++ b/osu.Game/Graphics/Cursor/MenuCursorContainer.cs
@@ -23,6 +23,21 @@ namespace osu.Game.Graphics.Cursor
private readonly IBindable screenshotCursorVisibility = new Bindable(true);
public override bool IsPresent => screenshotCursorVisibility.Value && base.IsPresent;
+ private bool hideCursorOnNonMouseInput;
+
+ public bool HideCursorOnNonMouseInput
+ {
+ get => hideCursorOnNonMouseInput;
+ set
+ {
+ if (hideCursorOnNonMouseInput == value)
+ return;
+
+ hideCursorOnNonMouseInput = value;
+ updateState();
+ }
+ }
+
protected override Drawable CreateCursor() => activeCursor = new Cursor();
private Cursor activeCursor = null!;
@@ -75,7 +90,7 @@ namespace osu.Game.Graphics.Cursor
private void updateState()
{
- bool combinedVisibility = State.Value == Visibility.Visible && lastInputWasMouse.Value && !isIdle.Value;
+ bool combinedVisibility = State.Value == Visibility.Visible && (lastInputWasMouse.Value || !hideCursorOnNonMouseInput) && !isIdle.Value;
if (visible == combinedVisibility)
return;
@@ -262,14 +277,19 @@ namespace osu.Game.Graphics.Cursor
{
switch (e)
{
- case MouseEvent:
+ case MouseDownEvent:
+ case MouseMoveEvent:
lastInputWasMouseSource.Value = true;
return false;
- default:
+ case KeyDownEvent keyDown when !keyDown.Repeat:
+ case JoystickPressEvent:
+ case MidiDownEvent:
lastInputWasMouseSource.Value = false;
return false;
}
+
+ return false;
}
}
diff --git a/osu.Game/Graphics/UserInterface/HoverSampleSet.cs b/osu.Game/Graphics/UserInterface/HoverSampleSet.cs
index 6358317e9d..f0ff76b35d 100644
--- a/osu.Game/Graphics/UserInterface/HoverSampleSet.cs
+++ b/osu.Game/Graphics/UserInterface/HoverSampleSet.cs
@@ -15,6 +15,9 @@ namespace osu.Game.Graphics.UserInterface
[Description("button")]
Button,
+ [Description("button-sidebar")]
+ ButtonSidebar,
+
[Description("toolbar")]
Toolbar,
diff --git a/osu.Game/Graphics/UserInterface/OsuCheckbox.cs b/osu.Game/Graphics/UserInterface/OsuCheckbox.cs
index bbd8f8ecea..8772c1e2d9 100644
--- a/osu.Game/Graphics/UserInterface/OsuCheckbox.cs
+++ b/osu.Game/Graphics/UserInterface/OsuCheckbox.cs
@@ -26,24 +26,24 @@ namespace osu.Game.Graphics.UserInterface
{
set
{
- if (labelText != null)
- labelText.Text = value;
+ if (LabelTextFlowContainer != null)
+ LabelTextFlowContainer.Text = value;
}
}
public MarginPadding LabelPadding
{
- get => labelText?.Padding ?? new MarginPadding();
+ get => LabelTextFlowContainer?.Padding ?? new MarginPadding();
set
{
- if (labelText != null)
- labelText.Padding = value;
+ if (LabelTextFlowContainer != null)
+ LabelTextFlowContainer.Padding = value;
}
}
protected readonly Nub Nub;
- private readonly OsuTextFlowContainer labelText;
+ protected readonly OsuTextFlowContainer LabelTextFlowContainer;
private Sample sampleChecked;
private Sample sampleUnchecked;
@@ -56,7 +56,7 @@ namespace osu.Game.Graphics.UserInterface
Children = new Drawable[]
{
- labelText = new OsuTextFlowContainer(ApplyLabelParameters)
+ LabelTextFlowContainer = new OsuTextFlowContainer(ApplyLabelParameters)
{
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
@@ -70,19 +70,19 @@ namespace osu.Game.Graphics.UserInterface
Nub.Anchor = Anchor.CentreRight;
Nub.Origin = Anchor.CentreRight;
Nub.Margin = new MarginPadding { Right = nub_padding };
- labelText.Padding = new MarginPadding { Right = Nub.EXPANDED_SIZE + nub_padding * 2 };
+ LabelTextFlowContainer.Padding = new MarginPadding { Right = Nub.EXPANDED_SIZE + nub_padding * 2 };
}
else
{
Nub.Anchor = Anchor.CentreLeft;
Nub.Origin = Anchor.CentreLeft;
Nub.Margin = new MarginPadding { Left = nub_padding };
- labelText.Padding = new MarginPadding { Left = Nub.EXPANDED_SIZE + nub_padding * 2 };
+ LabelTextFlowContainer.Padding = new MarginPadding { Left = Nub.EXPANDED_SIZE + nub_padding * 2 };
}
Nub.Current.BindTo(Current);
- Current.DisabledChanged += disabled => labelText.Alpha = Nub.Alpha = disabled ? 0.3f : 1;
+ Current.DisabledChanged += disabled => LabelTextFlowContainer.Alpha = Nub.Alpha = disabled ? 0.3f : 1;
}
///
diff --git a/osu.Game/Graphics/UserInterface/OsuSliderBar.cs b/osu.Game/Graphics/UserInterface/OsuSliderBar.cs
index 2a8b41fd20..9acb0c7f94 100644
--- a/osu.Game/Graphics/UserInterface/OsuSliderBar.cs
+++ b/osu.Game/Graphics/UserInterface/OsuSliderBar.cs
@@ -44,6 +44,8 @@ namespace osu.Game.Graphics.UserInterface
public virtual LocalisableString TooltipText { get; private set; }
+ public bool PlaySamplesOnAdjust { get; set; } = true;
+
///
/// Whether to format the tooltip as a percentage or the actual value.
///
@@ -187,6 +189,9 @@ namespace osu.Game.Graphics.UserInterface
private void playSample(T value)
{
+ if (!PlaySamplesOnAdjust)
+ return;
+
if (Clock == null || Clock.CurrentTime - lastSampleTime <= 30)
return;
diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelector.cs b/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelector.cs
index 42e1073baf..0e348108aa 100644
--- a/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelector.cs
+++ b/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelector.cs
@@ -31,6 +31,8 @@ namespace osu.Game.Graphics.UserInterfaceV2
protected override DirectorySelectorBreadcrumbDisplay CreateBreadcrumb() => new OsuDirectorySelectorBreadcrumbDisplay();
+ protected override Drawable CreateHiddenToggleButton() => new OsuDirectorySelectorHiddenToggle { Current = { BindTarget = ShowHiddenItems } };
+
protected override DirectorySelectorDirectory CreateParentDirectoryItem(DirectoryInfo directory) => new OsuDirectorySelectorParentDirectory(directory);
protected override DirectorySelectorDirectory CreateDirectoryItem(DirectoryInfo directory, string displayName = null) => new OsuDirectorySelectorDirectory(directory, displayName);
diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorHiddenToggle.cs b/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorHiddenToggle.cs
new file mode 100644
index 0000000000..7aaf12ca34
--- /dev/null
+++ b/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorHiddenToggle.cs
@@ -0,0 +1,38 @@
+// Copyright (c) ppy Pty Ltd . 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.Game.Graphics.UserInterface;
+using osu.Game.Overlays;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Graphics.UserInterfaceV2
+{
+ internal class OsuDirectorySelectorHiddenToggle : OsuCheckbox
+ {
+ public OsuDirectorySelectorHiddenToggle()
+ {
+ RelativeSizeAxes = Axes.None;
+ AutoSizeAxes = Axes.None;
+ Size = new Vector2(100, 50);
+ Anchor = Anchor.CentreLeft;
+ Origin = Anchor.CentreLeft;
+ LabelTextFlowContainer.Anchor = Anchor.CentreLeft;
+ LabelTextFlowContainer.Origin = Anchor.CentreLeft;
+ LabelText = @"Show hidden";
+ }
+
+ [BackgroundDependencyLoader(true)]
+ private void load(OverlayColourProvider? overlayColourProvider, OsuColour colours)
+ {
+ if (overlayColourProvider != null)
+ return;
+
+ Nub.AccentColour = colours.GreySeaFoamLighter;
+ Nub.GlowingAccentColour = Color4.White;
+ Nub.GlowColour = Color4.White;
+ }
+ }
+}
diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuFileSelector.cs b/osu.Game/Graphics/UserInterfaceV2/OsuFileSelector.cs
index 3e8b7dc209..70af68d595 100644
--- a/osu.Game/Graphics/UserInterfaceV2/OsuFileSelector.cs
+++ b/osu.Game/Graphics/UserInterfaceV2/OsuFileSelector.cs
@@ -33,6 +33,8 @@ namespace osu.Game.Graphics.UserInterfaceV2
protected override DirectorySelectorBreadcrumbDisplay CreateBreadcrumb() => new OsuDirectorySelectorBreadcrumbDisplay();
+ protected override Drawable CreateHiddenToggleButton() => new OsuDirectorySelectorHiddenToggle { Current = { BindTarget = ShowHiddenItems } };
+
protected override DirectorySelectorDirectory CreateParentDirectoryItem(DirectoryInfo directory) => new OsuDirectorySelectorParentDirectory(directory);
protected override DirectorySelectorDirectory CreateDirectoryItem(DirectoryInfo directory, string displayName = null) => new OsuDirectorySelectorDirectory(directory, displayName);
diff --git a/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs b/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs
index 2c9f250028..4469d50acb 100644
--- a/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs
+++ b/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs
@@ -114,6 +114,7 @@ namespace osu.Game.Online.API.Requests.Responses
[JsonProperty("has_replay")]
public bool HasReplay { get; set; }
+ // These properties are calculated or not relevant to any external usage.
public bool ShouldSerializeID() => false;
public bool ShouldSerializeUser() => false;
public bool ShouldSerializeBeatmap() => false;
@@ -122,6 +123,18 @@ namespace osu.Game.Online.API.Requests.Responses
public bool ShouldSerializeOnlineID() => false;
public bool ShouldSerializeHasReplay() => false;
+ // These fields only need to be serialised if they hold values.
+ // Generally this is required because this model may be used by server-side components, but
+ // we don't want to bother sending these fields in score submission requests, for instance.
+ public bool ShouldSerializeEndedAt() => EndedAt != default;
+ public bool ShouldSerializeStartedAt() => StartedAt != default;
+ public bool ShouldSerializeLegacyScoreId() => LegacyScoreId != null;
+ public bool ShouldSerializeLegacyTotalScore() => LegacyTotalScore != null;
+ public bool ShouldSerializeMods() => Mods.Length > 0;
+ public bool ShouldSerializeUserID() => UserID > 0;
+ public bool ShouldSerializeBeatmapID() => BeatmapID > 0;
+ public bool ShouldSerializeBuildID() => BuildID != null;
+
#endregion
public override string ToString() => $"score_id: {ID} user_id: {UserID}";
diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index b3eaf5cd01..2bdcb57f2a 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -1333,6 +1333,8 @@ namespace osu.Game
OverlayActivationMode.BindTo(newOsuScreen.OverlayActivationMode);
API.Activity.BindTo(newOsuScreen.Activity);
+ GlobalCursorDisplay.MenuCursor.HideCursorOnNonMouseInput = newOsuScreen.HideMenuCursorOnNonMouseInput;
+
if (newOsuScreen.HideOverlaysOnEnter)
CloseAllOverlays();
else
diff --git a/osu.Game/Overlays/Settings/Sections/Audio/VolumeSettings.cs b/osu.Game/Overlays/Settings/Sections/Audio/VolumeSettings.cs
index cea8fdd733..8f188f04d9 100644
--- a/osu.Game/Overlays/Settings/Sections/Audio/VolumeSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Audio/VolumeSettings.cs
@@ -8,6 +8,7 @@ using osu.Framework.Audio;
using osu.Framework.Graphics;
using osu.Framework.Localisation;
using osu.Game.Configuration;
+using osu.Game.Graphics.UserInterface;
using osu.Game.Localisation;
namespace osu.Game.Overlays.Settings.Sections.Audio
@@ -21,7 +22,7 @@ namespace osu.Game.Overlays.Settings.Sections.Audio
{
Children = new Drawable[]
{
- new SettingsSlider
+ new VolumeAdjustSlider
{
LabelText = AudioSettingsStrings.MasterVolume,
Current = audio.Volume,
@@ -35,14 +36,15 @@ namespace osu.Game.Overlays.Settings.Sections.Audio
KeyboardStep = 0.01f,
DisplayAsPercentage = true
},
- new SettingsSlider
+ new VolumeAdjustSlider
{
LabelText = AudioSettingsStrings.EffectVolume,
Current = audio.VolumeSample,
KeyboardStep = 0.01f,
DisplayAsPercentage = true
},
- new SettingsSlider
+
+ new VolumeAdjustSlider
{
LabelText = AudioSettingsStrings.MusicVolume,
Current = audio.VolumeTrack,
@@ -51,5 +53,15 @@ namespace osu.Game.Overlays.Settings.Sections.Audio
},
};
}
+
+ private class VolumeAdjustSlider : SettingsSlider
+ {
+ protected override Drawable CreateControl()
+ {
+ var sliderBar = (OsuSliderBar)base.CreateControl();
+ sliderBar.PlaySamplesOnAdjust = false;
+ return sliderBar;
+ }
+ }
}
}
diff --git a/osu.Game/Overlays/Settings/SidebarButton.cs b/osu.Game/Overlays/Settings/SidebarButton.cs
index c6a4cbbcaa..2c4832c68a 100644
--- a/osu.Game/Overlays/Settings/SidebarButton.cs
+++ b/osu.Game/Overlays/Settings/SidebarButton.cs
@@ -16,6 +16,11 @@ namespace osu.Game.Overlays.Settings
[Resolved]
protected OverlayColourProvider ColourProvider { get; private set; }
+ protected SidebarButton()
+ : base(HoverSampleSet.ButtonSidebar)
+ {
+ }
+
[BackgroundDependencyLoader]
private void load()
{
diff --git a/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs b/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs
index feedad7f25..449996131d 100644
--- a/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs
+++ b/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs
@@ -148,7 +148,7 @@ namespace osu.Game.Rulesets.Edit
public virtual float GetBeatSnapDistanceAt(HitObject referenceObject)
{
- return (float)(100 * EditorBeatmap.Difficulty.SliderMultiplier * referenceObject.DifficultyControlPoint.SliderVelocity / BeatSnapProvider.BeatDivisor);
+ return (float)(100 * EditorBeatmap.Difficulty.SliderMultiplier * 1 / BeatSnapProvider.BeatDivisor);
}
public virtual float DurationToDistance(HitObject referenceObject, double duration)
diff --git a/osu.Game/Rulesets/Timing/MultiplierControlPoint.cs b/osu.Game/Rulesets/Timing/MultiplierControlPoint.cs
index 1e80bd165b..279de2f940 100644
--- a/osu.Game/Rulesets/Timing/MultiplierControlPoint.cs
+++ b/osu.Game/Rulesets/Timing/MultiplierControlPoint.cs
@@ -11,12 +11,12 @@ namespace osu.Game.Rulesets.Timing
///
/// A control point which adds an aggregated multiplier based on the provided 's BeatLength and 's SpeedMultiplier.
///
- public class MultiplierControlPoint : IComparable
+ public class MultiplierControlPoint : IComparable, IControlPoint
{
///
/// The time in milliseconds at which this starts.
///
- public double StartTime;
+ public double Time { get; set; }
///
/// The aggregate multiplier which this provides.
@@ -54,13 +54,13 @@ namespace osu.Game.Rulesets.Timing
///
/// Creates a .
///
- /// The start time of this .
- public MultiplierControlPoint(double startTime)
+ /// The start time of this .
+ public MultiplierControlPoint(double time)
{
- StartTime = startTime;
+ Time = time;
}
// ReSharper disable once ImpureMethodCallOnReadonlyValueField
- public int CompareTo(MultiplierControlPoint other) => StartTime.CompareTo(other?.StartTime);
+ public int CompareTo(MultiplierControlPoint other) => Time.CompareTo(other?.Time);
}
}
diff --git a/osu.Game/Rulesets/UI/Scrolling/Algorithms/ConstantScrollAlgorithm.cs b/osu.Game/Rulesets/UI/Scrolling/Algorithms/ConstantScrollAlgorithm.cs
index 0bd8aa64c9..c957a84eb1 100644
--- a/osu.Game/Rulesets/UI/Scrolling/Algorithms/ConstantScrollAlgorithm.cs
+++ b/osu.Game/Rulesets/UI/Scrolling/Algorithms/ConstantScrollAlgorithm.cs
@@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.UI.Scrolling.Algorithms
return -PositionAt(startTime, endTime, timeRange, scrollLength);
}
- public float PositionAt(double time, double currentTime, double timeRange, float scrollLength)
+ public float PositionAt(double time, double currentTime, double timeRange, float scrollLength, double? originTime = null)
=> (float)((time - currentTime) / timeRange * scrollLength);
public double TimeAt(float position, double currentTime, double timeRange, float scrollLength)
diff --git a/osu.Game/Rulesets/UI/Scrolling/Algorithms/IScrollAlgorithm.cs b/osu.Game/Rulesets/UI/Scrolling/Algorithms/IScrollAlgorithm.cs
index d2fb9e3531..f78509f919 100644
--- a/osu.Game/Rulesets/UI/Scrolling/Algorithms/IScrollAlgorithm.cs
+++ b/osu.Game/Rulesets/UI/Scrolling/Algorithms/IScrollAlgorithm.cs
@@ -53,8 +53,9 @@ namespace osu.Game.Rulesets.UI.Scrolling.Algorithms
/// The current time.
/// The amount of visible time.
/// The absolute spatial length through .
+ /// The time to be used for control point lookups (ie. the parent's start time for nested hit objects).
/// The absolute spatial position.
- float PositionAt(double time, double currentTime, double timeRange, float scrollLength);
+ float PositionAt(double time, double currentTime, double timeRange, float scrollLength, double? originTime = null);
///
/// Computes the time which brings a point to a provided spatial position given the current time.
@@ -63,7 +64,7 @@ namespace osu.Game.Rulesets.UI.Scrolling.Algorithms
/// The current time.
/// The amount of visible time.
/// The absolute spatial length through .
- /// The time at which == .
+ /// The time at which == .
double TimeAt(float position, double currentTime, double timeRange, float scrollLength);
///
diff --git a/osu.Game/Rulesets/UI/Scrolling/Algorithms/OverlappingScrollAlgorithm.cs b/osu.Game/Rulesets/UI/Scrolling/Algorithms/OverlappingScrollAlgorithm.cs
index d41117bce8..54079c7895 100644
--- a/osu.Game/Rulesets/UI/Scrolling/Algorithms/OverlappingScrollAlgorithm.cs
+++ b/osu.Game/Rulesets/UI/Scrolling/Algorithms/OverlappingScrollAlgorithm.cs
@@ -4,22 +4,20 @@
#nullable disable
using System;
+using System.Linq;
using osu.Framework.Lists;
+using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Timing;
namespace osu.Game.Rulesets.UI.Scrolling.Algorithms
{
public class OverlappingScrollAlgorithm : IScrollAlgorithm
{
- private readonly MultiplierControlPoint searchPoint;
-
private readonly SortedList controlPoints;
public OverlappingScrollAlgorithm(SortedList controlPoints)
{
this.controlPoints = controlPoints;
-
- searchPoint = new MultiplierControlPoint();
}
public double GetDisplayStartTime(double originTime, float offset, double timeRange, float scrollLength)
@@ -37,8 +35,8 @@ namespace osu.Game.Rulesets.UI.Scrolling.Algorithms
return -PositionAt(startTime, endTime, timeRange, scrollLength);
}
- public float PositionAt(double time, double currentTime, double timeRange, float scrollLength)
- => (float)((time - currentTime) / timeRange * controlPointAt(time).Multiplier * scrollLength);
+ public float PositionAt(double time, double currentTime, double timeRange, float scrollLength, double? originTime = null)
+ => (float)((time - currentTime) / timeRange * controlPointAt(originTime ?? time).Multiplier * scrollLength);
public double TimeAt(float position, double currentTime, double timeRange, float scrollLength)
{
@@ -52,7 +50,7 @@ namespace osu.Game.Rulesets.UI.Scrolling.Algorithms
for (; i < controlPoints.Count; i++)
{
float lastPos = pos;
- pos = PositionAt(controlPoints[i].StartTime, currentTime, timeRange, scrollLength);
+ pos = PositionAt(controlPoints[i].Time, currentTime, timeRange, scrollLength);
if (pos > position)
{
@@ -64,7 +62,7 @@ namespace osu.Game.Rulesets.UI.Scrolling.Algorithms
i = Math.Clamp(i, 0, controlPoints.Count - 1);
- return controlPoints[i].StartTime + (position - pos) * timeRange / controlPoints[i].Multiplier / scrollLength;
+ return controlPoints[i].Time + (position - pos) * timeRange / controlPoints[i].Multiplier / scrollLength;
}
public void Reset()
@@ -78,19 +76,11 @@ namespace osu.Game.Rulesets.UI.Scrolling.Algorithms
/// The .
private MultiplierControlPoint controlPointAt(double time)
{
- if (controlPoints.Count == 0)
- return new MultiplierControlPoint(double.NegativeInfinity);
-
- if (time < controlPoints[0].StartTime)
- return controlPoints[0];
-
- searchPoint.StartTime = time;
- int index = controlPoints.BinarySearch(searchPoint);
-
- if (index < 0)
- index = ~index - 1;
-
- return controlPoints[index];
+ return ControlPointInfo.BinarySearch(controlPoints, time)
+ // The standard binary search will fail if there's no control points, or if the time is before the first.
+ // For this method, we want to use the first control point in the latter case.
+ ?? controlPoints.FirstOrDefault()
+ ?? new MultiplierControlPoint(double.NegativeInfinity);
}
}
}
diff --git a/osu.Game/Rulesets/UI/Scrolling/Algorithms/SequentialScrollAlgorithm.cs b/osu.Game/Rulesets/UI/Scrolling/Algorithms/SequentialScrollAlgorithm.cs
index bfddc22573..774beb20c7 100644
--- a/osu.Game/Rulesets/UI/Scrolling/Algorithms/SequentialScrollAlgorithm.cs
+++ b/osu.Game/Rulesets/UI/Scrolling/Algorithms/SequentialScrollAlgorithm.cs
@@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.UI.Scrolling.Algorithms
return (float)(objectLength * scrollLength);
}
- public float PositionAt(double time, double currentTime, double timeRange, float scrollLength)
+ public float PositionAt(double time, double currentTime, double timeRange, float scrollLength, double? originTime = null)
{
double timelineLength = relativePositionAt(time, timeRange) - relativePositionAt(currentTime, timeRange);
return (float)(timelineLength * scrollLength);
@@ -121,7 +121,7 @@ namespace osu.Game.Rulesets.UI.Scrolling.Algorithms
if (controlPoints.Count == 0)
return;
- positionMappings.Add(new PositionMapping(controlPoints[0].StartTime, controlPoints[0]));
+ positionMappings.Add(new PositionMapping(controlPoints[0].Time, controlPoints[0]));
for (int i = 0; i < controlPoints.Count - 1; i++)
{
@@ -129,9 +129,9 @@ namespace osu.Game.Rulesets.UI.Scrolling.Algorithms
var next = controlPoints[i + 1];
// Figure out how much of the time range the duration represents, and adjust it by the speed multiplier
- float length = (float)((next.StartTime - current.StartTime) / timeRange * current.Multiplier);
+ float length = (float)((next.Time - current.Time) / timeRange * current.Multiplier);
- positionMappings.Add(new PositionMapping(next.StartTime, next, positionMappings[^1].Position + length));
+ positionMappings.Add(new PositionMapping(next.Time, next, positionMappings[^1].Position + length));
}
}
diff --git a/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs b/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs
index 825aba5bc2..68469d083c 100644
--- a/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs
+++ b/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs
@@ -158,9 +158,9 @@ namespace osu.Game.Rulesets.UI.Scrolling
// Trim unwanted sequences of timing changes
timingChanges = timingChanges
// Collapse sections after the last hit object
- .Where(s => s.StartTime <= lastObjectTime)
+ .Where(s => s.Time <= lastObjectTime)
// Collapse sections with the same start time
- .GroupBy(s => s.StartTime).Select(g => g.Last()).OrderBy(s => s.StartTime);
+ .GroupBy(s => s.Time).Select(g => g.Last()).OrderBy(s => s.Time);
ControlPoints.AddRange(timingChanges);
diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs
index 37da157cc1..424fc7c44c 100644
--- a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs
+++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs
@@ -93,9 +93,9 @@ namespace osu.Game.Rulesets.UI.Scrolling
///
/// Given a time, return the position along the scrolling axis within this at time .
///
- public float PositionAtTime(double time, double currentTime)
+ public float PositionAtTime(double time, double currentTime, double? originTime = null)
{
- float scrollPosition = scrollingInfo.Algorithm.PositionAt(time, currentTime, timeRange.Value, scrollLength);
+ float scrollPosition = scrollingInfo.Algorithm.PositionAt(time, currentTime, timeRange.Value, scrollLength, originTime);
return axisInverted ? -scrollPosition : scrollPosition;
}
@@ -236,8 +236,10 @@ namespace osu.Game.Rulesets.UI.Scrolling
entry.LifetimeStart = Math.Min(entry.HitObject.StartTime - judgementOffset, computedStartTime);
}
- private void updateLayoutRecursive(DrawableHitObject hitObject)
+ private void updateLayoutRecursive(DrawableHitObject hitObject, double? parentHitObjectStartTime = null)
{
+ parentHitObjectStartTime ??= hitObject.HitObject.StartTime;
+
if (hitObject.HitObject is IHasDuration e)
{
float length = LengthAtTime(hitObject.HitObject.StartTime, e.EndTime);
@@ -249,17 +251,17 @@ namespace osu.Game.Rulesets.UI.Scrolling
foreach (var obj in hitObject.NestedHitObjects)
{
- updateLayoutRecursive(obj);
+ updateLayoutRecursive(obj, parentHitObjectStartTime);
// Nested hitobjects don't need to scroll, but they do need accurate positions and start lifetime
- updatePosition(obj, hitObject.HitObject.StartTime);
+ updatePosition(obj, hitObject.HitObject.StartTime, parentHitObjectStartTime);
setComputedLifetimeStart(obj.Entry);
}
}
- private void updatePosition(DrawableHitObject hitObject, double currentTime)
+ private void updatePosition(DrawableHitObject hitObject, double currentTime, double? parentHitObjectStartTime = null)
{
- float position = PositionAtTime(hitObject.HitObject.StartTime, currentTime);
+ float position = PositionAtTime(hitObject.HitObject.StartTime, currentTime, parentHitObjectStartTime);
if (scrollingAxis == Direction.Horizontal)
hitObject.X = position;
diff --git a/osu.Game/Screens/IOsuScreen.cs b/osu.Game/Screens/IOsuScreen.cs
index 7d8657a3df..a5739a41b1 100644
--- a/osu.Game/Screens/IOsuScreen.cs
+++ b/osu.Game/Screens/IOsuScreen.cs
@@ -41,6 +41,11 @@ namespace osu.Game.Screens
///
bool HideOverlaysOnEnter { get; }
+ ///
+ /// Whether the menu cursor should be hidden when non-mouse input is received.
+ ///
+ bool HideMenuCursorOnNonMouseInput { get; }
+
///
/// Whether overlays should be able to be opened when this screen is current.
///
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs
index 39740e650f..ba6b482729 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs
@@ -78,9 +78,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist
return;
bool isItemOwner = Item.OwnerID == api.LocalUser.Value.OnlineID || multiplayerClient.IsHost;
+ bool isValidItem = isItemOwner && !Item.Expired;
- AllowDeletion = isItemOwner && !Item.Expired && Item.ID != multiplayerClient.Room.Settings.PlaylistItemId;
- AllowEditing = isItemOwner && !Item.Expired;
+ AllowDeletion = isValidItem
+ && (Item.ID != multiplayerClient.Room.Settings.PlaylistItemId // This is an optimisation for the following check.
+ || multiplayerClient.Room.Playlist.Count(i => !i.Expired) > 1);
+
+ AllowEditing = isValidItem;
}
protected override void Dispose(bool isDisposing)
diff --git a/osu.Game/Screens/OsuScreen.cs b/osu.Game/Screens/OsuScreen.cs
index 0678a90f71..6be13bbda3 100644
--- a/osu.Game/Screens/OsuScreen.cs
+++ b/osu.Game/Screens/OsuScreen.cs
@@ -40,11 +40,10 @@ namespace osu.Game.Screens
public virtual bool AllowExternalScreenChange => false;
- ///
- /// Whether all overlays should be hidden when this screen is entered or resumed.
- ///
public virtual bool HideOverlaysOnEnter => false;
+ public virtual bool HideMenuCursorOnNonMouseInput => false;
+
///
/// The initial overlay activation mode to use when this screen is entered for the first time.
///
diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs
index 68b623b781..7048f83c09 100644
--- a/osu.Game/Screens/Play/Player.cs
+++ b/osu.Game/Screens/Play/Player.cs
@@ -66,6 +66,8 @@ namespace osu.Game.Screens.Play
public override bool HideOverlaysOnEnter => true;
+ public override bool HideMenuCursorOnNonMouseInput => true;
+
protected override OverlayActivation InitialOverlayActivationMode => OverlayActivation.UserTriggered;
// We are managing our own adjustments (see OnEntering/OnExiting).
diff --git a/osu.Game/Screens/Play/SubmittingPlayer.cs b/osu.Game/Screens/Play/SubmittingPlayer.cs
index d56b9c23c8..345bd5a134 100644
--- a/osu.Game/Screens/Play/SubmittingPlayer.cs
+++ b/osu.Game/Screens/Play/SubmittingPlayer.cs
@@ -86,16 +86,13 @@ namespace osu.Game.Screens.Play
// Generally a timeout would not happen here as APIAccess will timeout first.
if (!tcs.Task.Wait(60000))
- handleTokenFailure(new InvalidOperationException("Token retrieval timed out (request never run)"));
+ req.TriggerFailure(new InvalidOperationException("Token retrieval timed out (request never run)"));
return true;
void handleTokenFailure(Exception exception)
{
- // This method may be invoked multiple times due to the Task.Wait call above.
- // We only really care about the first error.
- if (!tcs.TrySetResult(false))
- return;
+ tcs.SetResult(false);
if (HandleTokenRetrievalFailure(exception))
{
diff --git a/osu.Game/Skinning/DefaultLegacySkin.cs b/osu.Game/Skinning/DefaultLegacySkin.cs
index 04f1286dc7..b80275a1e8 100644
--- a/osu.Game/Skinning/DefaultLegacySkin.cs
+++ b/osu.Game/Skinning/DefaultLegacySkin.cs
@@ -46,6 +46,8 @@ namespace osu.Game.Skinning
new Color4(242, 24, 57, 255)
};
+ Configuration.ConfigDictionary[nameof(SkinConfiguration.LegacySetting.AllowSliderBallTint)] = @"true";
+
Configuration.LegacyVersion = 2.7m;
}
}
diff --git a/osu.Game/Skinning/Editor/SkinComponentToolbox.cs b/osu.Game/Skinning/Editor/SkinComponentToolbox.cs
index 980dee8601..469657c03c 100644
--- a/osu.Game/Skinning/Editor/SkinComponentToolbox.cs
+++ b/osu.Game/Skinning/Editor/SkinComponentToolbox.cs
@@ -85,10 +85,6 @@ namespace osu.Game.Skinning.Editor
{
public Action? RequestPlacement;
- protected override bool ShouldBeConsideredForInput(Drawable child) => false;
-
- public override bool PropagateNonPositionalInputSubTree => false;
-
private readonly Drawable component;
private readonly CompositeDrawable? dependencySource;
@@ -177,6 +173,10 @@ namespace osu.Game.Skinning.Editor
public class DependencyBorrowingContainer : Container
{
+ protected override bool ShouldBeConsideredForInput(Drawable child) => false;
+
+ public override bool PropagateNonPositionalInputSubTree => false;
+
private readonly CompositeDrawable? donor;
public DependencyBorrowingContainer(CompositeDrawable? donor)
diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/KiaiFlashingDrawable.cs b/osu.Game/Skinning/LegacyKiaiFlashingDrawable.cs
similarity index 81%
rename from osu.Game.Rulesets.Osu/Skinning/Legacy/KiaiFlashingDrawable.cs
rename to osu.Game/Skinning/LegacyKiaiFlashingDrawable.cs
index 152ed5c3d9..2bcdd5b5a1 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/KiaiFlashingDrawable.cs
+++ b/osu.Game/Skinning/LegacyKiaiFlashingDrawable.cs
@@ -7,15 +7,15 @@ using osu.Framework.Graphics;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.Containers;
-namespace osu.Game.Rulesets.Osu.Skinning.Legacy
+namespace osu.Game.Skinning
{
- internal class KiaiFlashingDrawable : BeatSyncedContainer
+ public class LegacyKiaiFlashingDrawable : BeatSyncedContainer
{
private readonly Drawable flashingDrawable;
- private const float flash_opacity = 0.3f;
+ private const float flash_opacity = 0.55f;
- public KiaiFlashingDrawable(Func creationFunc)
+ public LegacyKiaiFlashingDrawable(Func creationFunc)
{
AutoSizeAxes = Axes.Both;
@@ -44,7 +44,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
flashingDrawable
.FadeTo(flash_opacity)
.Then()
- .FadeOut(timingPoint.BeatLength * 0.75f);
+ .FadeOut(Math.Max(80, timingPoint.BeatLength - 80), Easing.OutSine);
}
}
}
diff --git a/osu.Game/Skinning/SkinConfiguration.cs b/osu.Game/Skinning/SkinConfiguration.cs
index 0b1159f8fd..4e5d96ccb8 100644
--- a/osu.Game/Skinning/SkinConfiguration.cs
+++ b/osu.Game/Skinning/SkinConfiguration.cs
@@ -38,7 +38,8 @@ namespace osu.Game.Skinning
HitCirclePrefix,
HitCircleOverlap,
AnimationFramerate,
- LayeredHitSounds
+ LayeredHitSounds,
+ AllowSliderBallTint,
}
public static List DefaultComboColours { get; } = new List
diff --git a/osu.Game/Tests/Visual/ScrollingTestContainer.cs b/osu.Game/Tests/Visual/ScrollingTestContainer.cs
index cf7fe6e45d..1817a704b9 100644
--- a/osu.Game/Tests/Visual/ScrollingTestContainer.cs
+++ b/osu.Game/Tests/Visual/ScrollingTestContainer.cs
@@ -99,8 +99,8 @@ namespace osu.Game.Tests.Visual
public float GetLength(double startTime, double endTime, double timeRange, float scrollLength)
=> implementation.GetLength(startTime, endTime, timeRange, scrollLength);
- public float PositionAt(double time, double currentTime, double timeRange, float scrollLength)
- => implementation.PositionAt(time, currentTime, timeRange, scrollLength);
+ public float PositionAt(double time, double currentTime, double timeRange, float scrollLength, double? originTime = null)
+ => implementation.PositionAt(time, currentTime, timeRange, scrollLength, originTime);
public double TimeAt(float position, double currentTime, double timeRange, float scrollLength)
=> implementation.TimeAt(position, currentTime, timeRange, scrollLength);
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index f1fed6913b..22474c0592 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -18,7 +18,7 @@
-
+
@@ -35,8 +35,8 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
+
+
diff --git a/osu.iOS.props b/osu.iOS.props
index c79d0e4864..cf70b65578 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -61,8 +61,8 @@
-
-
+
+
@@ -82,7 +82,7 @@
-
+