diff --git a/osu.Android.props b/osu.Android.props
index 247140ceef..6dbc6cc377 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -52,10 +52,10 @@
-
+
-
+
diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaDifficultyCalculatorTest.cs
index 9e4db1f1c9..4ae6cb9c7c 100644
--- a/osu.Game.Rulesets.Mania.Tests/ManiaDifficultyCalculatorTest.cs
+++ b/osu.Game.Rulesets.Mania.Tests/ManiaDifficultyCalculatorTest.cs
@@ -16,11 +16,11 @@ namespace osu.Game.Rulesets.Mania.Tests
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Mania";
- [TestCase(2.3449735700206298d, 242, "diffcalc-test")]
+ [TestCase(2.3493769750220914d, 242, "diffcalc-test")]
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
=> base.Test(expectedStarRating, expectedMaxCombo, name);
- [TestCase(2.7879104989252959d, 242, "diffcalc-test")]
+ [TestCase(2.797245912537965d, 242, "diffcalc-test")]
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
=> Test(expectedStarRating, expectedMaxCombo, name, new ManiaModDoubleTime());
diff --git a/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs b/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs
index c2e532430c..2c7c84de97 100644
--- a/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs
+++ b/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs
@@ -21,7 +21,8 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills
protected override double SkillMultiplier => 1;
protected override double StrainDecayBase => 1;
- private readonly double[] holdEndTimes;
+ private readonly double[] startTimes;
+ private readonly double[] endTimes;
private readonly double[] individualStrains;
private double individualStrain;
@@ -30,7 +31,8 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills
public Strain(Mod[] mods, int totalColumns)
: base(mods)
{
- holdEndTimes = new double[totalColumns];
+ startTimes = new double[totalColumns];
+ endTimes = new double[totalColumns];
individualStrains = new double[totalColumns];
overallStrain = 1;
}
@@ -38,32 +40,27 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills
protected override double StrainValueOf(DifficultyHitObject current)
{
var maniaCurrent = (ManiaDifficultyHitObject)current;
+ double startTime = maniaCurrent.StartTime;
double endTime = maniaCurrent.EndTime;
int column = maniaCurrent.BaseObject.Column;
- double closestEndTime = Math.Abs(endTime - maniaCurrent.LastObject.StartTime); // Lowest value we can assume with the current information
-
- double holdFactor = 1.0; // Factor to all additional strains in case something else is held
- double holdAddition = 0; // Addition to the current note in case it's a hold and has to be released awkwardly
bool isOverlapping = false;
- // Fill up the holdEndTimes array
- for (int i = 0; i < holdEndTimes.Length; ++i)
+ double closestEndTime = Math.Abs(endTime - startTime); // Lowest value we can assume with the current information
+ double holdFactor = 1.0; // Factor to all additional strains in case something else is held
+ double holdAddition = 0; // Addition to the current note in case it's a hold and has to be released awkwardly
+
+ for (int i = 0; i < endTimes.Length; ++i)
{
// The current note is overlapped if a previous note or end is overlapping the current note body
- isOverlapping |= Precision.DefinitelyBigger(holdEndTimes[i], maniaCurrent.StartTime, 1) && Precision.DefinitelyBigger(endTime, holdEndTimes[i], 1);
+ isOverlapping |= Precision.DefinitelyBigger(endTimes[i], startTime, 1) && Precision.DefinitelyBigger(endTime, endTimes[i], 1);
// We give a slight bonus to everything if something is held meanwhile
- if (Precision.DefinitelyBigger(holdEndTimes[i], endTime, 1))
+ if (Precision.DefinitelyBigger(endTimes[i], endTime, 1))
holdFactor = 1.25;
- closestEndTime = Math.Min(closestEndTime, Math.Abs(endTime - holdEndTimes[i]));
-
- // Decay individual strains
- individualStrains[i] = applyDecay(individualStrains[i], current.DeltaTime, individual_decay_base);
+ closestEndTime = Math.Min(closestEndTime, Math.Abs(endTime - endTimes[i]));
}
- holdEndTimes[column] = endTime;
-
// The hold addition is given if there was an overlap, however it is only valid if there are no other note with a similar ending.
// Releasing multiple notes is just as easy as releasing 1. Nerfs the hold addition by half if the closest release is release_threshold away.
// holdAddition
@@ -77,12 +74,22 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills
if (isOverlapping)
holdAddition = 1 / (1 + Math.Exp(0.5 * (release_threshold - closestEndTime)));
- // Increase individual strain in own column
+ // Decay and increase individualStrains in own column
+ individualStrains[column] = applyDecay(individualStrains[column], startTime - startTimes[column], individual_decay_base);
individualStrains[column] += 2.0 * holdFactor;
- individualStrain = individualStrains[column];
- overallStrain = applyDecay(overallStrain, current.DeltaTime, overall_decay_base) + (1 + holdAddition) * holdFactor;
+ // For notes at the same time (in a chord), the individualStrain should be the hardest individualStrain out of those columns
+ individualStrain = maniaCurrent.DeltaTime <= 1 ? Math.Max(individualStrain, individualStrains[column]) : individualStrains[column];
+ // Decay and increase overallStrain
+ overallStrain = applyDecay(overallStrain, current.DeltaTime, overall_decay_base);
+ overallStrain += (1 + holdAddition) * holdFactor;
+
+ // Update startTimes and endTimes arrays
+ startTimes[column] = startTime;
+ endTimes[column] = endTime;
+
+ // By subtracting CurrentStrain, this skill effectively only considers the maximum strain of any one hitobject within each strain section.
return individualStrain + overallStrain - CurrentStrain;
}
diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs
new file mode 100644
index 0000000000..b68231ce64
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs
@@ -0,0 +1,213 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Utils;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Types;
+using osu.Game.Rulesets.Osu.Objects;
+using osuTK;
+using osuTK.Input;
+
+namespace osu.Game.Rulesets.Osu.Tests.Editor
+{
+ public class TestSceneObjectMerging : TestSceneOsuEditor
+ {
+ [Test]
+ public void TestSimpleMerge()
+ {
+ HitCircle? circle1 = null;
+ HitCircle? circle2 = null;
+
+ AddStep("select first two circles", () =>
+ {
+ circle1 = (HitCircle)EditorBeatmap.HitObjects.First(h => h is HitCircle);
+ circle2 = (HitCircle)EditorBeatmap.HitObjects.First(h => h is HitCircle && h != circle1);
+ EditorClock.Seek(circle1.StartTime);
+ EditorBeatmap.SelectedHitObjects.Add(circle1);
+ EditorBeatmap.SelectedHitObjects.Add(circle2);
+ });
+
+ mergeSelection();
+
+ AddAssert("slider created", () => circle1 is not null && circle2 is not null && sliderCreatedFor(
+ (pos: circle1.Position, pathType: PathType.Linear),
+ (pos: circle2.Position, pathType: null)));
+
+ AddStep("undo", () => Editor.Undo());
+ AddAssert("merged objects restored", () => circle1 is not null && circle2 is not null && objectsRestored(circle1, circle2));
+ }
+
+ [Test]
+ public void TestMergeCircleSlider()
+ {
+ HitCircle? circle1 = null;
+ Slider? slider = null;
+ HitCircle? circle2 = null;
+
+ AddStep("select a circle, slider, circle", () =>
+ {
+ circle1 = (HitCircle)EditorBeatmap.HitObjects.First(h => h is HitCircle);
+ slider = (Slider)EditorBeatmap.HitObjects.First(h => h is Slider && h.StartTime > circle1.StartTime);
+ circle2 = (HitCircle)EditorBeatmap.HitObjects.First(h => h is HitCircle && h.StartTime > slider.StartTime);
+ EditorClock.Seek(circle1.StartTime);
+ EditorBeatmap.SelectedHitObjects.Add(circle1);
+ EditorBeatmap.SelectedHitObjects.Add(slider);
+ EditorBeatmap.SelectedHitObjects.Add(circle2);
+ });
+
+ mergeSelection();
+
+ AddAssert("slider created", () =>
+ {
+ if (circle1 is null || circle2 is null || slider is null)
+ return false;
+
+ var controlPoints = slider.Path.ControlPoints;
+ (Vector2, PathType?)[] args = new (Vector2, PathType?)[controlPoints.Count + 2];
+ args[0] = (circle1.Position, PathType.Linear);
+
+ for (int i = 0; i < controlPoints.Count; i++)
+ {
+ args[i + 1] = (controlPoints[i].Position + slider.Position, i == controlPoints.Count - 1 ? PathType.Linear : controlPoints[i].Type);
+ }
+
+ args[^1] = (circle2.Position, null);
+ return sliderCreatedFor(args);
+ });
+
+ AddStep("undo", () => Editor.Undo());
+ AddAssert("merged objects restored", () => circle1 is not null && circle2 is not null && slider is not null && objectsRestored(circle1, slider, circle2));
+ }
+
+ [Test]
+ public void TestMergeSliderSlider()
+ {
+ Slider? slider1 = null;
+ SliderPath? slider1Path = null;
+ Slider? slider2 = null;
+
+ AddStep("select two sliders", () =>
+ {
+ slider1 = (Slider)EditorBeatmap.HitObjects.First(h => h is Slider);
+ slider1Path = new SliderPath(slider1.Path.ControlPoints.Select(p => new PathControlPoint(p.Position, p.Type)).ToArray(), slider1.Path.ExpectedDistance.Value);
+ slider2 = (Slider)EditorBeatmap.HitObjects.First(h => h is Slider && h.StartTime > slider1.StartTime);
+ EditorClock.Seek(slider1.StartTime);
+ EditorBeatmap.SelectedHitObjects.Add(slider1);
+ EditorBeatmap.SelectedHitObjects.Add(slider2);
+ });
+
+ mergeSelection();
+
+ AddAssert("slider created", () =>
+ {
+ if (slider1 is null || slider2 is null || slider1Path is null)
+ return false;
+
+ var controlPoints1 = slider1Path.ControlPoints;
+ var controlPoints2 = slider2.Path.ControlPoints;
+ (Vector2, PathType?)[] args = new (Vector2, PathType?)[controlPoints1.Count + controlPoints2.Count - 1];
+
+ for (int i = 0; i < controlPoints1.Count - 1; i++)
+ {
+ args[i] = (controlPoints1[i].Position + slider1.Position, controlPoints1[i].Type);
+ }
+
+ for (int i = 0; i < controlPoints2.Count; i++)
+ {
+ args[i + controlPoints1.Count - 1] = (controlPoints2[i].Position + controlPoints1[^1].Position + slider1.Position, controlPoints2[i].Type);
+ }
+
+ return sliderCreatedFor(args);
+ });
+
+ AddAssert("merged slider matches first slider", () =>
+ {
+ var mergedSlider = (Slider)EditorBeatmap.SelectedHitObjects.First();
+ return slider1 is not null && mergedSlider.HeadCircle.Samples.SequenceEqual(slider1.HeadCircle.Samples)
+ && mergedSlider.TailCircle.Samples.SequenceEqual(slider1.TailCircle.Samples)
+ && mergedSlider.Samples.SequenceEqual(slider1.Samples)
+ && mergedSlider.SampleControlPoint.IsRedundant(slider1.SampleControlPoint);
+ });
+
+ AddAssert("slider end is at same completion for last slider", () =>
+ {
+ if (slider1Path is null || slider2 is null)
+ return false;
+
+ var mergedSlider = (Slider)EditorBeatmap.SelectedHitObjects.First();
+ return Precision.AlmostEquals(mergedSlider.Path.Distance, slider1Path.CalculatedDistance + slider2.Path.Distance);
+ });
+ }
+
+ [Test]
+ public void TestNonMerge()
+ {
+ HitCircle? circle1 = null;
+ HitCircle? circle2 = null;
+ Spinner? spinner = null;
+
+ AddStep("select first two circles and spinner", () =>
+ {
+ circle1 = (HitCircle)EditorBeatmap.HitObjects.First(h => h is HitCircle);
+ circle2 = (HitCircle)EditorBeatmap.HitObjects.First(h => h is HitCircle && h != circle1);
+ spinner = (Spinner)EditorBeatmap.HitObjects.First(h => h is Spinner);
+ EditorClock.Seek(spinner.StartTime);
+ EditorBeatmap.SelectedHitObjects.Add(circle1);
+ EditorBeatmap.SelectedHitObjects.Add(circle2);
+ EditorBeatmap.SelectedHitObjects.Add(spinner);
+ });
+
+ mergeSelection();
+
+ AddAssert("slider created", () => circle1 is not null && circle2 is not null && sliderCreatedFor(
+ (pos: circle1.Position, pathType: PathType.Linear),
+ (pos: circle2.Position, pathType: null)));
+
+ AddAssert("spinner not merged", () => EditorBeatmap.HitObjects.Contains(spinner));
+ }
+
+ private void mergeSelection()
+ {
+ AddStep("merge selection", () =>
+ {
+ InputManager.PressKey(Key.LControl);
+ InputManager.PressKey(Key.LShift);
+ InputManager.Key(Key.M);
+ InputManager.ReleaseKey(Key.LShift);
+ InputManager.ReleaseKey(Key.LControl);
+ });
+ }
+
+ private bool sliderCreatedFor(params (Vector2 pos, PathType? pathType)[] expectedControlPoints)
+ {
+ if (EditorBeatmap.SelectedHitObjects.Count != 1)
+ return false;
+
+ var mergedSlider = (Slider)EditorBeatmap.SelectedHitObjects.First();
+ int i = 0;
+
+ foreach ((Vector2 pos, PathType? pathType) in expectedControlPoints)
+ {
+ var controlPoint = mergedSlider.Path.ControlPoints[i++];
+
+ if (!Precision.AlmostEquals(controlPoint.Position + mergedSlider.Position, pos) || controlPoint.Type != pathType)
+ return false;
+ }
+
+ return true;
+ }
+
+ private bool objectsRestored(params HitObject[] objects)
+ {
+ foreach (var hitObject in objects)
+ {
+ if (EditorBeatmap.HitObjects.Contains(hitObject))
+ return false;
+ }
+
+ return true;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
index 4349d25cb3..36c40c0fe2 100644
--- a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
+++ b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
@@ -2,7 +2,7 @@
-
+
diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
index 3c82c2dc33..fb0eff5cb2 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
@@ -270,8 +270,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
comboBasedMissCount = fullComboThreshold / Math.Max(1.0, scoreMaxCombo);
}
- // Clamp miss count since it's derived from combo and can be higher than total hits and that breaks some calculations
- comboBasedMissCount = Math.Min(comboBasedMissCount, totalHits);
+ // Clamp miss count to maximum amount of possible breaks
+ comboBasedMissCount = Math.Min(comboBasedMissCount, countOk + countMeh + countMiss);
return Math.Max(countMiss, comboBasedMissCount);
}
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
index bd11cc826f..794551dab7 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
@@ -251,13 +251,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private void convertToStream()
{
- if (editorBeatmap == null || changeHandler == null || beatDivisor == null)
+ if (editorBeatmap == null || beatDivisor == null)
return;
var timingPoint = editorBeatmap.ControlPointInfo.TimingPointAt(HitObject.StartTime);
double streamSpacing = timingPoint.BeatLength / beatDivisor.Value;
- changeHandler.BeginChange();
+ changeHandler?.BeginChange();
int i = 0;
double time = HitObject.StartTime;
@@ -292,7 +292,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
editorBeatmap.Remove(HitObject);
- changeHandler.EndChange();
+ changeHandler?.EndChange();
}
public override MenuItem[] ContextMenuItems => new MenuItem[]
diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs
index 4a6d94f303..f3c0a05bc2 100644
--- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs
+++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs
@@ -6,8 +6,11 @@ using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Primitives;
+using osu.Framework.Graphics.UserInterface;
+using osu.Framework.Input.Events;
using osu.Framework.Utils;
using osu.Game.Extensions;
+using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
@@ -15,6 +18,7 @@ using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Screens.Edit.Compose.Components;
using osuTK;
+using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Edit
{
@@ -53,6 +57,17 @@ namespace osu.Game.Rulesets.Osu.Edit
referencePathTypes = null;
}
+ protected override bool OnKeyDown(KeyDownEvent e)
+ {
+ if (e.Key == Key.M && e.ControlPressed && e.ShiftPressed)
+ {
+ mergeSelection();
+ return true;
+ }
+
+ return false;
+ }
+
public override bool HandleMovement(MoveSelectionEvent moveEvent)
{
var hitObjects = selectedMovableObjects;
@@ -320,7 +335,109 @@ namespace osu.Game.Rulesets.Osu.Edit
/// All osu! hitobjects which can be moved/rotated/scaled.
///
private OsuHitObject[] selectedMovableObjects => SelectedItems.OfType()
- .Where(h => !(h is Spinner))
+ .Where(h => h is not Spinner)
.ToArray();
+
+ ///
+ /// All osu! hitobjects which can be merged.
+ ///
+ private OsuHitObject[] selectedMergeableObjects => SelectedItems.OfType()
+ .Where(h => h is HitCircle or Slider)
+ .OrderBy(h => h.StartTime)
+ .ToArray();
+
+ private void mergeSelection()
+ {
+ var mergeableObjects = selectedMergeableObjects;
+
+ if (mergeableObjects.Length < 2)
+ return;
+
+ ChangeHandler?.BeginChange();
+
+ // Have an initial slider object.
+ var firstHitObject = mergeableObjects[0];
+ var mergedHitObject = firstHitObject as Slider ?? new Slider
+ {
+ StartTime = firstHitObject.StartTime,
+ Position = firstHitObject.Position,
+ NewCombo = firstHitObject.NewCombo,
+ SampleControlPoint = firstHitObject.SampleControlPoint,
+ };
+
+ if (mergedHitObject.Path.ControlPoints.Count == 0)
+ {
+ mergedHitObject.Path.ControlPoints.Add(new PathControlPoint(Vector2.Zero, PathType.Linear));
+ }
+
+ // Merge all the selected hit objects into one slider path.
+ bool lastCircle = firstHitObject is HitCircle;
+
+ foreach (var selectedMergeableObject in mergeableObjects.Skip(1))
+ {
+ if (selectedMergeableObject is IHasPath hasPath)
+ {
+ var offset = lastCircle ? selectedMergeableObject.Position - mergedHitObject.Position : mergedHitObject.Path.ControlPoints[^1].Position;
+ float distanceToLastControlPoint = Vector2.Distance(mergedHitObject.Path.ControlPoints[^1].Position, offset);
+
+ // Calculate the distance required to travel to the expected distance of the merging slider.
+ mergedHitObject.Path.ExpectedDistance.Value = mergedHitObject.Path.CalculatedDistance + distanceToLastControlPoint + hasPath.Path.Distance;
+
+ // Remove the last control point if it sits exactly on the start of the next control point.
+ if (Precision.AlmostEquals(distanceToLastControlPoint, 0))
+ {
+ mergedHitObject.Path.ControlPoints.RemoveAt(mergedHitObject.Path.ControlPoints.Count - 1);
+ }
+
+ mergedHitObject.Path.ControlPoints.AddRange(hasPath.Path.ControlPoints.Select(o => new PathControlPoint(o.Position + offset, o.Type)));
+ lastCircle = false;
+ }
+ else
+ {
+ // Turn the last control point into a linear type if this is the first merging circle in a sequence, so the subsequent control points can be inherited path type.
+ if (!lastCircle)
+ {
+ mergedHitObject.Path.ControlPoints.Last().Type = PathType.Linear;
+ }
+
+ mergedHitObject.Path.ControlPoints.Add(new PathControlPoint(selectedMergeableObject.Position - mergedHitObject.Position));
+ mergedHitObject.Path.ExpectedDistance.Value = null;
+ lastCircle = true;
+ }
+ }
+
+ // Make sure only the merged hit object is in the beatmap.
+ if (firstHitObject is Slider)
+ {
+ foreach (var selectedMergeableObject in mergeableObjects.Skip(1))
+ {
+ EditorBeatmap.Remove(selectedMergeableObject);
+ }
+ }
+ else
+ {
+ foreach (var selectedMergeableObject in mergeableObjects)
+ {
+ EditorBeatmap.Remove(selectedMergeableObject);
+ }
+
+ EditorBeatmap.Add(mergedHitObject);
+ }
+
+ // Make sure the merged hitobject is selected.
+ SelectedItems.Clear();
+ SelectedItems.Add(mergedHitObject);
+
+ ChangeHandler?.EndChange();
+ }
+
+ protected override IEnumerable
WinExe
diff --git a/osu.Game/Beatmaps/IWorkingBeatmap.cs b/osu.Game/Beatmaps/IWorkingBeatmap.cs
index 548341cc77..a39766abe1 100644
--- a/osu.Game/Beatmaps/IWorkingBeatmap.cs
+++ b/osu.Game/Beatmaps/IWorkingBeatmap.cs
@@ -134,6 +134,6 @@ namespace osu.Game.Beatmaps
///
/// Reads the correct track restart point from beatmap metadata and sets looping to enabled.
///
- void PrepareTrackForPreviewLooping();
+ void PrepareTrackForPreview(bool looping);
}
}
diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs
index 16464932e0..301610ee58 100644
--- a/osu.Game/Beatmaps/WorkingBeatmap.cs
+++ b/osu.Game/Beatmaps/WorkingBeatmap.cs
@@ -110,9 +110,9 @@ namespace osu.Game.Beatmaps
public Track LoadTrack() => track = GetBeatmapTrack() ?? GetVirtualTrack(1000);
- public void PrepareTrackForPreviewLooping()
+ public void PrepareTrackForPreview(bool looping)
{
- Track.Looping = true;
+ Track.Looping = looping;
Track.RestartPoint = Metadata.PreviewTime;
if (Track.RestartPoint == -1)
diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs
index cdaf35a1fb..0f2e724567 100644
--- a/osu.Game/Database/RealmAccess.cs
+++ b/osu.Game/Database/RealmAccess.cs
@@ -901,8 +901,15 @@ namespace osu.Game.Database
try
{
using (var source = storage.GetStream(Filename, mode: FileMode.Open))
- using (var destination = storage.GetStream(backupFilename, FileAccess.Write, FileMode.CreateNew))
- source.CopyTo(destination);
+ {
+ // source may not exist.
+ if (source == null)
+ return;
+
+ using (var destination = storage.GetStream(backupFilename, FileAccess.Write, FileMode.CreateNew))
+ source.CopyTo(destination);
+ }
+
return;
}
catch (IOException)
diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs
index 89f9aec5ee..a7cbe1f1ad 100644
--- a/osu.Game/Screens/Edit/Editor.cs
+++ b/osu.Game/Screens/Edit/Editor.cs
@@ -24,6 +24,7 @@ using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Framework.Screens;
using osu.Framework.Testing;
+using osu.Framework.Threading;
using osu.Framework.Timing;
using osu.Game.Audio;
using osu.Game.Beatmaps;
@@ -233,6 +234,8 @@ namespace osu.Game.Screens.Edit
AddInternal(editorBeatmap = new EditorBeatmap(playableBeatmap, loadableBeatmap.GetSkin(), loadableBeatmap.BeatmapInfo));
dependencies.CacheAs(editorBeatmap);
+ editorBeatmap.UpdateInProgress.BindValueChanged(updateInProgress);
+
canSave = editorBeatmap.BeatmapInfo.Ruleset.CreateInstance() is ILegacyRuleset;
if (canSave)
@@ -714,6 +717,27 @@ namespace osu.Game.Screens.Edit
this.Exit();
}
+ #region Mute from update application
+
+ private ScheduledDelegate temporaryMuteRestorationDelegate;
+ private bool temporaryMuteFromUpdateInProgress;
+
+ private void updateInProgress(ValueChangedEvent obj)
+ {
+ temporaryMuteFromUpdateInProgress = true;
+ updateSampleDisabledState();
+
+ // Debounce is arbitrarily high enough to avoid flip-flopping the value each other frame.
+ temporaryMuteRestorationDelegate?.Cancel();
+ temporaryMuteRestorationDelegate = Scheduler.AddDelayed(() =>
+ {
+ temporaryMuteFromUpdateInProgress = false;
+ updateSampleDisabledState();
+ }, 50);
+ }
+
+ #endregion
+
#region Clipboard support
private EditorMenuItem cutMenuItem;
@@ -829,7 +853,9 @@ namespace osu.Game.Screens.Edit
private void updateSampleDisabledState()
{
- samplePlaybackDisabled.Value = clock.SeekingOrStopped.Value || !(currentScreen is ComposeScreen);
+ samplePlaybackDisabled.Value = clock.SeekingOrStopped.Value
+ || currentScreen is not ComposeScreen
+ || temporaryMuteFromUpdateInProgress;
}
private void seek(UIEvent e, int direction)
diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs
index afbb5cc018..8aa754b305 100644
--- a/osu.Game/Screens/Edit/EditorBeatmap.cs
+++ b/osu.Game/Screens/Edit/EditorBeatmap.cs
@@ -22,6 +22,17 @@ namespace osu.Game.Screens.Edit
{
public class EditorBeatmap : TransactionalCommitComponent, IBeatmap, IBeatSnapProvider
{
+ ///
+ /// Will become true when a new update is queued, and false when all updates have been applied.
+ ///
+ ///
+ /// This is intended to be used to avoid performing operations (like playback of samples)
+ /// while mutating hitobjects.
+ ///
+ public IBindable UpdateInProgress => updateInProgress;
+
+ private readonly BindableBool updateInProgress = new BindableBool();
+
///
/// Invoked when a is added to this .
///
@@ -228,6 +239,8 @@ namespace osu.Game.Screens.Edit
{
// updates are debounced regardless of whether a batch is active.
batchPendingUpdates.Add(hitObject);
+
+ updateInProgress.Value = true;
}
///
@@ -237,6 +250,8 @@ namespace osu.Game.Screens.Edit
{
foreach (var h in HitObjects)
batchPendingUpdates.Add(h);
+
+ updateInProgress.Value = true;
}
///
@@ -329,6 +344,8 @@ namespace osu.Game.Screens.Edit
foreach (var h in deletes) HitObjectRemoved?.Invoke(h);
foreach (var h in inserts) HitObjectAdded?.Invoke(h);
foreach (var h in updates) HitObjectUpdated?.Invoke(h);
+
+ updateInProgress.Value = false;
}
///
diff --git a/osu.Game/Screens/Menu/IntroCircles.cs b/osu.Game/Screens/Menu/IntroCircles.cs
index 5f481ed00e..7a4bdb231f 100644
--- a/osu.Game/Screens/Menu/IntroCircles.cs
+++ b/osu.Game/Screens/Menu/IntroCircles.cs
@@ -19,11 +19,9 @@ namespace osu.Game.Screens.Menu
protected override string BeatmapFile => "circles.osz";
- public const double TRACK_START_DELAY_NON_THEMED = 1000;
- private const double track_start_delay_themed = 600;
+ public const double TRACK_START_DELAY = 600;
private const double delay_for_menu = 2900;
- private const double delay_step_one = 2300;
private Sample welcome;
@@ -47,16 +45,14 @@ namespace osu.Game.Screens.Menu
{
welcome?.Play();
- double trackStartDelay = UsingThemedIntro ? track_start_delay_themed : TRACK_START_DELAY_NON_THEMED;
-
Scheduler.AddDelayed(delegate
{
StartTrack();
PrepareMenuLoad();
- Scheduler.AddDelayed(LoadMenu, delay_for_menu - trackStartDelay);
- }, trackStartDelay);
+ Scheduler.AddDelayed(LoadMenu, delay_for_menu - TRACK_START_DELAY);
+ }, TRACK_START_DELAY);
logo.ScaleTo(1);
logo.FadeIn();
diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs
index bf713997f7..409c7d6c8d 100644
--- a/osu.Game/Screens/Menu/IntroScreen.cs
+++ b/osu.Game/Screens/Menu/IntroScreen.cs
@@ -276,12 +276,17 @@ namespace osu.Game.Screens.Menu
{
var drawableTrack = musicController.CurrentTrack;
- drawableTrack.Start();
-
if (!UsingThemedIntro)
{
- drawableTrack.VolumeTo(0).Then()
- .VolumeTo(1, 2000, Easing.OutQuint);
+ initialBeatmap?.PrepareTrackForPreview(false);
+
+ drawableTrack.VolumeTo(0);
+ drawableTrack.Restart();
+ drawableTrack.VolumeTo(1, 2200, Easing.InCubic);
+ }
+ else
+ {
+ drawableTrack.Restart();
}
}
diff --git a/osu.Game/Screens/Menu/IntroTriangles.cs b/osu.Game/Screens/Menu/IntroTriangles.cs
index 4ec79b852a..d777f78df2 100644
--- a/osu.Game/Screens/Menu/IntroTriangles.cs
+++ b/osu.Game/Screens/Menu/IntroTriangles.cs
@@ -88,7 +88,7 @@ namespace osu.Game.Screens.Menu
// If the user has requested no theme, fallback to the same intro voice and delay as IntroCircles.
// The triangles intro voice and theme are combined which makes it impossible to use.
welcome?.Play();
- Scheduler.AddDelayed(StartTrack, IntroCircles.TRACK_START_DELAY_NON_THEMED);
+ Scheduler.AddDelayed(StartTrack, IntroCircles.TRACK_START_DELAY);
}
else
StartTrack();
diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs
index 066a37055c..0071ada05a 100644
--- a/osu.Game/Screens/Menu/MainMenu.cs
+++ b/osu.Game/Screens/Menu/MainMenu.cs
@@ -192,7 +192,7 @@ namespace osu.Game.Screens.Menu
// presume the track is the current beatmap's track. not sure how correct this assumption is but it has worked until now.
if (!track.IsRunning)
{
- Beatmap.Value.PrepareTrackForPreviewLooping();
+ Beatmap.Value.PrepareTrackForPreview(false);
track.Restart();
}
}
diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs
index 25f2a94a3c..03216180fb 100644
--- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs
@@ -485,7 +485,7 @@ namespace osu.Game.Screens.OnlinePlay.Match
if (track != null)
{
- Beatmap.Value.PrepareTrackForPreviewLooping();
+ Beatmap.Value.PrepareTrackForPreview(true);
music?.EnsurePlayingSomething();
}
}
diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs
index 33ff31857f..0c2ca6d4af 100644
--- a/osu.Game/Screens/Select/SongSelect.cs
+++ b/osu.Game/Screens/Select/SongSelect.cs
@@ -683,7 +683,7 @@ namespace osu.Game.Screens.Select
}
private void ensureTrackLooping(IWorkingBeatmap beatmap, TrackChangeDirection changeDirection)
- => beatmap.PrepareTrackForPreviewLooping();
+ => beatmap.PrepareTrackForPreview(true);
public override bool OnBackButton()
{
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index c17d45e84a..d67f8415e7 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -23,9 +23,9 @@
-
-
-
+
+
+
@@ -35,13 +35,13 @@
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
+
+
-
-
+
+
-
+
diff --git a/osu.iOS.props b/osu.iOS.props
index 5bfb53bc9d..463af1143f 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -61,7 +61,7 @@
-
+
@@ -84,11 +84,11 @@
-
+
-
+