1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-18 03:47:04 +08:00

Merge branch 'master' into rhythm-fixes

This commit is contained in:
StanR
2024-09-23 14:25:32 +05:00
Unverified
41 changed files with 1183 additions and 131 deletions
+1 -1
View File
@@ -10,7 +10,7 @@
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.912.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.916.0" />
</ItemGroup>
<PropertyGroup>
<!-- Fody does not handle Android build well, and warns when unchanged.
+2
View File
@@ -13,5 +13,7 @@ namespace osu.Desktop.Windows
private static readonly string icon_directory = Path.GetDirectoryName(typeof(Icons).Assembly.Location)!;
public static string Lazer => Path.Join(icon_directory, "lazer.ico");
public static string Beatmap => Path.Join(icon_directory, "beatmap.ico");
}
}
@@ -40,10 +40,10 @@ namespace osu.Desktop.Windows
private static readonly FileAssociation[] file_associations =
{
new FileAssociation(@".osz", WindowsAssociationManagerStrings.OsuBeatmap, Icons.Lazer),
new FileAssociation(@".olz", WindowsAssociationManagerStrings.OsuBeatmap, Icons.Lazer),
new FileAssociation(@".osr", WindowsAssociationManagerStrings.OsuReplay, Icons.Lazer),
new FileAssociation(@".osk", WindowsAssociationManagerStrings.OsuSkin, Icons.Lazer),
new FileAssociation(@".osz", WindowsAssociationManagerStrings.OsuBeatmap, Icons.Beatmap),
new FileAssociation(@".olz", WindowsAssociationManagerStrings.OsuBeatmap, Icons.Beatmap),
new FileAssociation(@".osr", WindowsAssociationManagerStrings.OsuReplay, Icons.Beatmap),
new FileAssociation(@".osk", WindowsAssociationManagerStrings.OsuSkin, Icons.Beatmap),
};
private static readonly UriAssociation[] uri_associations =
Binary file not shown.

After

Width:  |  Height:  |  Size: 349 KiB

