diff --git a/osu.Desktop/Updater/VelopackUpdateManager.cs b/osu.Desktop/Updater/VelopackUpdateManager.cs
index ae58a8793c..7a79284533 100644
--- a/osu.Desktop/Updater/VelopackUpdateManager.cs
+++ b/osu.Desktop/Updater/VelopackUpdateManager.cs
@@ -66,7 +66,7 @@ namespace osu.Desktop.Updater
{
Activated = () =>
{
- restartToApplyUpdate();
+ Task.Run(restartToApplyUpdate);
return true;
}
});
@@ -88,7 +88,11 @@ namespace osu.Desktop.Updater
{
notification = new UpdateProgressNotification
{
- CompletionClickAction = restartToApplyUpdate,
+ CompletionClickAction = () =>
+ {
+ Task.Run(restartToApplyUpdate);
+ return true;
+ },
};
Schedule(() => notificationOverlay.Post(notification));
@@ -127,13 +131,10 @@ namespace osu.Desktop.Updater
return true;
}
- private bool restartToApplyUpdate()
+ private async Task restartToApplyUpdate()
{
- // TODO: Migrate this to async flow whenever available (see https://github.com/ppy/osu/pull/28743#discussion_r1740505665).
- // Currently there's an internal Thread.Sleep(300) which will cause a stutter when the user clicks to restart.
- updateManager.WaitExitThenApplyUpdates(pendingUpdate?.TargetFullRelease);
+ await updateManager.WaitExitThenApplyUpdatesAsync(pendingUpdate?.TargetFullRelease).ConfigureAwait(false);
Schedule(() => game.AttemptExit());
- return true;
}
}
}
diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj
index bf5f26b352..3df8c16f08 100644
--- a/osu.Desktop/osu.Desktop.csproj
+++ b/osu.Desktop/osu.Desktop.csproj
@@ -26,7 +26,7 @@
-
+
diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs
index 0899212b6c..7d21409ee8 100644
--- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs
@@ -3,7 +3,6 @@
using System;
using System.Collections.Generic;
-using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Beatmaps;
using osu.Game.Rulesets.Catch.Difficulty.Preprocessing;
@@ -44,7 +43,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
StarRating = Math.Sqrt(skills[0].DifficultyValue()) * difficulty_multiplier,
Mods = mods,
ApproachRate = preempt > 1200.0 ? -(preempt - 1800.0) / 120.0 : -(preempt - 1200.0) / 150.0 + 5.0,
- MaxCombo = beatmap.HitObjects.Count(h => h is Fruit) + beatmap.HitObjects.OfType().SelectMany(j => j.NestedHitObjects).Count(h => !(h is TinyDroplet)),
+ MaxCombo = beatmap.GetMaxCombo(),
};
return attributes;
diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
index e93475ecff..c4fcd1f760 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
@@ -81,7 +81,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate;
double drainRate = beatmap.Difficulty.DrainRate;
- int maxCombo = beatmap.GetMaxCombo();
int hitCirclesCount = beatmap.HitObjects.Count(h => h is HitCircle);
int sliderCount = beatmap.HitObjects.Count(h => h is Slider);
@@ -104,7 +103,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
ApproachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5,
OverallDifficulty = (80 - hitWindowGreat) / 6,
DrainRate = drainRate,
- MaxCombo = maxCombo,
+ MaxCombo = beatmap.GetMaxCombo(),
HitCircleCount = hitCirclesCount,
SliderCount = sliderCount,
SpinnerCount = spinnerCount,
diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs
index 56c3ba9315..ea16946dcb 100644
--- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs
+++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs
@@ -105,9 +105,7 @@ namespace osu.Game.Rulesets.Osu.Edit
// is not looking to change the duration of the slider but expand the whole pattern.
if (objectsInScale.Count == 1 && objectsInScale.First().Key is Slider slider)
{
- var originalInfo = objectsInScale[slider];
- Debug.Assert(originalInfo.PathControlPointPositions != null && originalInfo.PathControlPointTypes != null);
- scaleSlider(slider, scale, originalInfo.PathControlPointPositions, originalInfo.PathControlPointTypes, axisRotation);
+ scaleSlider(slider, scale, actualOrigin, objectsInScale[slider], axisRotation);
}
else
{
@@ -159,21 +157,25 @@ namespace osu.Game.Rulesets.Osu.Edit
return scale;
}
- private void scaleSlider(Slider slider, Vector2 scale, Vector2[] originalPathPositions, PathType?[] originalPathTypes, float axisRotation = 0)
+ private void scaleSlider(Slider slider, Vector2 scale, Vector2 origin, OriginalHitObjectState originalInfo, float axisRotation = 0)
{
+ Debug.Assert(originalInfo.PathControlPointPositions != null && originalInfo.PathControlPointTypes != null);
+
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 = GeometryUtils.GetScaledPosition(scale, Vector2.Zero, originalPathPositions[i], axisRotation);
- slider.Path.ControlPoints[i].Type = originalPathTypes[i];
+ slider.Path.ControlPoints[i].Position = GeometryUtils.GetScaledPosition(scale, Vector2.Zero, originalInfo.PathControlPointPositions[i], axisRotation);
+ slider.Path.ControlPoints[i].Type = originalInfo.PathControlPointTypes[i];
}
// Snap the slider's length to the current beat divisor
// to calculate the final resulting duration / bounding box before the final checks.
slider.SnapTo(snapProvider);
+ slider.Position = GeometryUtils.GetScaledPosition(scale, origin, originalInfo.Position, axisRotation);
+
//if sliderhead or sliderend end up outside playfield, revert scaling.
Quad scaledQuad = GeometryUtils.GetSurroundingQuad(new OsuHitObject[] { slider });
(bool xInBounds, bool yInBounds) = isQuadInBounds(scaledQuad);
@@ -182,7 +184,9 @@ namespace osu.Game.Rulesets.Osu.Edit
return;
for (int i = 0; i < slider.Path.ControlPoints.Count; i++)
- slider.Path.ControlPoints[i].Position = originalPathPositions[i];
+ slider.Path.ControlPoints[i].Position = originalInfo.PathControlPointPositions[i];
+
+ slider.Position = originalInfo.Position;
// Snap the slider's length again to undo the potentially-invalid length applied by the previous snap.
slider.SnapTo(snapProvider);
diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs
index dff370d259..33b0c14185 100644
--- a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs
+++ b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs
@@ -10,7 +10,10 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
+using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Components.RadioButtons;
using osuTK;
@@ -35,6 +38,8 @@ namespace osu.Game.Rulesets.Osu.Edit
private OsuCheckbox xCheckBox = null!;
private OsuCheckbox yCheckBox = null!;
+ private BindableList selectedItems { get; } = new BindableList();
+
public PreciseScalePopover(OsuSelectionScaleHandler scaleHandler, OsuGridToolboxGroup gridToolbox)
{
this.scaleHandler = scaleHandler;
@@ -44,8 +49,10 @@ namespace osu.Game.Rulesets.Osu.Edit
}
[BackgroundDependencyLoader]
- private void load()
+ private void load(EditorBeatmap editorBeatmap)
{
+ selectedItems.BindTo(editorBeatmap.SelectedHitObjects);
+
Child = new FillFlowContainer
{
Width = 220,
@@ -191,14 +198,26 @@ namespace osu.Game.Rulesets.Osu.Edit
updateAxisCheckBoxesEnabled();
}
- private Vector2? getOriginPosition(PreciseScaleInfo scale) =>
- scale.Origin switch
+ private Vector2? getOriginPosition(PreciseScaleInfo scale)
+ {
+ switch (scale.Origin)
{
- ScaleOrigin.GridCentre => gridToolbox.StartPosition.Value,
- ScaleOrigin.PlayfieldCentre => OsuPlayfield.BASE_SIZE / 2,
- ScaleOrigin.SelectionCentre => null,
- _ => throw new ArgumentOutOfRangeException(nameof(scale))
- };
+ case ScaleOrigin.GridCentre:
+ return gridToolbox.StartPosition.Value;
+
+ case ScaleOrigin.PlayfieldCentre:
+ return OsuPlayfield.BASE_SIZE / 2;
+
+ case ScaleOrigin.SelectionCentre:
+ if (selectedItems.Count == 1 && selectedItems.First() is Slider slider)
+ return slider.Position;
+
+ return null;
+
+ default:
+ throw new ArgumentOutOfRangeException(nameof(scale));
+ }
+ }
private Axes getAdjustAxis(PreciseScaleInfo scale) => scale.XAxis ? scale.YAxis ? Axes.Both : Axes.X : Axes.Y;
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs
index 5f5deca1ba..b3a68ec92d 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs
@@ -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())
+ 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;
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs
index 28323693d0..e3c550fbe9 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs
@@ -14,7 +14,6 @@ using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour;
using osu.Game.Rulesets.Taiko.Difficulty.Skills;
using osu.Game.Rulesets.Taiko.Mods;
-using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.Scoring;
namespace osu.Game.Rulesets.Taiko.Difficulty
@@ -100,7 +99,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
ColourDifficulty = colourRating,
PeakDifficulty = combinedRating,
GreatHitWindow = hitWindows.WindowFor(HitResult.Great) / clockRate,
- MaxCombo = beatmap.HitObjects.Count(h => h is Hit),
+ MaxCombo = beatmap.GetMaxCombo(),
};
return attributes;
diff --git a/osu.Game.Tests/Utils/BindableValueAccessorTest.cs b/osu.Game.Tests/Utils/BindableValueAccessorTest.cs
new file mode 100644
index 0000000000..f09623dbfc
--- /dev/null
+++ b/osu.Game.Tests/Utils/BindableValueAccessorTest.cs
@@ -0,0 +1,52 @@
+// 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.Framework.Bindables;
+using osu.Game.Utils;
+
+namespace osu.Game.Tests.Utils
+{
+ [TestFixture]
+ public class BindableValueAccessorTest
+ {
+ [Test]
+ public void GetValue()
+ {
+ const int value = 1337;
+
+ BindableInt bindable = new BindableInt(value);
+ Assert.That(BindableValueAccessor.GetValue(bindable), Is.EqualTo(value));
+ }
+
+ [Test]
+ public void SetValue()
+ {
+ const int value = 1337;
+
+ BindableInt bindable = new BindableInt();
+ BindableValueAccessor.SetValue(bindable, value);
+
+ Assert.That(bindable.Value, Is.EqualTo(value));
+ }
+
+ [Test]
+ public void GetInvalidBindable()
+ {
+ BindableList