@@ -1,10 +1,12 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Utils;
using osu.Game.Rulesets.Catch.Skinning.Default;
using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Rulesets.Catch.Objects.Drawables
{
@@ -36,23 +38,43 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
StartTimeBindable.BindValueChanged(_ => UpdateComboColour());
}
private float startScale;
private float endScale;
private float startAngle;
private float endAngle;
protected override void UpdateInitialTransforms()
{
base.UpdateInitialTransforms();
// Important to have this in UpdateInitialTransforms() to it is re-triggered by RefreshStateTransforms().
const float end_scale = 0.6f;
const float random_scale_range = 1.6f;
ScalingContainer.ScaleTo(HitObject.Scale * (end_scale + random_scale_range * RandomSingle(3)))
.Then().ScaleTo(HitObject.Scale * end_scale, HitObject.TimePreempt);
startScale = end_scale + random_scale_range * RandomSingle(3);
endScale = end_scale;
ScalingContainer.RotateTo(getRandomAngle(1))
.Then()
.RotateTo(getRandomAngle(2), HitObject.TimePreempt);
startAngle = getRandomAngle(1);
endAngle = getRandomAngle(2);
float getRandomAngle(int series) => 180 * (RandomSingle(series) * 2 - 1);
}
protected override void Update()
{
base.Update();
double preemptProgress = (Time.Current - (HitObject.StartTime - InitialLifetimeOffset)) / HitObject.TimePreempt;
// Clamp scale and rotation at the point of bananas being caught, else let them freely extrapolate.
if (Result.IsHit)
preemptProgress = Math.Min(1, preemptProgress);
ScalingContainer.Scale = new Vector2(HitObject.Scale * (float)Interpolation.Lerp(startScale, endScale, preemptProgress));
ScalingContainer.Rotation = (float)Interpolation.Lerp(startAngle, endAngle, preemptProgress);
}
public override void PlaySamples()
{
base.PlaySamples();
@@ -2,7 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Utils;
using osu.Game.Rulesets.Catch.Skinning.Default;
using osu.Game.Skinning;
@@ -28,15 +28,24 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
_ => new DropletPiece());
}
private float startRotation;
protected override void UpdateInitialTransforms()
{
base.UpdateInitialTransforms();
// roughly matches osu-stable
float startRotation = RandomSingle(1) * 20;
double duration = HitObject.TimePreempt + 2000;
// Important to have this in UpdateInitialTransforms() to it is re-triggered by RefreshStateTransforms().
startRotation = RandomSingle(1) * 20;
}
ScalingContainer.RotateTo(startRotation).RotateTo(startRotation + 720, duration);
protected override void Update()
{
base.Update();
// No clamping for droplets. They should be considered indefinitely spinning regardless of time.
// They also never end up on the plate, so they shouldn't stop spinning when caught.
double preemptProgress = (Time.Current - (HitObject.StartTime - InitialLifetimeOffset)) / (HitObject.TimePreempt + 2000);
ScalingContainer.Rotation = (float)Interpolation.Lerp(startRotation, startRotation + 720, preemptProgress);
}
}
}
@@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Catch.Skinning.Default;
using osu.Game.Skinning;
@@ -32,7 +31,8 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
{
base.UpdateInitialTransforms();
ScalingContainer.RotateTo((RandomSingle(1) - 0.5f) * 40);
// Important to have this in UpdateInitialTransforms() to it is re-triggered by RefreshStateTransforms().
ScalingContainer.Rotation = (RandomSingle(1) - 0.5f) * 40;
}
}
}
@@ -116,7 +116,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
() => EditorBeatmap.HitObjects.OfType<HitCircle>().ElementAt(1).Position,
() => Is.EqualTo(OsuPlayfield.BASE_SIZE - new Vector2(200)));
AddStep("change rotation origin", () => getPopover().ChildrenOfType<EditorRadioButton>().ElementAt(1).TriggerClick());
AddStep("change rotation origin", () => getPopover().ChildrenOfType<EditorRadioButton>().ElementAt(2).TriggerClick());
AddAssert("first object rotated 90deg around selection centre",
() => EditorBeatmap.HitObjects.OfType<HitCircle>().ElementAt(0).Position, () => Is.EqualTo(new Vector2(200, 200)));
AddAssert("second object rotated 90deg around selection centre",
@@ -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 NUnit.Framework;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
@@ -22,9 +20,9 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
{
public partial class TestSceneSliderSelectionBlueprint : SelectionBlueprintTestScene
{
private Slider slider;
private DrawableSlider drawableObject;
private TestSliderBlueprint blueprint;
private Slider slider = null!;
private DrawableSlider drawableObject = null!;
private TestSliderBlueprint blueprint = null!;
[SetUp]
public void Setup() => Schedule(() =>
@@ -218,6 +216,9 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddAssert("tail positioned correctly",
() => Precision.AlmostEquals(blueprint.TailOverlay.CirclePiece.ScreenSpaceDrawQuad.Centre, drawableObject.TailCircle.ScreenSpaceDrawQuad.Centre));
AddAssert("end drag marker positioned correctly",
() => Precision.AlmostEquals(blueprint.TailOverlay.EndDragMarker!.ToScreenSpace(blueprint.TailOverlay.EndDragMarker.OriginPosition), drawableObject.TailCircle.ScreenSpaceDrawQuad.Centre, 2));
}
private void moveMouseToControlPoint(int index)
@@ -230,14 +231,14 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
}
private void checkControlPointSelected(int index, bool selected)
=> AddAssert($"control point {index} {(selected ? "selected" : "not selected")}", () => blueprint.ControlPointVisualiser.Pieces[index].IsSelected.Value == selected);
=> AddAssert($"control point {index} {(selected ? "selected" : "not selected")}", () => blueprint.ControlPointVisualiser!.Pieces[index].IsSelected.Value == selected);
private partial class TestSliderBlueprint : SliderSelectionBlueprint
{
public new SliderBodyPiece BodyPiece => base.BodyPiece;
public new TestSliderCircleOverlay HeadOverlay => (TestSliderCircleOverlay)base.HeadOverlay;
public new TestSliderCircleOverlay TailOverlay => (TestSliderCircleOverlay)base.TailOverlay;
public new PathControlPointVisualiser<Slider> ControlPointVisualiser => base.ControlPointVisualiser;
public new PathControlPointVisualiser<Slider>? ControlPointVisualiser => base.ControlPointVisualiser;
public TestSliderBlueprint(Slider slider)
: base(slider)
@@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
private double currentStrain;
private double skillMultiplier => 23.65;
private double skillMultiplier => 25.08;
private double strainDecayBase => 0.15;
private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000);
@@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
hasHiddenMod = mods.Any(m => m is OsuModHidden);
}
private double skillMultiplier => 0.052;
private double skillMultiplier => 0.05512;
private double strainDecayBase => 0.15;
private double currentStrain;
@@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
return currentStrain;
}
public override double DifficultyValue() => GetCurrentStrainPeaks().Sum() * OsuStrainSkill.DEFAULT_DIFFICULTY_MULTIPLIER;
public override double DifficultyValue() => GetCurrentStrainPeaks().Sum();
public static double DifficultyToPerformance(double difficulty) => 25 * Math.Pow(difficulty, 2);
}
@@ -12,12 +12,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
{
public abstract class OsuStrainSkill : StrainSkill
{
/// <summary>
/// The default multiplier applied by <see cref="OsuStrainSkill"/> to the final difficulty value after all other calculations.
/// May be overridden via <see cref="DifficultyMultiplier"/>.
/// </summary>
public const double DEFAULT_DIFFICULTY_MULTIPLIER = 1.06;
/// <summary>
/// The number of sections with the highest strains, which the peak strain reductions will apply to.
/// This is done in order to decrease their impact on the overall difficulty of the map for this skill.
@@ -29,11 +23,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
/// </summary>
protected virtual double ReducedStrainBaseline => 0.75;
/// <summary>
/// The final multiplier to be applied to <see cref="DifficultyValue"/> after all other calculations.
/// </summary>
protected virtual double DifficultyMultiplier => DEFAULT_DIFFICULTY_MULTIPLIER;
protected OsuStrainSkill(Mod[] mods)
: base(mods)
{
@@ -65,7 +54,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
weight *= DecayWeight;
}
return difficulty * DifficultyMultiplier;
return difficulty;
}
public static double DifficultyToPerformance(double difficulty) => Math.Pow(5.0 * Math.Max(1.0, difficulty / 0.0675) - 4.0, 3.0) / 100000.0;
@@ -16,14 +16,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
/// </summary>
public class Speed : OsuStrainSkill
{
private double skillMultiplier => 1.375;
private double skillMultiplier => 1.430;
private double strainDecayBase => 0.3;
private double currentStrain;
private double currentRhythm;
protected override int ReducedSectionCount => 5;
protected override double DifficultyMultiplier => 1.04;
private readonly List<double> objectStrains = new List<double>();
@@ -70,7 +70,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
if (endDragMarkerContainer != null)
{
endDragMarkerContainer.Position = circle.Position;
endDragMarkerContainer.Position = circle.Position + slider.StackOffset;
endDragMarkerContainer.Scale = CirclePiece.Scale * 1.2f;
var diff = slider.Path.PositionAt(1) - slider.Path.PositionAt(0.99f);
endDragMarkerContainer.Rotation = float.RadiansToDegrees(MathF.Atan2(diff.Y, diff.X));
@@ -106,6 +106,7 @@ namespace osu.Game.Rulesets.Osu.Edit
{
RotationHandler = BlueprintContainer.SelectionHandler.RotationHandler,
ScaleHandler = (OsuSelectionScaleHandler)BlueprintContainer.SelectionHandler.ScaleHandler,
GridToolbox = OsuGridToolboxGroup,
},
new GenerateToolboxGroup(),
FreehandSliderToolboxGroup
@@ -3,6 +3,7 @@
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.UserInterface;
@@ -25,6 +26,9 @@ namespace osu.Game.Rulesets.Osu.Edit
{
public partial class OsuSelectionHandler : EditorSelectionHandler
{
[Resolved]
private OsuGridToolboxGroup gridToolbox { get; set; } = null!;
protected override void OnSelectionChanged()
{
base.OnSelectionChanged();
@@ -123,13 +127,43 @@ namespace osu.Game.Rulesets.Osu.Edit
{
var hitObjects = selectedMovableObjects;
var flipQuad = flipOverOrigin ? new Quad(0, 0, OsuPlayfield.BASE_SIZE.X, OsuPlayfield.BASE_SIZE.Y) : GeometryUtils.GetSurroundingQuad(hitObjects);
// If we're flipping over the origin, we take the grid origin position from the grid toolbox.
var flipQuad = flipOverOrigin ? new Quad(gridToolbox.StartPositionX.Value, gridToolbox.StartPositionY.Value, 0, 0) : GeometryUtils.GetSurroundingQuad(hitObjects);
Vector2 flipAxis = direction == Direction.Vertical ? Vector2.UnitY : Vector2.UnitX;
if (flipOverOrigin)
{
// If we're flipping over the origin, we take one of the axes of the grid.
// Take the axis closest to the direction we want to flip over.
switch (gridToolbox.GridType.Value)
{
case PositionSnapGridType.Square:
flipAxis = GeometryUtils.RotateVector(Vector2.UnitX, -((gridToolbox.GridLinesRotation.Value + 360 + 45) % 90 - 45));
flipAxis = direction == Direction.Vertical ? flipAxis.PerpendicularLeft : flipAxis;
break;
case PositionSnapGridType.Triangle:
// Hex grid has 3 axes, so you can not directly flip over one of the axes,
// however it's still possible to achieve that flip by combining multiple flips over the other axes.
// Angle degree range for vertical = (-120, -60]
// Angle degree range for horizontal = [-30, 30)
flipAxis = direction == Direction.Vertical
? GeometryUtils.RotateVector(Vector2.UnitX, -((gridToolbox.GridLinesRotation.Value + 360 + 30) % 60 + 60))
: GeometryUtils.RotateVector(Vector2.UnitX, -((gridToolbox.GridLinesRotation.Value + 360) % 60 - 30));
break;
}
}
var controlPointFlipQuad = new Quad();
bool didFlip = false;
foreach (var h in hitObjects)
{
var flippedPosition = GeometryUtils.GetFlippedPosition(direction, flipQuad, h.Position);
var flippedPosition = GeometryUtils.GetFlippedPosition(flipAxis, flipQuad, h.Position);
// Clamp the flipped position inside the playfield bounds, because the flipped position might be outside the playfield bounds if the origin is not centered.
flippedPosition = Vector2.Clamp(flippedPosition, Vector2.Zero, OsuPlayfield.BASE_SIZE);
if (!Precision.AlmostEquals(flippedPosition, h.Position))
{
@@ -142,12 +176,7 @@ namespace osu.Game.Rulesets.Osu.Edit
didFlip = true;
foreach (var cp in slider.Path.ControlPoints)
{
cp.Position = new Vector2(
(direction == Direction.Horizontal ? -1 : 1) * cp.Position.X,
(direction == Direction.Vertical ? -1 : 1) * cp.Position.Y
);
}
cp.Position = GeometryUtils.GetFlippedPosition(flipAxis, controlPointFlipQuad, cp.Position);
}
}
@@ -69,6 +69,7 @@ namespace osu.Game.Rulesets.Osu.Edit
private Dictionary<OsuHitObject, OriginalHitObjectState>? objectsInScale;
private Vector2? defaultOrigin;
private List<Vector2>? originalConvexHull;
public override void Begin()
{
@@ -84,9 +85,12 @@ namespace osu.Game.Rulesets.Osu.Edit
? GeometryUtils.GetSurroundingQuad(slider.Path.ControlPoints.Select(p => slider.Position + p.Position))
: GeometryUtils.GetSurroundingQuad(objectsInScale.Keys);
defaultOrigin = OriginalSurroundingQuad.Value.Centre;
originalConvexHull = objectsInScale.Count == 1 && objectsInScale.First().Key is Slider slider2
? GeometryUtils.GetConvexHull(slider2.Path.ControlPoints.Select(p => slider2.Position + p.Position))
: GeometryUtils.GetConvexHull(objectsInScale.Keys);
}
public override void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both)
public override void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both, float axisRotation = 0)
{
if (!OperationInProgress.Value)
throw new InvalidOperationException($"Cannot {nameof(Update)} a scale operation without calling {nameof(Begin)} first!");
@@ -94,6 +98,7 @@ namespace osu.Game.Rulesets.Osu.Edit
Debug.Assert(objectsInScale != null && defaultOrigin != null && OriginalSurroundingQuad != null);
Vector2 actualOrigin = origin ?? defaultOrigin.Value;
scale = clampScaleToAdjustAxis(scale, adjustAxis);
// for the time being, allow resizing of slider paths only if the slider is
// the only hit object selected. with a group selection, it's likely the user
@@ -102,15 +107,15 @@ namespace osu.Game.Rulesets.Osu.Edit
{
var originalInfo = objectsInScale[slider];
Debug.Assert(originalInfo.PathControlPointPositions != null && originalInfo.PathControlPointTypes != null);
scaleSlider(slider, scale, originalInfo.PathControlPointPositions, originalInfo.PathControlPointTypes);
scaleSlider(slider, scale, originalInfo.PathControlPointPositions, originalInfo.PathControlPointTypes, axisRotation);
}
else
{
scale = ClampScaleToPlayfieldBounds(scale, actualOrigin);
scale = ClampScaleToPlayfieldBounds(scale, actualOrigin, adjustAxis, axisRotation);
foreach (var (ho, originalState) in objectsInScale)
{
ho.Position = GeometryUtils.GetScaledPosition(scale, actualOrigin, originalState.Position);
ho.Position = GeometryUtils.GetScaledPosition(scale, actualOrigin, originalState.Position, axisRotation);
}
}
@@ -134,14 +139,34 @@ namespace osu.Game.Rulesets.Osu.Edit
private IEnumerable<OsuHitObject> selectedMovableObjects => selectedItems.Cast<OsuHitObject>()
.Where(h => h is not Spinner);
private void scaleSlider(Slider slider, Vector2 scale, Vector2[] originalPathPositions, PathType?[] originalPathTypes)
private Vector2 clampScaleToAdjustAxis(Vector2 scale, Axes adjustAxis)
{
switch (adjustAxis)
{
case Axes.Y:
scale.X = 1;
break;
case Axes.X:
scale.Y = 1;
break;
case Axes.None:
scale = Vector2.One;
break;
}
return scale;
}
private void scaleSlider(Slider slider, Vector2 scale, Vector2[] originalPathPositions, PathType?[] originalPathTypes, float axisRotation = 0)
{
scale = Vector2.ComponentMax(scale, new Vector2(Precision.FLOAT_EPSILON));
// Maintain the path types in case they were defaulted to bezier at some point during scaling
for (int i = 0; i < slider.Path.ControlPoints.Count; i++)
{
slider.Path.ControlPoints[i].Position = originalPathPositions[i] * scale;
slider.Path.ControlPoints[i].Position = GeometryUtils.GetScaledPosition(scale, Vector2.Zero, originalPathPositions[i], axisRotation);
slider.Path.ControlPoints[i].Type = originalPathTypes[i];
}
@@ -176,11 +201,13 @@ namespace osu.Game.Rulesets.Osu.Edit
/// </summary>
/// <param name="origin">The origin from which the scale operation is performed</param>
/// <param name="scale">The scale to be clamped</param>
/// <param name="adjustAxis">The axes to adjust the scale in.</param>
/// <param name="axisRotation">The rotation of the axes in degrees</param>
/// <returns>The clamped scale vector</returns>
public Vector2 ClampScaleToPlayfieldBounds(Vector2 scale, Vector2? origin = null)
public Vector2 ClampScaleToPlayfieldBounds(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both, float axisRotation = 0)
{
//todo: this is not always correct for selections involving sliders. This approximation assumes each point is scaled independently, but sliderends move with the sliderhead.
if (objectsInScale == null)
if (objectsInScale == null || adjustAxis == Axes.None)
return scale;
Debug.Assert(defaultOrigin != null && OriginalSurroundingQuad != null);
@@ -188,24 +215,60 @@ namespace osu.Game.Rulesets.Osu.Edit
if (objectsInScale.Count == 1 && objectsInScale.First().Key is Slider slider)
origin = slider.Position;
float cos = MathF.Cos(float.DegreesToRadians(-axisRotation));
float sin = MathF.Sin(float.DegreesToRadians(-axisRotation));
scale = clampScaleToAdjustAxis(scale, adjustAxis);
Vector2 actualOrigin = origin ?? defaultOrigin.Value;
var selectionQuad = OriginalSurroundingQuad.Value;
IEnumerable<Vector2> points;
var tl1 = Vector2.Divide(-actualOrigin, selectionQuad.TopLeft - actualOrigin);
var tl2 = Vector2.Divide(OsuPlayfield.BASE_SIZE - actualOrigin, selectionQuad.TopLeft - actualOrigin);
var br1 = Vector2.Divide(-actualOrigin, selectionQuad.BottomRight - actualOrigin);
var br2 = Vector2.Divide(OsuPlayfield.BASE_SIZE - actualOrigin, selectionQuad.BottomRight - actualOrigin);
if (axisRotation == 0)
{
var selectionQuad = OriginalSurroundingQuad.Value;
points = new[]
{
selectionQuad.TopLeft,
selectionQuad.TopRight,
selectionQuad.BottomLeft,
selectionQuad.BottomRight
};
}
else
points = originalConvexHull!;
if (!Precision.AlmostEquals(selectionQuad.TopLeft.X - actualOrigin.X, 0))
scale.X = selectionQuad.TopLeft.X - actualOrigin.X < 0 ? MathHelper.Clamp(scale.X, tl2.X, tl1.X) : MathHelper.Clamp(scale.X, tl1.X, tl2.X);
if (!Precision.AlmostEquals(selectionQuad.TopLeft.Y - actualOrigin.Y, 0))
scale.Y = selectionQuad.TopLeft.Y - actualOrigin.Y < 0 ? MathHelper.Clamp(scale.Y, tl2.Y, tl1.Y) : MathHelper.Clamp(scale.Y, tl1.Y, tl2.Y);
if (!Precision.AlmostEquals(selectionQuad.BottomRight.X - actualOrigin.X, 0))
scale.X = selectionQuad.BottomRight.X - actualOrigin.X < 0 ? MathHelper.Clamp(scale.X, br2.X, br1.X) : MathHelper.Clamp(scale.X, br1.X, br2.X);
if (!Precision.AlmostEquals(selectionQuad.BottomRight.Y - actualOrigin.Y, 0))
scale.Y = selectionQuad.BottomRight.Y - actualOrigin.Y < 0 ? MathHelper.Clamp(scale.Y, br2.Y, br1.Y) : MathHelper.Clamp(scale.Y, br1.Y, br2.Y);
foreach (var point in points)
{
scale = clampToBound(scale, point, Vector2.Zero);
scale = clampToBound(scale, point, OsuPlayfield.BASE_SIZE);
}
return Vector2.ComponentMax(scale, new Vector2(Precision.FLOAT_EPSILON));
float minPositiveComponent(Vector2 v) => MathF.Min(v.X < 0 ? float.PositiveInfinity : v.X, v.Y < 0 ? float.PositiveInfinity : v.Y);
Vector2 clampToBound(Vector2 s, Vector2 p, Vector2 bound)
{
p -= actualOrigin;
bound -= actualOrigin;
var a = new Vector2(cos * cos * p.X - sin * cos * p.Y, -sin * cos * p.X + sin * sin * p.Y);
var b = new Vector2(sin * sin * p.X + sin * cos * p.Y, sin * cos * p.X + cos * cos * p.Y);
switch (adjustAxis)
{
case Axes.X:
s.X = MathF.Min(scale.X, minPositiveComponent(Vector2.Divide(bound - b, a)));
break;
case Axes.Y:
s.Y = MathF.Min(scale.Y, minPositiveComponent(Vector2.Divide(bound - a, b)));
break;
case Axes.Both:
s = Vector2.ComponentMin(s, s * minPositiveComponent(Vector2.Divide(bound, a * s.X + b * s.Y)));
break;
}
return s;
}
}
private void moveSelectionInBounds()
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@@ -19,16 +20,19 @@ namespace osu.Game.Rulesets.Osu.Edit
{
private readonly SelectionRotationHandler rotationHandler;
private readonly Bindable<PreciseRotationInfo> rotationInfo = new Bindable<PreciseRotationInfo>(new PreciseRotationInfo(0, RotationOrigin.PlayfieldCentre));
private readonly OsuGridToolboxGroup gridToolbox;
private readonly Bindable<PreciseRotationInfo> rotationInfo = new Bindable<PreciseRotationInfo>(new PreciseRotationInfo(0, RotationOrigin.GridCentre));
private SliderWithTextBoxInput<float> angleInput = null!;
private EditorRadioButtonCollection rotationOrigin = null!;
private RadioButton selectionCentreButton = null!;
public PreciseRotationPopover(SelectionRotationHandler rotationHandler)
public PreciseRotationPopover(SelectionRotationHandler rotationHandler, OsuGridToolboxGroup gridToolbox)
{
this.rotationHandler = rotationHandler;
this.gridToolbox = gridToolbox;
AllowableAnchors = new[] { Anchor.CentreLeft, Anchor.CentreRight };
}
@@ -58,6 +62,9 @@ namespace osu.Game.Rulesets.Osu.Edit
RelativeSizeAxes = Axes.X,
Items = new[]
{
new RadioButton("Grid centre",
() => rotationInfo.Value = rotationInfo.Value with { Origin = RotationOrigin.GridCentre },
() => new SpriteIcon { Icon = FontAwesome.Regular.PlusSquare }),
new RadioButton("Playfield centre",
() => rotationInfo.Value = rotationInfo.Value with { Origin = RotationOrigin.PlayfieldCentre },
() => new SpriteIcon { Icon = FontAwesome.Regular.Square }),
@@ -93,10 +100,19 @@ namespace osu.Game.Rulesets.Osu.Edit
rotationInfo.BindValueChanged(rotation =>
{
rotationHandler.Update(rotation.NewValue.Degrees, rotation.NewValue.Origin == RotationOrigin.PlayfieldCentre ? OsuPlayfield.BASE_SIZE / 2 : null);
rotationHandler.Update(rotation.NewValue.Degrees, getOriginPosition(rotation.NewValue));
});
}
private Vector2? getOriginPosition(PreciseRotationInfo rotation) =>
rotation.Origin switch
{
RotationOrigin.GridCentre => gridToolbox.StartPosition.Value,
RotationOrigin.PlayfieldCentre => OsuPlayfield.BASE_SIZE / 2,
RotationOrigin.SelectionCentre => null,
_ => throw new ArgumentOutOfRangeException(nameof(rotation))
};
protected override void PopIn()
{
base.PopIn();
@@ -114,6 +130,7 @@ namespace osu.Game.Rulesets.Osu.Edit
public enum RotationOrigin
{
GridCentre,
PlayfieldCentre,
SelectionCentre
}
@@ -20,21 +20,25 @@ namespace osu.Game.Rulesets.Osu.Edit
{
private readonly OsuSelectionScaleHandler scaleHandler;
private readonly Bindable<PreciseScaleInfo> scaleInfo = new Bindable<PreciseScaleInfo>(new PreciseScaleInfo(1, ScaleOrigin.PlayfieldCentre, true, true));
private readonly OsuGridToolboxGroup gridToolbox;
private readonly Bindable<PreciseScaleInfo> scaleInfo = new Bindable<PreciseScaleInfo>(new PreciseScaleInfo(1, ScaleOrigin.GridCentre, true, true));
private SliderWithTextBoxInput<float> scaleInput = null!;
private BindableNumber<float> scaleInputBindable = null!;
private EditorRadioButtonCollection scaleOrigin = null!;
private RadioButton gridCentreButton = null!;
private RadioButton playfieldCentreButton = null!;
private RadioButton selectionCentreButton = null!;
private OsuCheckbox xCheckBox = null!;
private OsuCheckbox yCheckBox = null!;
public PreciseScalePopover(OsuSelectionScaleHandler scaleHandler)
public PreciseScalePopover(OsuSelectionScaleHandler scaleHandler, OsuGridToolboxGroup gridToolbox)
{
this.scaleHandler = scaleHandler;
this.gridToolbox = gridToolbox;
AllowableAnchors = new[] { Anchor.CentreLeft, Anchor.CentreRight };
}
@@ -66,6 +70,9 @@ namespace osu.Game.Rulesets.Osu.Edit
RelativeSizeAxes = Axes.X,
Items = new[]
{
gridCentreButton = new RadioButton("Grid centre",
() => setOrigin(ScaleOrigin.GridCentre),
() => new SpriteIcon { Icon = FontAwesome.Regular.PlusSquare }),
playfieldCentreButton = new RadioButton("Playfield centre",
() => setOrigin(ScaleOrigin.PlayfieldCentre),
() => new SpriteIcon { Icon = FontAwesome.Regular.Square }),
@@ -97,6 +104,10 @@ namespace osu.Game.Rulesets.Osu.Edit
},
}
};
gridCentreButton.Selected.DisabledChanged += isDisabled =>
{
gridCentreButton.TooltipText = isDisabled ? "The current selection cannot be scaled relative to grid centre." : string.Empty;
};
playfieldCentreButton.Selected.DisabledChanged += isDisabled =>
{
playfieldCentreButton.TooltipText = isDisabled ? "The current selection cannot be scaled relative to playfield centre." : string.Empty;
@@ -123,19 +134,20 @@ namespace osu.Game.Rulesets.Osu.Edit
selectionCentreButton.Selected.Disabled = !(scaleHandler.CanScaleX.Value || scaleHandler.CanScaleY.Value);
playfieldCentreButton.Selected.Disabled = scaleHandler.IsScalingSlider.Value && !selectionCentreButton.Selected.Disabled;
gridCentreButton.Selected.Disabled = playfieldCentreButton.Selected.Disabled;
scaleOrigin.Items.First(b => !b.Selected.Disabled).Select();
scaleInfo.BindValueChanged(scale =>
{
var newScale = new Vector2(scale.NewValue.XAxis ? scale.NewValue.Scale : 1, scale.NewValue.YAxis ? scale.NewValue.Scale : 1);
scaleHandler.Update(newScale, getOriginPosition(scale.NewValue));
var newScale = new Vector2(scale.NewValue.Scale, scale.NewValue.Scale);
scaleHandler.Update(newScale, getOriginPosition(scale.NewValue), getAdjustAxis(scale.NewValue), getRotation(scale.NewValue));
});
}
private void updateAxisCheckBoxesEnabled()
{
if (scaleInfo.Value.Origin == ScaleOrigin.PlayfieldCentre)
if (scaleInfo.Value.Origin != ScaleOrigin.SelectionCentre)
{
toggleAxisAvailable(xCheckBox.Current, true);
toggleAxisAvailable(yCheckBox.Current, true);
@@ -162,7 +174,7 @@ namespace osu.Game.Rulesets.Osu.Edit
return;
const float max_scale = 10;
var scale = scaleHandler.ClampScaleToPlayfieldBounds(new Vector2(max_scale), getOriginPosition(scaleInfo.Value));
var scale = scaleHandler.ClampScaleToPlayfieldBounds(new Vector2(max_scale), getOriginPosition(scaleInfo.Value), getAdjustAxis(scaleInfo.Value), getRotation(scaleInfo.Value));
if (!scaleInfo.Value.XAxis)
scale.X = max_scale;
@@ -179,7 +191,18 @@ namespace osu.Game.Rulesets.Osu.Edit
updateAxisCheckBoxesEnabled();
}
private Vector2? getOriginPosition(PreciseScaleInfo scale) => scale.Origin == ScaleOrigin.PlayfieldCentre ? OsuPlayfield.BASE_SIZE / 2 : null;
private Vector2? getOriginPosition(PreciseScaleInfo scale) =>
scale.Origin switch
{
ScaleOrigin.GridCentre => gridToolbox.StartPosition.Value,
ScaleOrigin.PlayfieldCentre => OsuPlayfield.BASE_SIZE / 2,
ScaleOrigin.SelectionCentre => null,
_ => throw new ArgumentOutOfRangeException(nameof(scale))
};
private Axes getAdjustAxis(PreciseScaleInfo scale) => scale.XAxis ? scale.YAxis ? Axes.Both : Axes.X : Axes.Y;
private float getRotation(PreciseScaleInfo scale) => scale.Origin == ScaleOrigin.GridCentre ? gridToolbox.GridLinesRotation.Value : 0;
private void setAxis(bool x, bool y)
{
@@ -204,6 +227,7 @@ namespace osu.Game.Rulesets.Osu.Edit
public enum ScaleOrigin
{
GridCentre,
PlayfieldCentre,
SelectionCentre
}
@@ -27,6 +27,8 @@ namespace osu.Game.Rulesets.Osu.Edit
public SelectionRotationHandler RotationHandler { get; init; } = null!;
public OsuSelectionScaleHandler ScaleHandler { get; init; } = null!;
public OsuGridToolboxGroup GridToolbox { get; init; } = null!;
public TransformToolboxGroup()
: base("transform")
{
@@ -44,10 +46,10 @@ namespace osu.Game.Rulesets.Osu.Edit
{
rotateButton = new EditorToolButton("Rotate",
() => new SpriteIcon { Icon = FontAwesome.Solid.Undo },
() => new PreciseRotationPopover(RotationHandler)),
() => new PreciseRotationPopover(RotationHandler, GridToolbox)),
scaleButton = new EditorToolButton("Scale",
() => new SpriteIcon { Icon = FontAwesome.Solid.ArrowsAlt },
() => new PreciseScalePopover(ScaleHandler))
() => new PreciseScalePopover(ScaleHandler, GridToolbox))
}
};
}
@@ -91,20 +91,35 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
drawableObjectPiece.ApplyCustomUpdateState -= applyDimToDrawableHitObject;
drawableObjectPiece.ApplyCustomUpdateState += applyDimToDrawableHitObject;
}
else
applyDim(piece);
}
void applyDim(Drawable piece)
{
piece.FadeColour(new Color4(195, 195, 195, 255));
using (piece.BeginDelayedSequence(InitialLifetimeOffset - OsuHitWindows.MISS_WINDOW))
piece.FadeColour(Color4.White, 100);
// but at the end apply the transforms now regardless of whether this is a DHO or not.
// the above is just to ensure they don't get overwritten later.
applyDim(piece);
}
void applyDimToDrawableHitObject(DrawableHitObject dho, ArmedState _) => applyDim(dho);
}
protected override void ClearNestedHitObjects()
{
base.ClearNestedHitObjects();
// any dimmable pieces that are DHOs will be pooled separately.
// `applyDimToDrawableHitObject` is a closure that implicitly captures `this`,
// and because of separate pooling of parent and child objects, there is no guarantee that the pieces will be associated with `this` again on re-use.
// therefore, clean up the subscription here to avoid crosstalk.
// not doing so can result in the callback attempting to read things from `this` when it is in a completely bogus state (not in use or similar).
foreach (var piece in DimmablePieces.OfType<DrawableHitObject>())
piece.ApplyCustomUpdateState -= applyDimToDrawableHitObject;
}
private void applyDim(Drawable piece)
{
piece.FadeColour(new Color4(195, 195, 195, 255));
using (piece.BeginDelayedSequence(InitialLifetimeOffset - OsuHitWindows.MISS_WINDOW))
piece.FadeColour(Color4.White, 100);
}
private void applyDimToDrawableHitObject(DrawableHitObject dho, ArmedState _) => applyDim(dho);
protected sealed override double InitialLifetimeOffset => HitObject.TimePreempt;
private OsuInputManager osuActionInputManager;
+33
View File
@@ -0,0 +1,33 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Game.Utils;
using osuTK;
namespace osu.Game.Tests.Utils
{
[TestFixture]
public class GeometryUtilsTest
{
[TestCase(new int[] { }, new int[] { })]
[TestCase(new[] { 0, 0 }, new[] { 0, 0 })]
[TestCase(new[] { 0, 0, 1, 1, 1, -1, 2, 0 }, new[] { 0, 0, 1, 1, 2, 0, 1, -1 })]
[TestCase(new[] { 0, 0, 1, 1, 1, -1, 2, 0, 1, 0 }, new[] { 0, 0, 1, 1, 2, 0, 1, -1 })]
[TestCase(new[] { 0, 0, 1, 1, 2, -1, 2, 0, 1, 0, 4, 10 }, new[] { 0, 0, 4, 10, 2, -1 })]
public void TestConvexHull(int[] values, int[] expected)
{
var points = new Vector2[values.Length / 2];
for (int i = 0; i < values.Length; i += 2)
points[i / 2] = new Vector2(values[i], values[i + 1]);
var expectedPoints = new Vector2[expected.Length / 2];
for (int i = 0; i < expected.Length; i += 2)
expectedPoints[i / 2] = new Vector2(expected[i], expected[i + 1]);
var hull = GeometryUtils.GetConvexHull(points);
Assert.That(hull, Is.EquivalentTo(expectedPoints));
}
}
}
@@ -134,7 +134,7 @@ namespace osu.Game.Tests.Visual.Editing
OriginalSurroundingQuad = new Quad(targetContainer!.X, targetContainer.Y, targetContainer.Width, targetContainer.Height);
}
public override void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both)
public override void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both, float axisRotation = 0)
{
if (targetContainer == null)
throw new InvalidOperationException($"Cannot {nameof(Update)} a scale operation without calling {nameof(Begin)} first!");
@@ -144,6 +144,28 @@ namespace osu.Game.Tests.Visual.Navigation
exitViaEscapeAndConfirm();
}
[Test]
public void TestEnterGameplayWhileFilteringToNoSelection()
{
TestPlaySongSelect songSelect = null;
PushAndConfirm(() => songSelect = new TestPlaySongSelect());
AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded);
AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely());
AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault);
AddStep("force selection", () =>
{
songSelect.FinaliseSelection();
songSelect.FilterControl.CurrentTextSearch.Value = "test";
});
AddUntilStep("wait for player", () => !songSelect.IsCurrentScreen());
AddStep("return to song select", () => songSelect.MakeCurrent());
AddUntilStep("wait for selection lost", () => songSelect.Beatmap.IsDefault);
}
[Test]
public void TestSongSelectBackActionHandling()
{
@@ -1,11 +1,13 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Localisation;
using osuTK;
namespace osu.Game.Tests.Visual.UserInterface
@@ -25,7 +27,10 @@ namespace osu.Game.Tests.Visual.UserInterface
RelativeSizeAxes = Axes.Both,
Child = new FillFlowContainer
{
RelativeSizeAxes = Axes.Both,
RelativeSizeAxes = Axes.Y,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 400,
Direction = FillDirection.Vertical,
Spacing = new Vector2(5),
Padding = new MarginPadding(10),
@@ -53,9 +58,45 @@ namespace osu.Game.Tests.Visual.UserInterface
PlaceholderText = "Mine is 42!",
TabbableContentContainer = this,
},
new FormCheckBox
{
Caption = EditorSetupStrings.LetterboxDuringBreaks,
HintText = EditorSetupStrings.LetterboxDuringBreaksDescription,
},
new FormCheckBox
{
Caption = EditorSetupStrings.LetterboxDuringBreaks,
HintText = EditorSetupStrings.LetterboxDuringBreaksDescription,
Current = { Disabled = true },
},
new FormSliderBar<float>
{
Caption = "Instantaneous slider",
Current = new BindableFloat
{
MinValue = 0,
MaxValue = 10,
Value = 5,
Precision = 0.1f,
},
TabbableContentContainer = this,
},
new FormSliderBar<float>
{
Caption = "Non-instantaneous slider",
Current = new BindableFloat
{
MinValue = 0,
MaxValue = 10,
Value = 5,
Precision = 0.1f,
},
Instantaneous = false,
TabbableContentContainer = this,
},
},
},
},
}
};
}
}
@@ -3,6 +3,7 @@
using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Threading.Tasks;
using Microsoft.Data.Sqlite;
@@ -78,7 +79,7 @@ namespace osu.Game.Beatmaps
// cached database exists on disk.
&& storage.Exists(cache_database_name);
public bool TryLookup(BeatmapInfo beatmapInfo, out OnlineBeatmapMetadata? onlineMetadata)
public bool TryLookup(BeatmapInfo beatmapInfo, [NotNullWhen(true)] out OnlineBeatmapMetadata? onlineMetadata)
{
Debug.Assert(beatmapInfo.BeatmapSet != null);
@@ -98,7 +99,7 @@ namespace osu.Game.Beatmaps
try
{
using (var db = new SqliteConnection(string.Concat(@"Data Source=", storage.GetFullPath(@"online.db", true))))
using (var db = getConnection())
{
db.Open();
@@ -125,6 +126,9 @@ namespace osu.Game.Beatmaps
return false;
}
private SqliteConnection getConnection() =>
new SqliteConnection(string.Concat(@"Data Source=", storage.GetFullPath(@"online.db", true)));
private void prepareLocalCache()
{
bool isRefetch = storage.Exists(cache_database_name);
@@ -191,6 +195,15 @@ namespace osu.Game.Beatmaps
});
}
public int GetCacheVersion()
{
using (var connection = getConnection())
{
connection.Open();
return getCacheVersion(connection);
}
}
private int getCacheVersion(SqliteConnection connection)
{
using (var cmd = connection.CreateCommand())
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@@ -11,6 +12,7 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Game.Beatmaps;
using osu.Game.Extensions;
using osu.Game.Online.API;
@@ -61,6 +63,9 @@ namespace osu.Game.Database
[Resolved]
private IAPIProvider api { get; set; } = null!;
[Resolved]
private Storage storage { get; set; } = null!;
protected virtual int TimeToSleepDuringGameplay => 30000;
protected override void LoadComplete()
@@ -78,6 +83,7 @@ namespace osu.Game.Database
processScoresWithMissingStatistics();
convertLegacyTotalScoreToStandardised();
upgradeScoreRanks();
backpopulateMissingSubmissionAndRankDates();
}, TaskCreationOptions.LongRunning).ContinueWith(t =>
{
if (t.Exception?.InnerException is ObjectDisposedException)
@@ -443,6 +449,104 @@ namespace osu.Game.Database
completeNotification(notification, processedCount, scoreIds.Count, failedCount);
}
private void backpopulateMissingSubmissionAndRankDates()
{
var localMetadataSource = new LocalCachedBeatmapMetadataSource(storage);
if (!localMetadataSource.Available)
{
Logger.Log("Cannot backpopulate missing submission/rank dates because the local metadata cache is missing.");
return;
}
try
{
if (localMetadataSource.GetCacheVersion() < 2)
{
Logger.Log("Cannot backpopulate missing submission/rank dates because the local metadata cache is too old.");
return;
}
}
catch (Exception ex)
{
Logger.Log($"Error when trying to query version of local metadata cache: {ex}");
return;
}
Logger.Log("Querying for beatmap sets that contain missing submission/rank date...");
HashSet<Guid> beatmapSetIds = realmAccess.Run(r => new HashSet<Guid>(
r.All<BeatmapSetInfo>()
.Where(b => b.StatusInt > 0 && (b.DateRanked == null || b.DateSubmitted == null))
.AsEnumerable()
.Select(b => b.ID)));
if (beatmapSetIds.Count == 0)
return;
Logger.Log($"Found {beatmapSetIds.Count} beatmap sets with missing submission/rank date.");
var notification = showProgressNotification(beatmapSetIds.Count, "Populating missing submission and rank dates", "beatmap sets now have correct submission and rank dates.");
int processedCount = 0;
int failedCount = 0;
foreach (var id in beatmapSetIds)
{
if (notification?.State == ProgressNotificationState.Cancelled)
break;
updateNotificationProgress(notification, processedCount, beatmapSetIds.Count);
sleepIfRequired();
try
{
// Can't use async overload because we're not on the update thread.
// ReSharper disable once MethodHasAsyncOverload
bool succeeded = realmAccess.Write(r =>
{
BeatmapSetInfo beatmapSet = r.Find<BeatmapSetInfo>(id)!;
// we want any ranked representative of the set.
// the reason for checking ranked status of the difficulty is that it can be locally modified,
// at which point the lookup will fail - but there might still be another unmodified difficulty on which it will work.
if (beatmapSet.Beatmaps.FirstOrDefault(b => b.Status >= BeatmapOnlineStatus.Ranked) is not BeatmapInfo beatmap)
return false;
bool lookupSucceeded = localMetadataSource.TryLookup(beatmap, out var result);
if (lookupSucceeded)
{
Debug.Assert(result != null);
beatmapSet.DateRanked = result.DateRanked;
beatmapSet.DateSubmitted = result.DateSubmitted;
return true;
}
Logger.Log($"Could not find {beatmapSet.GetDisplayString()} in local cache while backpopulating missing submission/rank date");
return false;
});
if (succeeded)
++processedCount;
else
++failedCount;
}
catch (ObjectDisposedException)
{
throw;
}
catch (Exception e)
{
Logger.Log($"Failed to update ranked/submitted dates for beatmap set {id}: {e}");
++failedCount;
}
}
completeNotification(notification, processedCount, beatmapSetIds.Count, failedCount);
}
private void updateNotificationProgress(ProgressNotification? notification, int processedCount, int totalCount)
{
if (notification == null)
@@ -75,7 +75,7 @@ namespace osu.Game.Graphics.UserInterface
[BackgroundDependencyLoader(true)]
private void load(OverlayColourProvider? colourProvider, OsuColour colours, AudioManager audio)
{
BackgroundColour = colourProvider?.Background5 ?? Color4.Black.Opacity(0.5f);
BackgroundColour = colourProvider?.Background5 ?? Color4.Black;
HoverColour = colourProvider?.Light4 ?? colours.PinkDarker;
SelectionColour = colourProvider?.Background3 ?? colours.PinkDarker.Opacity(0.5f);
@@ -397,7 +397,7 @@ namespace osu.Game.Graphics.UserInterface
{
bool hovered = Enabled.Value && IsHovered;
var hoveredColour = colourProvider?.Light4 ?? colours.PinkDarker;
var unhoveredColour = colourProvider?.Background5 ?? Color4.Black.Opacity(0.5f);
var unhoveredColour = colourProvider?.Background5 ?? Color4.Black;
Colour = Color4.White;
Alpha = Enabled.Value ? 1 : 0.3f;
@@ -46,7 +46,7 @@ namespace osu.Game.Graphics.UserInterface
protected override void LoadComplete()
{
base.LoadComplete();
CurrentNumber.BindValueChanged(current => TooltipText = getTooltipText(current.NewValue), true);
CurrentNumber.BindValueChanged(current => TooltipText = GetDisplayableValue(current.NewValue), true);
}
protected override void OnUserChange(T value)
@@ -55,7 +55,7 @@ namespace osu.Game.Graphics.UserInterface
playSample(value);
TooltipText = getTooltipText(value);
TooltipText = GetDisplayableValue(value);
}
private void playSample(T value)
@@ -83,7 +83,7 @@ namespace osu.Game.Graphics.UserInterface
channel.Play();
}
private LocalisableString getTooltipText(T value)
public LocalisableString GetDisplayableValue(T value)
{
if (CurrentNumber.IsInteger)
return int.CreateTruncating(value).ToString("N0");
@@ -0,0 +1,161 @@
// 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.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Localisation;
using osu.Game.Overlays;
namespace osu.Game.Graphics.UserInterfaceV2
{
public partial class FormCheckBox : CompositeDrawable, IHasCurrentValue<bool>
{
public Bindable<bool> Current
{
get => current.Current;
set => current.Current = value;
}
private readonly BindableWithCurrent<bool> current = new BindableWithCurrent<bool>();
/// <summary>
/// Caption describing this slider bar, displayed on top of the controls.
/// </summary>
public LocalisableString Caption { get; init; }
/// <summary>
/// Hint text containing an extended description of this slider bar, displayed in a tooltip when hovering the caption.
/// </summary>
public LocalisableString HintText { get; init; }
private Box background = null!;
private FormFieldCaption caption = null!;
private OsuSpriteText text = null!;
private Nub checkbox = null!;
private Sample? sampleChecked;
private Sample? sampleUnchecked;
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
[BackgroundDependencyLoader]
private void load(AudioManager audio)
{
RelativeSizeAxes = Axes.X;
Height = 50;
Masking = true;
CornerRadius = 5;
CornerExponent = 2.5f;
InternalChildren = new Drawable[]
{
background = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background5,
},
new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding(9),
Children = new Drawable[]
{
caption = new FormFieldCaption
{
Caption = Caption,
TooltipText = HintText,
Anchor = Anchor.TopLeft,
Origin = Anchor.TopLeft,
},
text = new OsuSpriteText
{
RelativeSizeAxes = Axes.X,
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
},
checkbox = new Nub
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Current = Current,
},
},
},
};
sampleChecked = audio.Samples.Get(@"UI/check-on");
sampleUnchecked = audio.Samples.Get(@"UI/check-off");
}
protected override void LoadComplete()
{
base.LoadComplete();
current.BindValueChanged(_ =>
{
updateState();
playSamples();
background.FlashColour(ColourInfo.GradientVertical(colourProvider.Background5, colourProvider.Dark2), 800, Easing.OutQuint);
});
current.BindDisabledChanged(_ => updateState(), true);
}
private void playSamples()
{
if (Current.Value)
sampleChecked?.Play();
else
sampleUnchecked?.Play();
}
protected override bool OnHover(HoverEvent e)
{
updateState();
return true;
}
protected override void OnHoverLost(HoverLostEvent e)
{
base.OnHoverLost(e);
updateState();
}
protected override bool OnClick(ClickEvent e)
{
if (!Current.Disabled)
Current.Value = !Current.Value;
return true;
}
private void updateState()
{
background.Colour = Current.Disabled ? colourProvider.Background4 : colourProvider.Background5;
caption.Colour = Current.Disabled ? colourProvider.Foreground1 : colourProvider.Content2;
checkbox.Colour = Current.Disabled ? colourProvider.Foreground1 : colourProvider.Content1;
text.Colour = Current.Disabled ? colourProvider.Foreground1 : colourProvider.Content1;
text.Text = Current.Value ? CommonStrings.Enabled : CommonStrings.Disabled;
if (!Current.Disabled)
{
BorderThickness = IsHovered ? 2 : 0;
if (IsHovered)
BorderColour = colourProvider.Light4;
}
}
}
}
@@ -0,0 +1,374 @@
// 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.Globalization;
using System.Numerics;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
namespace osu.Game.Graphics.UserInterfaceV2
{
public partial class FormSliderBar<T> : CompositeDrawable, IHasCurrentValue<T>
where T : struct, INumber<T>, IMinMaxValue<T>
{
public Bindable<T> Current
{
get => current.Current;
set => current.Current = value;
}
private bool instantaneous = true;
/// <summary>
/// Whether changes to the slider should instantaneously transfer to the text box (and vice versa).
/// If <see langword="false"/>, the transfer will happen on text box commit (explicit, or implicit via focus loss), or on slider drag end.
/// </summary>
public bool Instantaneous
{
get => instantaneous;
set
{
instantaneous = value;
if (slider.IsNotNull())
slider.TransferValueOnCommit = !instantaneous;
}
}
private CompositeDrawable? tabbableContentContainer;
public CompositeDrawable? TabbableContentContainer
{
set
{
tabbableContentContainer = value;
if (textBox.IsNotNull())
textBox.TabbableContentContainer = tabbableContentContainer;
}
}
private readonly BindableNumberWithCurrent<T> current = new BindableNumberWithCurrent<T>();
/// <summary>
/// Caption describing this slider bar, displayed on top of the controls.
/// </summary>
public LocalisableString Caption { get; init; }
/// <summary>
/// Hint text containing an extended description of this slider bar, displayed in a tooltip when hovering the caption.
/// </summary>
public LocalisableString HintText { get; init; }
private Box background = null!;
private Box flashLayer = null!;
private FormTextBox.InnerTextBox textBox = null!;
private Slider slider = null!;
private FormFieldCaption caption = null!;
private IFocusManager focusManager = null!;
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
RelativeSizeAxes = Axes.X;
Height = 50;
Masking = true;
CornerRadius = 5;
InternalChildren = new Drawable[]
{
background = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background5,
},
flashLayer = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Colour4.Transparent,
},
new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding(9),
Children = new Drawable[]
{
caption = new FormFieldCaption
{
Anchor = Anchor.TopLeft,
Origin = Anchor.TopLeft,
Caption = Caption,
TooltipText = HintText,
},
textBox = new FormNumberBox.InnerNumberBox
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
RelativeSizeAxes = Axes.X,
Width = 0.5f,
CommitOnFocusLost = true,
SelectAllOnFocus = true,
AllowDecimals = true,
OnInputError = () =>
{
flashLayer.Colour = ColourInfo.GradientVertical(colours.Red3.Opacity(0), colours.Red3);
flashLayer.FadeOutFromOne(200, Easing.OutQuint);
},
TabbableContentContainer = tabbableContentContainer,
},
slider = new Slider
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
RelativeSizeAxes = Axes.X,
Width = 0.5f,
Current = Current,
TransferValueOnCommit = !instantaneous,
}
},
},
};
}
protected override void LoadComplete()
{
base.LoadComplete();
focusManager = GetContainingFocusManager()!;
textBox.Focused.BindValueChanged(_ => updateState());
textBox.OnCommit += textCommitted;
textBox.Current.BindValueChanged(textChanged);
slider.IsDragging.BindValueChanged(_ => updateState());
current.BindValueChanged(_ =>
{
updateState();
updateTextBoxFromSlider();
}, true);
}
private bool updatingFromTextBox;
private void textChanged(ValueChangedEvent<string> change)
{
if (!instantaneous) return;
tryUpdateSliderFromTextBox();
}
private void textCommitted(TextBox t, bool isNew)
{
tryUpdateSliderFromTextBox();
// If the attempted update above failed, restore text box to match the slider.
Current.TriggerChange();
flashLayer.Colour = ColourInfo.GradientVertical(colourProvider.Dark2.Opacity(0), colourProvider.Dark2);
flashLayer.FadeOutFromOne(800, Easing.OutQuint);
}
private void tryUpdateSliderFromTextBox()
{
updatingFromTextBox = true;
try
{
switch (Current)
{
case Bindable<int> bindableInt:
bindableInt.Value = int.Parse(textBox.Current.Value);
break;
case Bindable<double> bindableDouble:
bindableDouble.Value = double.Parse(textBox.Current.Value);
break;
default:
Current.Parse(textBox.Current.Value, CultureInfo.CurrentCulture);
break;
}
}
catch
{
// ignore parsing failures.
// sane state will eventually be restored by a commit (either explicit, or implicit via focus loss).
}
updatingFromTextBox = false;
}
protected override bool OnHover(HoverEvent e)
{
updateState();
return true;
}
protected override void OnHoverLost(HoverLostEvent e)
{
base.OnHoverLost(e);
updateState();
}
protected override bool OnClick(ClickEvent e)
{
focusManager.ChangeFocus(textBox);
return true;
}
private void updateState()
{
textBox.Alpha = 1;
background.Colour = Current.Disabled ? colourProvider.Background4 : colourProvider.Background5;
caption.Colour = Current.Disabled ? colourProvider.Foreground1 : colourProvider.Content2;
textBox.Colour = Current.Disabled ? colourProvider.Foreground1 : colourProvider.Content1;
BorderThickness = IsHovered || textBox.Focused.Value || slider.IsDragging.Value ? 2 : 0;
BorderColour = textBox.Focused.Value ? colourProvider.Highlight1 : colourProvider.Light4;
if (textBox.Focused.Value)
background.Colour = ColourInfo.GradientVertical(colourProvider.Background5, colourProvider.Dark3);
else if (IsHovered || slider.IsDragging.Value)
background.Colour = ColourInfo.GradientVertical(colourProvider.Background5, colourProvider.Dark4);
else
background.Colour = colourProvider.Background5;
}
private void updateTextBoxFromSlider()
{
if (updatingFromTextBox) return;
textBox.Text = slider.GetDisplayableValue(Current.Value).ToString();
}
private partial class Slider : OsuSliderBar<T>
{
public BindableBool IsDragging { get; set; } = new BindableBool();
private Box leftBox = null!;
private Box rightBox = null!;
private Circle nub = null!;
private const float nub_width = 10;
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
[BackgroundDependencyLoader]
private void load()
{
Height = 40;
RelativeSizeAxes = Axes.X;
RangePadding = nub_width / 2;
Children = new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.Both,
Masking = true,
CornerRadius = 5,
Children = new Drawable[]
{
leftBox = new Box
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
},
rightBox = new Box
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
},
},
},
new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Horizontal = RangePadding, },
Child = nub = new Circle
{
Width = nub_width,
RelativeSizeAxes = Axes.Y,
RelativePositionAxes = Axes.X,
Origin = Anchor.TopCentre,
}
},
new HoverClickSounds()
};
}
protected override void LoadComplete()
{
base.LoadComplete();
updateState();
}
protected override void UpdateAfterChildren()
{
base.UpdateAfterChildren();
leftBox.Width = Math.Clamp(RangePadding + nub.DrawPosition.X, 0, Math.Max(0, DrawWidth)) / DrawWidth;
rightBox.Width = Math.Clamp(DrawWidth - nub.DrawPosition.X - RangePadding, 0, Math.Max(0, DrawWidth)) / DrawWidth;
}
protected override bool OnDragStart(DragStartEvent e)
{
bool dragging = base.OnDragStart(e);
IsDragging.Value = dragging;
updateState();
return dragging;
}
protected override void OnDragEnd(DragEndEvent e)
{
base.OnDragEnd(e);
IsDragging.Value = false;
updateState();
}
protected override bool OnHover(HoverEvent e)
{
updateState();
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
updateState();
base.OnHoverLost(e);
}
private void updateState()
{
rightBox.Colour = colourProvider.Background6;
leftBox.Colour = IsHovered || IsDragged ? colourProvider.Highlight1.Opacity(0.5f) : colourProvider.Dark2;
nub.Colour = IsHovered || IsDragged ? colourProvider.Highlight1 : colourProvider.Light4;
}
protected override void UpdateValue(float value)
{
nub.MoveToX(value, 200, Easing.OutPow10);
}
}
}
}
@@ -59,8 +59,19 @@ namespace osu.Game.Graphics.UserInterfaceV2
private readonly BindableWithCurrent<string> current = new BindableWithCurrent<string>();
/// <summary>
/// Caption describing this slider bar, displayed on top of the controls.
/// </summary>
public LocalisableString Caption { get; init; }
/// <summary>
/// Hint text containing an extended description of this slider bar, displayed in a tooltip when hovering the caption.
/// </summary>
public LocalisableString HintText { get; init; }
/// <summary>
/// Text displayed in the text box when its contents are empty.
/// </summary>
public LocalisableString PlaceholderText { get; init; }
private Box background = null!;
@@ -122,7 +133,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
if (!current.Disabled && !ReadOnly)
{
flashLayer.Colour = ColourInfo.GradientVertical(colourProvider.Dark1.Opacity(0), colourProvider.Dark2);
flashLayer.Colour = ColourInfo.GradientVertical(colourProvider.Dark2.Opacity(0), colourProvider.Dark2);
flashLayer.FadeOutFromOne(800, Easing.OutQuint);
}
};
@@ -73,7 +73,7 @@ namespace osu.Game.Overlays.SkinEditor
isFlippedY = false;
}
public override void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both)
public override void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both, float axisRotation = 0)
{
if (objectsInScale == null)
throw new InvalidOperationException($"Cannot {nameof(Update)} a scale operation without calling {nameof(Begin)} first!");
@@ -52,10 +52,11 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// If the default <see langword="null"/> value is supplied, a sane implementation-defined default will be used.
/// </param>
/// <param name="adjustAxis">The axes to adjust the scale in.</param>
public void ScaleSelection(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both)
/// <param name="axisRotation">The rotation of the axes in degrees.</param>
public void ScaleSelection(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both, float axisRotation = 0)
{
Begin();
Update(scale, origin, adjustAxis);
Update(scale, origin, adjustAxis, axisRotation);
Commit();
}
@@ -91,7 +92,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// If the default <see langword="null"/> value is supplied, a sane implementation-defined default will be used.
/// </param>
/// <param name="adjustAxis">The axes to adjust the scale in.</param>
public virtual void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both)
/// <param name="axisRotation">The rotation of the axes in degrees.</param>
public virtual void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both, float axisRotation = 0)
{
}
+12 -3
View File
@@ -39,14 +39,23 @@ namespace osu.Game.Screens.Play.HUD
private IBindable<APIUser>? apiUser;
private readonly Container cornerContainer;
public PlayerAvatar()
{
Size = new Vector2(default_size);
InternalChild = avatar = new UpdateableAvatar(isInteractive: false)
InternalChild = cornerContainer = new Container
{
Masking = true,
RelativeSizeAxes = Axes.Both,
Masking = true
Child = avatar = new UpdateableAvatar(isInteractive: false)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fill,
}
};
}
@@ -66,7 +75,7 @@ namespace osu.Game.Screens.Play.HUD
{
base.LoadComplete();
CornerRadius.BindValueChanged(e => avatar.CornerRadius = e.NewValue * default_size, true);
CornerRadius.BindValueChanged(e => cornerContainer.CornerRadius = e.NewValue * default_size, true);
}
public bool UsesFixedAnchor { get; set; }
+19 -7
View File
@@ -9,6 +9,7 @@ using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Audio.Track;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
@@ -16,6 +17,7 @@ using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Utils;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
@@ -26,7 +28,7 @@ using osuTK.Graphics;
namespace osu.Game.Screens.Play
{
public partial class SkipOverlay : CompositeDrawable, IKeyBindingHandler<GlobalAction>
public partial class SkipOverlay : BeatSyncedContainer, IKeyBindingHandler<GlobalAction>
{
/// <summary>
/// The total number of successful skips performed by this overlay.
@@ -36,10 +38,9 @@ namespace osu.Game.Screens.Play
private readonly double startTime;
public Action RequestSkip;
private Button button;
private ButtonContainer buttonContainer;
private Box remainingTimeBox;
private Circle remainingTimeBox;
private FadeContainer fadeContainer;
private double displayTime;
@@ -51,7 +52,6 @@ namespace osu.Game.Screens.Play
private IGameplayClock gameplayClock { get; set; }
internal bool IsButtonVisible => fadeContainer.State == Visibility.Visible && buttonContainer.State.Value == Visibility.Visible;
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
/// <summary>
@@ -87,13 +87,13 @@ namespace osu.Game.Screens.Play
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
remainingTimeBox = new Box
remainingTimeBox = new Circle
{
Height = 5,
RelativeSizeAxes = Axes.X,
Colour = colours.Yellow,
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
Colour = colours.Yellow,
RelativeSizeAxes = Axes.X
}
}
}
@@ -210,6 +210,18 @@ namespace osu.Game.Screens.Play
{
}
protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes)
{
base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes);
if (fadeOutBeginTime <= gameplayClock.CurrentTime)
return;
float progress = (float)(gameplayClock.CurrentTime - displayTime) / (float)(fadeOutBeginTime - displayTime);
float newWidth = 1 - Math.Clamp(progress, 0, 1);
remainingTimeBox.ResizeWidthTo(newWidth, timingPoint.BeatLength * 2, Easing.OutQuint);
}
public partial class FadeContainer : Container, IStateful<Visibility>
{
[CanBeNull]
+3 -4
View File
@@ -706,7 +706,7 @@ namespace osu.Game.Screens.Select
private bool beatmapsSplitOut;
private void applyActiveCriteria(bool debounce, bool alwaysResetScrollPosition = true)
private void applyActiveCriteria(bool debounce)
{
PendingFilter?.Cancel();
PendingFilter = null;
@@ -735,8 +735,7 @@ namespace osu.Game.Screens.Select
root.Filter(activeCriteria);
itemsCache.Invalidate();
if (alwaysResetScrollPosition || !Scroll.UserScrolling)
ScrollToSelected(true);
ScrollToSelected(true);
FilterApplied?.Invoke();
}
@@ -1036,7 +1035,7 @@ namespace osu.Game.Screens.Select
itemsCache.Validate();
// update and let external consumers know about selection loss.
if (BeatmapSetsLoaded)
if (BeatmapSetsLoaded && AllowSelection)
{
bool selectionLost = selectedBeatmapSet != null && selectedBeatmapSet.State.Value != CarouselItemState.Selected;
+22 -1
View File
@@ -127,6 +127,8 @@ namespace osu.Game.Screens.Select
private Sample sampleChangeDifficulty = null!;
private Sample sampleChangeBeatmap = null!;
private bool pendingFilterApplication;
private Container carouselContainer = null!;
protected BeatmapDetailArea BeatmapDetails { get; private set; } = null!;
@@ -328,7 +330,20 @@ namespace osu.Game.Screens.Select
GetRecommendedBeatmap = s => recommender?.GetRecommendedBeatmap(s),
}, c => carouselContainer.Child = c);
FilterControl.FilterChanged = Carousel.Filter;
FilterControl.FilterChanged = criteria =>
{
// If a filter operation is applied when we're in a state that doesn't allow selection,
// we might end up in an unexpected state. This is because currently carousel panels are in charge
// of updating the global selection (which is very hard to deal with).
//
// For now let's just avoid filtering when selection isn't allowed locally.
// This should be nuked from existence when we get around to fixing the complexity of song select <-> beatmap carousel.
// The debounce part of BeatmapCarousel's filtering should probably also be removed and handled locally.
if (Carousel.AllowSelection)
Carousel.Filter(criteria);
else
pendingFilterApplication = true;
};
if (ShowSongSelectFooter)
{
@@ -701,6 +716,12 @@ namespace osu.Game.Screens.Select
Carousel.AllowSelection = true;
if (pendingFilterApplication)
{
Carousel.Filter(FilterControl.CreateCriteria());
pendingFilterApplication = false;
}
BeatmapDetails.Refresh();
beginLooping();
+81 -4
View File
@@ -51,6 +51,9 @@ namespace osu.Game.Utils
/// Given a flip direction, a surrounding quad for all selected objects, and a position,
/// will return the flipped position in screen space coordinates.
/// </summary>
/// <param name="direction">The direction to flip towards.</param>
/// <param name="quad">The quad surrounding all selected objects. The center of this determines the position of the axis.</param>
/// <param name="position">The position to flip.</param>
public static Vector2 GetFlippedPosition(Direction direction, Quad quad, Vector2 position)
{
var centre = quad.Centre;
@@ -69,6 +72,20 @@ namespace osu.Game.Utils
return position;
}
/// <summary>
/// Given a flip axis vector, a surrounding quad for all selected objects, and a position,
/// will return the flipped position in screen space coordinates.
/// </summary>
/// <param name="axis">The vector indicating the direction to flip towards. This is perpendicular to the mirroring axis.</param>
/// <param name="quad">The quad surrounding all selected objects. The center of this determines the position of the axis.</param>
/// <param name="position">The position to flip.</param>
public static Vector2 GetFlippedPosition(Vector2 axis, Quad quad, Vector2 position)
{
var centre = quad.Centre;
return position - 2 * Vector2.Dot(position - centre, axis) * axis;
}
/// <summary>
/// Given a scale vector, a surrounding quad for all selected objects, and a position,
/// will return the scaled position in screen space coordinates.
@@ -93,9 +110,9 @@ namespace osu.Game.Utils
/// Given a scale multiplier, an origin, and a position,
/// will return the scaled position in screen space coordinates.
/// </summary>
public static Vector2 GetScaledPosition(Vector2 scale, Vector2 origin, Vector2 position)
public static Vector2 GetScaledPosition(Vector2 scale, Vector2 origin, Vector2 position, float axisRotation = 0)
{
return origin + (position - origin) * scale;
return origin + RotateVector(RotateVector(position - origin, axisRotation) * scale, -axisRotation);
}
/// <summary>
@@ -127,7 +144,67 @@ namespace osu.Game.Utils
/// </summary>
/// <param name="hitObjects">The hit objects to calculate a quad for.</param>
public static Quad GetSurroundingQuad(IEnumerable<IHasPosition> hitObjects) =>
GetSurroundingQuad(hitObjects.SelectMany(h =>
GetSurroundingQuad(enumerateStartAndEndPositions(hitObjects));
/// <summary>
/// Returns the points that make up the convex hull of the provided points.
/// </summary>
/// <param name="points">The points to calculate a convex hull.</param>
public static List<Vector2> GetConvexHull(IEnumerable<Vector2> points)
{
var pointsList = points.OrderBy(p => p.X).ThenBy(p => p.Y).ToList();
if (pointsList.Count < 3)
return pointsList;
var convexHullLower = new List<Vector2>
{
pointsList[0],
pointsList[1]
};
var convexHullUpper = new List<Vector2>
{
pointsList[^1],
pointsList[^2]
};
// Build the lower hull.
for (int i = 2; i < pointsList.Count; i++)
{
Vector2 c = pointsList[i];
while (convexHullLower.Count > 1 && isClockwise(convexHullLower[^2], convexHullLower[^1], c))
convexHullLower.RemoveAt(convexHullLower.Count - 1);
convexHullLower.Add(c);
}
// Build the upper hull.
for (int i = pointsList.Count - 3; i >= 0; i--)
{
Vector2 c = pointsList[i];
while (convexHullUpper.Count > 1 && isClockwise(convexHullUpper[^2], convexHullUpper[^1], c))
convexHullUpper.RemoveAt(convexHullUpper.Count - 1);
convexHullUpper.Add(c);
}
convexHullLower.RemoveAt(convexHullLower.Count - 1);
convexHullUpper.RemoveAt(convexHullUpper.Count - 1);
convexHullLower.AddRange(convexHullUpper);
return convexHullLower;
float crossProduct(Vector2 v1, Vector2 v2) => v1.X * v2.Y - v1.Y * v2.X;
bool isClockwise(Vector2 a, Vector2 b, Vector2 c) => crossProduct(b - a, c - a) >= 0;
}
public static List<Vector2> GetConvexHull(IEnumerable<IHasPosition> hitObjects) =>
GetConvexHull(enumerateStartAndEndPositions(hitObjects));
private static IEnumerable<Vector2> enumerateStartAndEndPositions(IEnumerable<IHasPosition> hitObjects) =>
hitObjects.SelectMany(h =>
{
if (h is IHasPath path)
{
@@ -140,6 +217,6 @@ namespace osu.Game.Utils
}
return new[] { h.Position };
}));
});
}
}
+1 -1
View File
@@ -35,7 +35,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Realm" Version="11.5.0" />
<PackageReference Include="ppy.osu.Framework" Version="2024.912.0" />
<PackageReference Include="ppy.osu.Framework" Version="2024.916.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2024.904.0" />
<PackageReference Include="Sentry" Version="4.3.0" />
<!-- Held back due to 0.34.0 failing AOT compilation on ZstdSharp.dll dependency. -->
+1 -1
View File
@@ -17,6 +17,6 @@
<MtouchInterpreter>-all</MtouchInterpreter>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.iOS" Version="2024.912.0" />
<PackageReference Include="ppy.osu.Framework.iOS" Version="2024.916.0" />
</ItemGroup>
</Project>