diff --git a/osu.Android.props b/osu.Android.props
index 752eb160ed..f7b7b6fb23 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -51,7 +51,7 @@
-
+
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs
index db42667033..160af47a6d 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs
@@ -10,9 +10,13 @@ using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
+using osu.Game.Screens.Backgrounds;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Components.Timelines.Summary;
+using osu.Game.Screens.Edit.GameplayTest;
+using osu.Game.Screens.Play;
using osu.Game.Tests.Beatmaps.IO;
+using osuTK.Graphics;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Editing
@@ -58,6 +62,42 @@ namespace osu.Game.Tests.Visual.Editing
AddUntilStep("player pushed", () => (editorPlayer = Stack.CurrentScreen as EditorPlayer) != null);
AddStep("exit player", () => editorPlayer.Exit());
AddUntilStep("current screen is editor", () => Stack.CurrentScreen is Editor);
+ AddUntilStep("background has correct params", () =>
+ {
+ var background = this.ChildrenOfType().Single();
+ return background.Colour == Color4.DarkGray && background.BlurAmount.Value == 0;
+ });
+ }
+
+ [Test]
+ public void TestGameplayTestWhenTrackRunning()
+ {
+ AddStep("start track", () => EditorClock.Start());
+ AddAssert("sample playback enabled", () => !Editor.SamplePlaybackDisabled.Value);
+
+ AddStep("click test gameplay button", () =>
+ {
+ var button = Editor.ChildrenOfType().Single();
+
+ InputManager.MoveMouseTo(button);
+ InputManager.Click(MouseButton.Left);
+ });
+
+ EditorPlayer editorPlayer = null;
+ AddUntilStep("player pushed", () => (editorPlayer = Stack.CurrentScreen as EditorPlayer) != null);
+ AddAssert("editor track stopped", () => !EditorClock.IsRunning);
+ AddAssert("sample playback disabled", () => Editor.SamplePlaybackDisabled.Value);
+
+ AddStep("exit player", () => editorPlayer.Exit());
+ AddUntilStep("current screen is editor", () => Stack.CurrentScreen is Editor);
+ AddUntilStep("background has correct params", () =>
+ {
+ var background = this.ChildrenOfType().Single();
+ return background.Colour == Color4.DarkGray && background.BlurAmount.Value == 0;
+ });
+
+ AddStep("start track", () => EditorClock.Start());
+ AddAssert("sample playback re-enabled", () => !Editor.SamplePlaybackDisabled.Value);
}
[Test]
@@ -111,6 +151,35 @@ namespace osu.Game.Tests.Visual.Editing
AddAssert("track stopped", () => !Beatmap.Value.Track.IsRunning);
}
+ [Test]
+ public void TestSharedClockState()
+ {
+ AddStep("seek to 00:01:00", () => EditorClock.Seek(60_000));
+ AddStep("click test gameplay button", () =>
+ {
+ var button = Editor.ChildrenOfType().Single();
+
+ InputManager.MoveMouseTo(button);
+ InputManager.Click(MouseButton.Left);
+ });
+
+ EditorPlayer editorPlayer = null;
+ AddUntilStep("player pushed", () => (editorPlayer = Stack.CurrentScreen as EditorPlayer) != null);
+
+ GameplayClockContainer gameplayClockContainer = null;
+ AddStep("fetch gameplay clock", () => gameplayClockContainer = editorPlayer.ChildrenOfType().First());
+ AddUntilStep("gameplay clock running", () => gameplayClockContainer.IsRunning);
+ AddAssert("gameplay time past 00:01:00", () => gameplayClockContainer.CurrentTime >= 60_000);
+
+ double timeAtPlayerExit = 0;
+ AddWaitStep("wait some", 5);
+ AddStep("store time before exit", () => timeAtPlayerExit = gameplayClockContainer.CurrentTime);
+
+ AddStep("exit player", () => editorPlayer.Exit());
+ AddUntilStep("current screen is editor", () => Stack.CurrentScreen is Editor);
+ AddAssert("time is past player exit", () => EditorClock.CurrentTime >= timeAtPlayerExit);
+ }
+
public override void TearDownSteps()
{
base.TearDownSteps();
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectDifficultyPointAdjustments.cs b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectDifficultyPointAdjustments.cs
new file mode 100644
index 0000000000..4012a672ed
--- /dev/null
+++ b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectDifficultyPointAdjustments.cs
@@ -0,0 +1,171 @@
+// 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 Humanizer;
+using NUnit.Framework;
+using osu.Framework.Testing;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.UI;
+using osu.Game.Screens.Edit.Compose.Components.Timeline;
+using osu.Game.Screens.Edit.Timing;
+using osu.Game.Tests.Beatmaps;
+using osuTK;
+using osuTK.Input;
+
+namespace osu.Game.Tests.Visual.Editing
+{
+ public class TestSceneHitObjectDifficultyPointAdjustments : EditorTestScene
+ {
+ protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
+
+ protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false);
+
+ public override void SetUpSteps()
+ {
+ base.SetUpSteps();
+
+ AddStep("add test objects", () =>
+ {
+ EditorBeatmap.Add(new Slider
+ {
+ StartTime = 0,
+ Position = (OsuPlayfield.BASE_SIZE - new Vector2(0, 100)) / 2,
+ Path = new SliderPath
+ {
+ ControlPoints =
+ {
+ new PathControlPoint(new Vector2(0, 0)),
+ new PathControlPoint(new Vector2(0, 100))
+ }
+ }
+ });
+
+ EditorBeatmap.Add(new Slider
+ {
+ StartTime = 500,
+ Position = (OsuPlayfield.BASE_SIZE - new Vector2(100, 0)) / 2,
+ Path = new SliderPath
+ {
+ ControlPoints =
+ {
+ new PathControlPoint(new Vector2(0, 0)),
+ new PathControlPoint(new Vector2(100, 0))
+ }
+ },
+ DifficultyControlPoint = new DifficultyControlPoint
+ {
+ SliderVelocity = 2
+ }
+ });
+ });
+ }
+
+ [Test]
+ public void TestSingleSelection()
+ {
+ clickDifficultyPiece(0);
+ velocityPopoverHasSingleValue(1);
+
+ dismissPopover();
+
+ // select first object to ensure that difficulty pieces for unselected objects
+ // work independently from selection state.
+ AddStep("select first object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.First()));
+
+ clickDifficultyPiece(1);
+ velocityPopoverHasSingleValue(2);
+
+ setVelocityViaPopover(5);
+ hitObjectHasVelocity(1, 5);
+ }
+
+ [Test]
+ public void TestMultipleSelectionWithSameSliderVelocity()
+ {
+ AddStep("unify slider velocity", () =>
+ {
+ foreach (var h in EditorBeatmap.HitObjects)
+ h.DifficultyControlPoint.SliderVelocity = 1.5;
+ });
+
+ AddStep("select both objects", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
+ clickDifficultyPiece(0);
+ velocityPopoverHasSingleValue(1.5);
+
+ dismissPopover();
+
+ clickDifficultyPiece(1);
+ velocityPopoverHasSingleValue(1.5);
+
+ setVelocityViaPopover(5);
+ hitObjectHasVelocity(0, 5);
+ hitObjectHasVelocity(1, 5);
+ }
+
+ [Test]
+ public void TestMultipleSelectionWithDifferentSliderVelocity()
+ {
+ AddStep("select both objects", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
+ clickDifficultyPiece(0);
+ velocityPopoverHasIndeterminateValue();
+
+ dismissPopover();
+
+ clickDifficultyPiece(1);
+ velocityPopoverHasIndeterminateValue();
+
+ setVelocityViaPopover(3);
+ hitObjectHasVelocity(0, 3);
+ hitObjectHasVelocity(1, 3);
+ }
+
+ private void clickDifficultyPiece(int objectIndex) => AddStep($"click {objectIndex.ToOrdinalWords()} difficulty piece", () =>
+ {
+ var difficultyPiece = this.ChildrenOfType().Single(piece => piece.HitObject == EditorBeatmap.HitObjects.ElementAt(objectIndex));
+
+ InputManager.MoveMouseTo(difficultyPiece);
+ InputManager.Click(MouseButton.Left);
+ });
+
+ private void velocityPopoverHasSingleValue(double velocity) => AddUntilStep($"velocity popover has {velocity}", () =>
+ {
+ var popover = this.ChildrenOfType().SingleOrDefault();
+ var slider = popover?.ChildrenOfType>().Single();
+
+ return slider?.Current.Value == velocity;
+ });
+
+ private void velocityPopoverHasIndeterminateValue() => AddUntilStep("velocity popover has indeterminate value", () =>
+ {
+ var popover = this.ChildrenOfType().SingleOrDefault();
+ var slider = popover?.ChildrenOfType>().Single();
+
+ return slider != null && slider.Current.Value == null;
+ });
+
+ private void dismissPopover()
+ {
+ AddStep("dismiss popover", () => InputManager.Key(Key.Escape));
+ AddUntilStep("wait for dismiss", () => !this.ChildrenOfType().Any(popover => popover.IsPresent));
+ }
+
+ private void setVelocityViaPopover(double velocity) => AddStep($"set {velocity} via popover", () =>
+ {
+ var popover = this.ChildrenOfType().Single();
+ var slider = popover.ChildrenOfType>().Single();
+ slider.Current.Value = velocity;
+ });
+
+ private void hitObjectHasVelocity(int objectIndex, double velocity) => AddAssert($"{objectIndex.ToOrdinalWords()} has velocity {velocity}", () =>
+ {
+ var h = EditorBeatmap.HitObjects.ElementAt(objectIndex);
+ return h.DifficultyControlPoint.SliderVelocity == velocity;
+ });
+ }
+}
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSamplePointAdjustments.cs b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSamplePointAdjustments.cs
new file mode 100644
index 0000000000..dca30a6fc0
--- /dev/null
+++ b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSamplePointAdjustments.cs
@@ -0,0 +1,252 @@
+// 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 Humanizer;
+using NUnit.Framework;
+using osu.Framework.Testing;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Graphics.UserInterfaceV2;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.UI;
+using osu.Game.Screens.Edit.Compose.Components.Timeline;
+using osu.Game.Screens.Edit.Timing;
+using osu.Game.Tests.Beatmaps;
+using osuTK;
+using osuTK.Input;
+
+namespace osu.Game.Tests.Visual.Editing
+{
+ public class TestSceneHitObjectSamplePointAdjustments : EditorTestScene
+ {
+ protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
+
+ protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false);
+
+ public override void SetUpSteps()
+ {
+ base.SetUpSteps();
+
+ AddStep("add test objects", () =>
+ {
+ EditorBeatmap.Add(new HitCircle
+ {
+ StartTime = 0,
+ Position = (OsuPlayfield.BASE_SIZE - new Vector2(100, 0)) / 2,
+ SampleControlPoint = new SampleControlPoint
+ {
+ SampleBank = "normal",
+ SampleVolume = 80
+ }
+ });
+
+ EditorBeatmap.Add(new HitCircle
+ {
+ StartTime = 500,
+ Position = (OsuPlayfield.BASE_SIZE + new Vector2(100, 0)) / 2,
+ SampleControlPoint = new SampleControlPoint
+ {
+ SampleBank = "soft",
+ SampleVolume = 60
+ }
+ });
+ });
+ }
+
+ [Test]
+ public void TestSingleSelection()
+ {
+ clickSamplePiece(0);
+ samplePopoverHasSingleBank("normal");
+ samplePopoverHasSingleVolume(80);
+
+ dismissPopover();
+
+ // select first object to ensure that sample pieces for unselected objects
+ // work independently from selection state.
+ AddStep("select first object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.First()));
+
+ clickSamplePiece(1);
+ samplePopoverHasSingleBank("soft");
+ samplePopoverHasSingleVolume(60);
+
+ setVolumeViaPopover(90);
+ hitObjectHasSampleVolume(1, 90);
+
+ setBankViaPopover("drum");
+ hitObjectHasSampleBank(1, "drum");
+ }
+
+ [Test]
+ public void TestMultipleSelectionWithSameSampleVolume()
+ {
+ AddStep("unify sample volume", () =>
+ {
+ foreach (var h in EditorBeatmap.HitObjects)
+ h.SampleControlPoint.SampleVolume = 50;
+ });
+
+ AddStep("select both objects", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
+ clickSamplePiece(0);
+ samplePopoverHasSingleVolume(50);
+
+ dismissPopover();
+
+ clickSamplePiece(1);
+ samplePopoverHasSingleVolume(50);
+
+ setVolumeViaPopover(75);
+ hitObjectHasSampleVolume(0, 75);
+ hitObjectHasSampleVolume(1, 75);
+ }
+
+ [Test]
+ public void TestMultipleSelectionWithDifferentSampleVolume()
+ {
+ AddStep("select both objects", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
+ clickSamplePiece(0);
+ samplePopoverHasIndeterminateVolume();
+
+ dismissPopover();
+
+ clickSamplePiece(1);
+ samplePopoverHasIndeterminateVolume();
+
+ setVolumeViaPopover(30);
+ hitObjectHasSampleVolume(0, 30);
+ hitObjectHasSampleVolume(1, 30);
+ }
+
+ [Test]
+ public void TestMultipleSelectionWithSameSampleBank()
+ {
+ AddStep("unify sample bank", () =>
+ {
+ foreach (var h in EditorBeatmap.HitObjects)
+ h.SampleControlPoint.SampleBank = "soft";
+ });
+
+ AddStep("select both objects", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
+ clickSamplePiece(0);
+ samplePopoverHasSingleBank("soft");
+
+ dismissPopover();
+
+ clickSamplePiece(1);
+ samplePopoverHasSingleBank("soft");
+
+ setBankViaPopover(string.Empty);
+ hitObjectHasSampleBank(0, "soft");
+ hitObjectHasSampleBank(1, "soft");
+ samplePopoverHasSingleBank("soft");
+
+ setBankViaPopover("drum");
+ hitObjectHasSampleBank(0, "drum");
+ hitObjectHasSampleBank(1, "drum");
+ samplePopoverHasSingleBank("drum");
+ }
+
+ [Test]
+ public void TestMultipleSelectionWithDifferentSampleBank()
+ {
+ AddStep("select both objects", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
+ clickSamplePiece(0);
+ samplePopoverHasIndeterminateBank();
+
+ dismissPopover();
+
+ clickSamplePiece(1);
+ samplePopoverHasIndeterminateBank();
+
+ setBankViaPopover(string.Empty);
+ hitObjectHasSampleBank(0, "normal");
+ hitObjectHasSampleBank(1, "soft");
+ samplePopoverHasIndeterminateBank();
+
+ setBankViaPopover("normal");
+ hitObjectHasSampleBank(0, "normal");
+ hitObjectHasSampleBank(1, "normal");
+ samplePopoverHasSingleBank("normal");
+ }
+
+ private void clickSamplePiece(int objectIndex) => AddStep($"click {objectIndex.ToOrdinalWords()} difficulty piece", () =>
+ {
+ var difficultyPiece = this.ChildrenOfType().Single(piece => piece.HitObject == EditorBeatmap.HitObjects.ElementAt(objectIndex));
+
+ InputManager.MoveMouseTo(difficultyPiece);
+ InputManager.Click(MouseButton.Left);
+ });
+
+ private void samplePopoverHasSingleVolume(int volume) => AddUntilStep($"sample popover has volume {volume}", () =>
+ {
+ var popover = this.ChildrenOfType().SingleOrDefault();
+ var slider = popover?.ChildrenOfType>().Single();
+
+ return slider?.Current.Value == volume;
+ });
+
+ private void samplePopoverHasIndeterminateVolume() => AddUntilStep("sample popover has indeterminate volume", () =>
+ {
+ var popover = this.ChildrenOfType().SingleOrDefault();
+ var slider = popover?.ChildrenOfType>().Single();
+
+ return slider != null && slider.Current.Value == null;
+ });
+
+ private void samplePopoverHasSingleBank(string bank) => AddUntilStep($"sample popover has bank {bank}", () =>
+ {
+ var popover = this.ChildrenOfType().SingleOrDefault();
+ var textBox = popover?.ChildrenOfType().First();
+
+ return textBox?.Current.Value == bank && string.IsNullOrEmpty(textBox?.PlaceholderText.ToString());
+ });
+
+ private void samplePopoverHasIndeterminateBank() => AddUntilStep("sample popover has indeterminate bank", () =>
+ {
+ var popover = this.ChildrenOfType().SingleOrDefault();
+ var textBox = popover?.ChildrenOfType().First();
+
+ return textBox != null && string.IsNullOrEmpty(textBox.Current.Value) && !string.IsNullOrEmpty(textBox.PlaceholderText.ToString());
+ });
+
+ private void dismissPopover()
+ {
+ AddStep("dismiss popover", () => InputManager.Key(Key.Escape));
+ AddUntilStep("wait for dismiss", () => !this.ChildrenOfType().Any(popover => popover.IsPresent));
+ }
+
+ private void setVolumeViaPopover(int volume) => AddStep($"set volume {volume} via popover", () =>
+ {
+ var popover = this.ChildrenOfType().Single();
+ var slider = popover.ChildrenOfType>().Single();
+ slider.Current.Value = volume;
+ });
+
+ private void hitObjectHasSampleVolume(int objectIndex, int volume) => AddAssert($"{objectIndex.ToOrdinalWords()} has volume {volume}", () =>
+ {
+ var h = EditorBeatmap.HitObjects.ElementAt(objectIndex);
+ return h.SampleControlPoint.SampleVolume == volume;
+ });
+
+ private void setBankViaPopover(string bank) => AddStep($"set bank {bank} via popover", () =>
+ {
+ var popover = this.ChildrenOfType().Single();
+ var textBox = popover.ChildrenOfType().First();
+ textBox.Current.Value = bank;
+ // force a commit via keyboard.
+ // this is needed when testing attempting to set empty bank - which should revert to the previous value, but only on commit.
+ InputManager.ChangeFocus(textBox);
+ InputManager.Key(Key.Enter);
+ });
+
+ private void hitObjectHasSampleBank(int objectIndex, string bank) => AddAssert($"{objectIndex.ToOrdinalWords()} has bank {bank}", () =>
+ {
+ var h = EditorBeatmap.HitObjects.ElementAt(objectIndex);
+ return h.SampleControlPoint.SampleBank == bank;
+ });
+ }
+}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs
index 36fc6812bd..7167d3120a 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs
@@ -31,12 +31,18 @@ namespace osu.Game.Tests.Visual.Gameplay
{
AddUntilStep("wait for fail", () => Player.HasFailed);
AddUntilStep("wait for fail overlay", () => ((FailPlayer)Player).FailOverlay.State.Value == Visibility.Visible);
+
+ // The pause screen and fail animation both ramp frequency.
+ // This tests to ensure that it doesn't reset during that handoff.
+ AddAssert("frequency only ever decreased", () => !((FailPlayer)Player).FrequencyIncreased);
}
private class FailPlayer : TestPlayer
{
public new FailOverlay FailOverlay => base.FailOverlay;
+ public bool FrequencyIncreased { get; private set; }
+
public FailPlayer()
: base(false, false)
{
@@ -47,6 +53,19 @@ namespace osu.Game.Tests.Visual.Gameplay
base.LoadComplete();
HealthProcessor.FailConditions += (_, __) => true;
}
+
+ private double lastFrequency = double.MaxValue;
+
+ protected override void Update()
+ {
+ base.Update();
+
+ double freq = Beatmap.Value.Track.AggregateFrequency.Value;
+
+ FrequencyIncreased |= freq > lastFrequency;
+
+ lastFrequency = freq;
+ }
}
}
}
diff --git a/osu.Game/Beatmaps/IWorkingBeatmap.cs b/osu.Game/Beatmaps/IWorkingBeatmap.cs
index a916b37b85..ba887edf62 100644
--- a/osu.Game/Beatmaps/IWorkingBeatmap.cs
+++ b/osu.Game/Beatmaps/IWorkingBeatmap.cs
@@ -17,33 +17,69 @@ namespace osu.Game.Beatmaps
{
public interface IWorkingBeatmap
{
+ IBeatmapInfo BeatmapInfo { get; }
+
+ IBeatmapSetInfo BeatmapSetInfo { get; }
+
+ IBeatmapMetadataInfo Metadata { get; }
+
///
- /// Retrieves the which this represents.
+ /// Whether the Beatmap has finished loading.
+ ///
+ public bool BeatmapLoaded { get; }
+
+ ///
+ /// Whether the Background has finished loading.
+ ///
+ public bool BackgroundLoaded { get; }
+
+ ///
+ /// Whether the Waveform has finished loading.
+ ///
+ public bool WaveformLoaded { get; }
+
+ ///
+ /// Whether the Storyboard has finished loading.
+ ///
+ public bool StoryboardLoaded { get; }
+
+ ///
+ /// Whether the Skin has finished loading.
+ ///
+ public bool SkinLoaded { get; }
+
+ ///
+ /// Whether the Track has finished loading.
+ ///
+ public bool TrackLoaded { get; }
+
+ ///
+ /// Retrieves the which this represents.
///
IBeatmap Beatmap { get; }
///
- /// Retrieves the background for this .
+ /// Retrieves the background for this .
///
Texture Background { get; }
///
- /// Retrieves the for the of this .
+ /// Retrieves the for the of this .
///
Waveform Waveform { get; }
///
- /// Retrieves the which this provides.
+ /// Retrieves the which this provides.
///
Storyboard Storyboard { get; }
///
- /// Retrieves the which this provides.
+ /// Retrieves the which this provides.
///
ISkin Skin { get; }
///
- /// Retrieves the which this has loaded.
+ /// Retrieves the which this has loaded.
///
Track Track { get; }
@@ -67,7 +103,7 @@ namespace osu.Game.Beatmaps
///
///
/// In a standard game context, the loading of the track is managed solely by MusicController, which will
- /// automatically load the track of the current global IBindable WorkingBeatmap.
+ /// automatically load the track of the current global IBindable IWorkingBeatmap.
/// As such, this method should only be called in very special scenarios, such as external tests or apps which are
/// outside of the game context.
///
@@ -79,5 +115,20 @@ namespace osu.Game.Beatmaps
///
/// The storage path to the file.
Stream GetStream(string storagePath);
+
+ ///
+ /// Beings loading the contents of this asynchronously.
+ ///
+ public void BeginAsyncLoad();
+
+ ///
+ /// Cancels the asynchronous loading of the contents of this .
+ ///
+ public void CancelAsyncLoad();
+
+ ///
+ /// Reads the correct track restart point from beatmap metadata and sets looping to enabled.
+ ///
+ void PrepareTrackForPreviewLooping();
}
}
diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs
index d2c0f7de0f..f68f34f673 100644
--- a/osu.Game/Beatmaps/WorkingBeatmap.cs
+++ b/osu.Game/Beatmaps/WorkingBeatmap.cs
@@ -27,9 +27,7 @@ namespace osu.Game.Beatmaps
public abstract class WorkingBeatmap : IWorkingBeatmap
{
public readonly BeatmapInfo BeatmapInfo;
-
public readonly BeatmapSetInfo BeatmapSetInfo;
-
public readonly BeatmapMetadata Metadata;
protected AudioManager AudioManager { get; }
@@ -89,6 +87,9 @@ namespace osu.Game.Beatmaps
var rulesetInstance = ruleset.CreateInstance();
+ if (rulesetInstance == null)
+ throw new RulesetLoadException("Creating ruleset instance failed when attempting to create playable beatmap.");
+
IBeatmapConverter converter = CreateBeatmapConverter(Beatmap, rulesetInstance);
// Check if the beatmap can be converted
@@ -176,17 +177,8 @@ namespace osu.Game.Beatmaps
private CancellationTokenSource loadCancellation = new CancellationTokenSource();
- ///
- /// Beings loading the contents of this asynchronously.
- ///
- public void BeginAsyncLoad()
- {
- loadBeatmapAsync();
- }
+ public void BeginAsyncLoad() => loadBeatmapAsync();
- ///
- /// Cancels the asynchronous loading of the contents of this .
- ///
public void CancelAsyncLoad()
{
lock (beatmapFetchLock)
@@ -234,6 +226,10 @@ namespace osu.Game.Beatmaps
public virtual bool BeatmapLoaded => beatmapLoadTask?.IsCompleted ?? false;
+ IBeatmapInfo IWorkingBeatmap.BeatmapInfo => BeatmapInfo;
+ IBeatmapMetadataInfo IWorkingBeatmap.Metadata => Metadata;
+ IBeatmapSetInfo IWorkingBeatmap.BeatmapSetInfo => BeatmapSetInfo;
+
public IBeatmap Beatmap
{
get
@@ -273,9 +269,6 @@ namespace osu.Game.Beatmaps
[NotNull]
public Track LoadTrack() => loadedTrack = GetBeatmapTrack() ?? GetVirtualTrack(1000);
- ///
- /// Reads the correct track restart point from beatmap metadata and sets looping to enabled.
- ///
public void PrepareTrackForPreviewLooping()
{
Track.Looping = true;
diff --git a/osu.Game/Database/ImportTask.cs b/osu.Game/Database/ImportTask.cs
index 1433a567a9..1fb5a42630 100644
--- a/osu.Game/Database/ImportTask.cs
+++ b/osu.Game/Database/ImportTask.cs
@@ -47,10 +47,30 @@ namespace osu.Game.Database
///
public ArchiveReader GetReader()
{
- if (Stream != null)
- return new ZipArchiveReader(Stream, Path);
+ return Stream != null
+ ? getReaderFrom(Stream)
+ : getReaderFrom(Path);
+ }
- return getReaderFrom(Path);
+ ///
+ /// Creates an from a stream.
+ ///
+ /// A seekable stream containing the archive content.
+ /// A reader giving access to the archive's content.
+ private ArchiveReader getReaderFrom(Stream stream)
+ {
+ if (!(stream is MemoryStream memoryStream))
+ {
+ // This isn't used in any current path. May need to reconsider for performance reasons (ie. if we don't expect the incoming stream to be copied out).
+ byte[] buffer = new byte[stream.Length];
+ stream.Read(buffer, 0, (int)stream.Length);
+ memoryStream = new MemoryStream(buffer);
+ }
+
+ if (ZipUtils.IsZipArchive(memoryStream))
+ return new ZipArchiveReader(memoryStream, Path);
+
+ return new LegacyByteArrayReader(memoryStream.ToArray(), Path);
}
///
diff --git a/osu.Game/Graphics/OsuColour.cs b/osu.Game/Graphics/OsuColour.cs
index 3aa4dbf1d8..d3afb21933 100644
--- a/osu.Game/Graphics/OsuColour.cs
+++ b/osu.Game/Graphics/OsuColour.cs
@@ -52,15 +52,18 @@ namespace osu.Game.Graphics
public Color4 ForStarDifficulty(double starDifficulty) => ColourUtils.SampleFromLinearGradient(new[]
{
- (1.5f, Color4Extensions.FromHex("4fc0ff")),
+ (0.1f, Color4Extensions.FromHex("aaaaaa")),
+ (0.1f, Color4Extensions.FromHex("4290fb")),
+ (1.25f, Color4Extensions.FromHex("4fc0ff")),
(2.0f, Color4Extensions.FromHex("4fffd5")),
(2.5f, Color4Extensions.FromHex("7cff4f")),
- (3.25f, Color4Extensions.FromHex("f6f05c")),
- (4.5f, Color4Extensions.FromHex("ff8068")),
- (6.0f, Color4Extensions.FromHex("ff3c71")),
- (7.0f, Color4Extensions.FromHex("6563de")),
- (8.0f, Color4Extensions.FromHex("18158e")),
- (8.0f, Color4.Black),
+ (3.3f, Color4Extensions.FromHex("f6f05c")),
+ (4.2f, Color4Extensions.FromHex("ff8068")),
+ (4.9f, Color4Extensions.FromHex("ff4e6f")),
+ (5.8f, Color4Extensions.FromHex("c645b8")),
+ (6.7f, Color4Extensions.FromHex("6563de")),
+ (7.7f, Color4Extensions.FromHex("18158e")),
+ (9.0f, Color4.Black),
}, (float)Math.Round(starDifficulty, 2, MidpointRounding.AwayFromZero));
///
diff --git a/osu.Game/Graphics/UserInterface/OsuSliderBar.cs b/osu.Game/Graphics/UserInterface/OsuSliderBar.cs
index d4310dc901..333ae4f832 100644
--- a/osu.Game/Graphics/UserInterface/OsuSliderBar.cs
+++ b/osu.Game/Graphics/UserInterface/OsuSliderBar.cs
@@ -19,6 +19,7 @@ using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Framework.Utils;
using osu.Game.Overlays;
+using osu.Game.Utils;
namespace osu.Game.Graphics.UserInterface
{
@@ -219,7 +220,7 @@ namespace osu.Game.Graphics.UserInterface
decimal decimalPrecision = normalise(CurrentNumber.Precision.ToDecimal(NumberFormatInfo.InvariantInfo), max_decimal_digits);
// Find the number of significant digits (we could have less than 5 after normalize())
- int significantDigits = findPrecision(decimalPrecision);
+ int significantDigits = FormatUtils.FindPrecision(decimalPrecision);
TooltipText = floatValue.ToString($"N{significantDigits}");
}
@@ -248,23 +249,5 @@ namespace osu.Game.Graphics.UserInterface
/// The normalised decimal.
private decimal normalise(decimal d, int sd)
=> decimal.Parse(Math.Round(d, sd).ToString(string.Concat("0.", new string('#', sd)), CultureInfo.InvariantCulture), CultureInfo.InvariantCulture);
-
- ///
- /// Finds the number of digits after the decimal.
- ///
- /// The value to find the number of decimal digits for.
- /// The number decimal digits.
- private int findPrecision(decimal d)
- {
- int precision = 0;
-
- while (d != Math.Round(d))
- {
- d *= 10;
- precision++;
- }
-
- return precision;
- }
}
}
diff --git a/osu.Game/IO/Archives/LegacyByteArrayReader.cs b/osu.Game/IO/Archives/LegacyByteArrayReader.cs
index 0c3620403f..ea8ff3bbe0 100644
--- a/osu.Game/IO/Archives/LegacyByteArrayReader.cs
+++ b/osu.Game/IO/Archives/LegacyByteArrayReader.cs
@@ -7,7 +7,7 @@ using System.IO;
namespace osu.Game.IO.Archives
{
///
- /// Allows reading a single file from the provided stream.
+ /// Allows reading a single file from the provided byte array.
///
public class LegacyByteArrayReader : ArchiveReader
{
diff --git a/osu.Game/Overlays/Settings/SettingsHeader.cs b/osu.Game/Overlays/Settings/SettingsHeader.cs
index 69b7b69a29..f9ee8df0bd 100644
--- a/osu.Game/Overlays/Settings/SettingsHeader.cs
+++ b/osu.Game/Overlays/Settings/SettingsHeader.cs
@@ -6,7 +6,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Localisation;
using osu.Game.Graphics;
-using osu.Game.Graphics.Sprites;
+using osu.Game.Graphics.Containers;
namespace osu.Game.Overlays.Settings
{
@@ -29,36 +29,26 @@ namespace osu.Game.Overlays.Settings
Children = new Drawable[]
{
- new FillFlowContainer
+ new OsuTextFlowContainer
{
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
- Direction = FillDirection.Vertical,
- Children = new Drawable[]
+ Padding = new MarginPadding
{
- new OsuSpriteText
- {
- Text = heading,
- Font = OsuFont.TorusAlternate.With(size: 40),
- Margin = new MarginPadding
- {
- Left = SettingsPanel.CONTENT_MARGINS,
- Top = Toolbar.Toolbar.TOOLTIP_HEIGHT
- },
- },
- new OsuSpriteText
- {
- Colour = colourProvider.Content2,
- Text = subheading,
- Font = OsuFont.GetFont(size: 18),
- Margin = new MarginPadding
- {
- Left = SettingsPanel.CONTENT_MARGINS,
- Bottom = 30
- },
- },
+ Horizontal = SettingsPanel.CONTENT_MARGINS,
+ Top = Toolbar.Toolbar.TOOLTIP_HEIGHT,
+ Bottom = 30
}
- }
+ }.With(flow =>
+ {
+ flow.AddText(heading, header => header.Font = OsuFont.TorusAlternate.With(size: 40));
+ flow.NewLine();
+ flow.AddText(subheading, subheader =>
+ {
+ subheader.Colour = colourProvider.Content2;
+ subheader.Font = OsuFont.GetFont(size: 18);
+ });
+ })
};
}
}
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.cs
index 21457ea273..b230bab0c2 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.cs
@@ -1,9 +1,11 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
+using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
@@ -14,19 +16,20 @@ using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Rulesets.Objects;
using osu.Game.Screens.Edit.Timing;
+using osuTK;
namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
public class DifficultyPointPiece : HitObjectPointPiece, IHasPopover
{
- private readonly HitObject hitObject;
+ public readonly HitObject HitObject;
private readonly BindableNumber speedMultiplier;
public DifficultyPointPiece(HitObject hitObject)
: base(hitObject.DifficultyControlPoint)
{
- this.hitObject = hitObject;
+ HitObject = hitObject;
speedMultiplier = hitObject.DifficultyControlPoint.SliderVelocityBindable.GetBoundCopy();
}
@@ -44,14 +47,13 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
return true;
}
- public Popover GetPopover() => new DifficultyEditPopover(hitObject);
+ public Popover GetPopover() => new DifficultyEditPopover(HitObject);
public class DifficultyEditPopover : OsuPopover
{
private readonly HitObject hitObject;
- private readonly DifficultyControlPoint point;
- private SliderWithTextBoxInput sliderVelocitySlider;
+ private IndeterminateSliderWithTextBoxInput sliderVelocitySlider;
[Resolved(canBeNull: true)]
private EditorBeatmap beatmap { get; set; }
@@ -59,7 +61,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
public DifficultyEditPopover(HitObject hitObject)
{
this.hitObject = hitObject;
- point = hitObject.DifficultyControlPoint;
}
[BackgroundDependencyLoader]
@@ -72,11 +73,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
Width = 200,
Direction = FillDirection.Vertical,
AutoSizeAxes = Axes.Y,
+ Spacing = new Vector2(0, 15),
Children = new Drawable[]
{
- sliderVelocitySlider = new SliderWithTextBoxInput("Velocity")
+ sliderVelocitySlider = new IndeterminateSliderWithTextBoxInput("Velocity", new DifficultyControlPoint().SliderVelocityBindable)
{
- Current = new DifficultyControlPoint().SliderVelocityBindable,
KeyboardStep = 0.1f
},
new OsuTextFlowContainer
@@ -89,17 +90,37 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
}
};
- var selectedPointBindable = point.SliderVelocityBindable;
+ // if the piece belongs to a currently selected object, assume that the user wants to change all selected objects.
+ // if the piece belongs to an unselected object, operate on that object alone, independently of the selection.
+ var relevantObjects = (beatmap.SelectedHitObjects.Contains(hitObject) ? beatmap.SelectedHitObjects : hitObject.Yield()).ToArray();
+ var relevantControlPoints = relevantObjects.Select(h => h.DifficultyControlPoint).ToArray();
- // there may be legacy control points, which contain infinite precision for compatibility reasons (see LegacyDifficultyControlPoint).
- // generally that level of precision could only be set by externally editing the .osu file, so at the point
- // a user is looking to update this within the editor it should be safe to obliterate this additional precision.
- double expectedPrecision = new DifficultyControlPoint().SliderVelocityBindable.Precision;
- if (selectedPointBindable.Precision < expectedPrecision)
- selectedPointBindable.Precision = expectedPrecision;
+ // even if there are multiple objects selected, we can still display a value if they all have the same value.
+ var selectedPointBindable = relevantControlPoints.Select(point => point.SliderVelocity).Distinct().Count() == 1 ? relevantControlPoints.First().SliderVelocityBindable : null;
- sliderVelocitySlider.Current = selectedPointBindable;
- sliderVelocitySlider.Current.BindValueChanged(_ => beatmap?.Update(hitObject));
+ if (selectedPointBindable != null)
+ {
+ // there may be legacy control points, which contain infinite precision for compatibility reasons (see LegacyDifficultyControlPoint).
+ // generally that level of precision could only be set by externally editing the .osu file, so at the point
+ // a user is looking to update this within the editor it should be safe to obliterate this additional precision.
+ sliderVelocitySlider.Current.Value = selectedPointBindable.Value;
+ }
+
+ sliderVelocitySlider.Current.BindValueChanged(val =>
+ {
+ if (val.NewValue == null)
+ return;
+
+ beatmap.BeginChange();
+
+ foreach (var h in relevantObjects)
+ {
+ h.DifficultyControlPoint.SliderVelocity = val.NewValue.Value;
+ beatmap.Update(h);
+ }
+
+ beatmap.EndChange();
+ });
}
}
}
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs
index 6a26f69e41..2cbfe88519 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs
@@ -1,9 +1,14 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+#nullable enable
+
+using System.Collections.Generic;
+using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
+using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
@@ -14,12 +19,13 @@ using osu.Game.Graphics;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Rulesets.Objects;
using osu.Game.Screens.Edit.Timing;
+using osuTK;
namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
public class SamplePointPiece : HitObjectPointPiece, IHasPopover
{
- private readonly HitObject hitObject;
+ public readonly HitObject HitObject;
private readonly Bindable bank;
private readonly BindableNumber volume;
@@ -27,7 +33,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
public SamplePointPiece(HitObject hitObject)
: base(hitObject.SampleControlPoint)
{
- this.hitObject = hitObject;
+ HitObject = hitObject;
volume = hitObject.SampleControlPoint.SampleVolumeBindable.GetBoundCopy();
bank = hitObject.SampleControlPoint.SampleBankBindable.GetBoundCopy();
}
@@ -50,23 +56,21 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
Label.Text = $"{bank.Value} {volume.Value}";
}
- public Popover GetPopover() => new SampleEditPopover(hitObject);
+ public Popover GetPopover() => new SampleEditPopover(HitObject);
public class SampleEditPopover : OsuPopover
{
private readonly HitObject hitObject;
- private readonly SampleControlPoint point;
- private LabelledTextBox bank;
- private SliderWithTextBoxInput volume;
+ private LabelledTextBox bank = null!;
+ private IndeterminateSliderWithTextBoxInput volume = null!;
[Resolved(canBeNull: true)]
- private EditorBeatmap beatmap { get; set; }
+ private EditorBeatmap beatmap { get; set; } = null!;
public SampleEditPopover(HitObject hitObject)
{
this.hitObject = hitObject;
- point = hitObject.SampleControlPoint;
}
[BackgroundDependencyLoader]
@@ -79,25 +83,84 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
Width = 200,
Direction = FillDirection.Vertical,
AutoSizeAxes = Axes.Y,
+ Spacing = new Vector2(0, 10),
Children = new Drawable[]
{
bank = new LabelledTextBox
{
Label = "Bank Name",
},
- volume = new SliderWithTextBoxInput("Volume")
- {
- Current = new SampleControlPoint().SampleVolumeBindable,
- }
+ volume = new IndeterminateSliderWithTextBoxInput("Volume", new SampleControlPoint().SampleVolumeBindable)
}
}
};
- bank.Current = point.SampleBankBindable;
- bank.Current.BindValueChanged(_ => beatmap.Update(hitObject));
+ // if the piece belongs to a currently selected object, assume that the user wants to change all selected objects.
+ // if the piece belongs to an unselected object, operate on that object alone, independently of the selection.
+ var relevantObjects = (beatmap.SelectedHitObjects.Contains(hitObject) ? beatmap.SelectedHitObjects : hitObject.Yield()).ToArray();
+ var relevantControlPoints = relevantObjects.Select(h => h.SampleControlPoint).ToArray();
- volume.Current = point.SampleVolumeBindable;
- volume.Current.BindValueChanged(_ => beatmap.Update(hitObject));
+ // even if there are multiple objects selected, we can still display sample volume or bank if they all have the same value.
+ string? commonBank = getCommonBank(relevantControlPoints);
+ if (!string.IsNullOrEmpty(commonBank))
+ bank.Current.Value = commonBank;
+
+ int? commonVolume = getCommonVolume(relevantControlPoints);
+ if (commonVolume != null)
+ volume.Current.Value = commonVolume.Value;
+
+ updateBankPlaceholderText(relevantObjects);
+ bank.Current.BindValueChanged(val =>
+ {
+ updateBankFor(relevantObjects, val.NewValue);
+ updateBankPlaceholderText(relevantObjects);
+ });
+ // on commit, ensure that the value is correct by sourcing it from the objects' control points again.
+ // this ensures that committing empty text causes a revert to the previous value.
+ bank.OnCommit += (_, __) => bank.Current.Value = getCommonBank(relevantControlPoints);
+
+ volume.Current.BindValueChanged(val => updateVolumeFor(relevantObjects, val.NewValue));
+ }
+
+ private static string? getCommonBank(SampleControlPoint[] relevantControlPoints) => relevantControlPoints.Select(point => point.SampleBank).Distinct().Count() == 1 ? relevantControlPoints.First().SampleBank : null;
+ private static int? getCommonVolume(SampleControlPoint[] relevantControlPoints) => relevantControlPoints.Select(point => point.SampleVolume).Distinct().Count() == 1 ? (int?)relevantControlPoints.First().SampleVolume : null;
+
+ private void updateBankFor(IEnumerable objects, string? newBank)
+ {
+ if (string.IsNullOrEmpty(newBank))
+ return;
+
+ beatmap.BeginChange();
+
+ foreach (var h in objects)
+ {
+ h.SampleControlPoint.SampleBank = newBank;
+ beatmap.Update(h);
+ }
+
+ beatmap.EndChange();
+ }
+
+ private void updateBankPlaceholderText(IEnumerable objects)
+ {
+ string? commonBank = getCommonBank(objects.Select(h => h.SampleControlPoint).ToArray());
+ bank.PlaceholderText = string.IsNullOrEmpty(commonBank) ? "(multiple)" : null;
+ }
+
+ private void updateVolumeFor(IEnumerable objects, int? newVolume)
+ {
+ if (newVolume == null)
+ return;
+
+ beatmap.BeginChange();
+
+ foreach (var h in objects)
+ {
+ h.SampleControlPoint.SampleVolume = newVolume.Value;
+ beatmap.Update(h);
+ }
+
+ beatmap.EndChange();
}
}
}
diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs
index 738f607cc8..2a7e2c9cef 100644
--- a/osu.Game/Screens/Edit/Editor.cs
+++ b/osu.Game/Screens/Edit/Editor.cs
@@ -31,6 +31,7 @@ using osu.Game.Screens.Edit.Components.Menus;
using osu.Game.Screens.Edit.Components.Timelines.Summary;
using osu.Game.Screens.Edit.Compose;
using osu.Game.Screens.Edit.Design;
+using osu.Game.Screens.Edit.GameplayTest;
using osu.Game.Screens.Edit.Setup;
using osu.Game.Screens.Edit.Timing;
using osu.Game.Screens.Edit.Verify;
@@ -324,6 +325,19 @@ namespace osu.Game.Screens.Edit
///
public void UpdateClockSource() => clock.ChangeSource(Beatmap.Value.Track);
+ ///
+ /// Creates an instance representing the current state of the editor.
+ ///
+ ///
+ /// The next beatmap to be shown, in the case of difficulty switch.
+ /// indicates that the beatmap will not be changing.
+ ///
+ public EditorState GetState([CanBeNull] BeatmapInfo nextBeatmap = null) => new EditorState
+ {
+ Time = clock.CurrentTimeAccurate,
+ ClipboardContent = nextBeatmap == null || editorBeatmap.BeatmapInfo.RulesetID == nextBeatmap.RulesetID ? Clipboard.Content.Value : string.Empty
+ };
+
///
/// Restore the editor to a provided state.
///
@@ -486,7 +500,18 @@ namespace osu.Game.Screens.Edit
public override void OnEntering(IScreen last)
{
base.OnEntering(last);
+ dimBackground();
+ resetTrack(true);
+ }
+ public override void OnResuming(IScreen last)
+ {
+ base.OnResuming(last);
+ dimBackground();
+ }
+
+ private void dimBackground()
+ {
ApplyToBackground(b =>
{
// todo: temporary. we want to be applying dim using the UserDimContainer eventually.
@@ -495,8 +520,6 @@ namespace osu.Game.Screens.Edit
b.IgnoreUserSettings.Value = true;
b.BlurAmount.Value = 0;
});
-
- resetTrack(true);
}
public override bool OnExiting(IScreen next)
@@ -535,9 +558,9 @@ namespace osu.Game.Screens.Edit
public override void OnSuspending(IScreen next)
{
- refetchBeatmap();
-
base.OnSuspending(next);
+ clock.Stop();
+ refetchBeatmap();
}
private void refetchBeatmap()
@@ -770,11 +793,7 @@ namespace osu.Game.Screens.Edit
return new DifficultyMenuItem(beatmapInfo, isCurrentDifficulty, SwitchToDifficulty);
}
- protected void SwitchToDifficulty(BeatmapInfo nextBeatmap) => loader?.ScheduleDifficultySwitch(nextBeatmap, new EditorState
- {
- Time = clock.CurrentTimeAccurate,
- ClipboardContent = editorBeatmap.BeatmapInfo.RulesetID == nextBeatmap.RulesetID ? Clipboard.Content.Value : string.Empty
- });
+ protected void SwitchToDifficulty(BeatmapInfo nextBeatmap) => loader?.ScheduleDifficultySwitch(nextBeatmap, GetState(nextBeatmap));
private void cancelExit()
{
@@ -797,7 +816,7 @@ namespace osu.Game.Screens.Edit
pushEditorPlayer();
}
- void pushEditorPlayer() => this.Push(new PlayerLoader(() => new EditorPlayer()));
+ void pushEditorPlayer() => this.Push(new EditorPlayerLoader(this));
}
public double SnapTime(double time, double? referenceTime) => editorBeatmap.SnapTime(time, referenceTime);
diff --git a/osu.Game/Screens/Edit/EditorPlayer.cs b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs
similarity index 52%
rename from osu.Game/Screens/Edit/EditorPlayer.cs
rename to osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs
index b2fab3fefc..f49603c754 100644
--- a/osu.Game/Screens/Edit/EditorPlayer.cs
+++ b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs
@@ -3,21 +3,30 @@
using osu.Framework.Allocation;
using osu.Framework.Screens;
+using osu.Game.Beatmaps;
using osu.Game.Overlays;
using osu.Game.Screens.Play;
-namespace osu.Game.Screens.Edit
+namespace osu.Game.Screens.Edit.GameplayTest
{
public class EditorPlayer : Player
{
- public EditorPlayer()
- : base(new PlayerConfiguration { ShowResults = false })
- {
- }
+ private readonly Editor editor;
+ private readonly EditorState editorState;
[Resolved]
private MusicController musicController { get; set; }
+ public EditorPlayer(Editor editor)
+ : base(new PlayerConfiguration { ShowResults = false })
+ {
+ this.editor = editor;
+ editorState = editor.GetState();
+ }
+
+ protected override GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart)
+ => new MasterGameplayClockContainer(beatmap, editorState.Time, true);
+
protected override void LoadComplete()
{
base.LoadComplete();
@@ -35,9 +44,22 @@ namespace osu.Game.Screens.Edit
protected override bool CheckModsAllowFailure() => false; // never fail.
+ public override void OnEntering(IScreen last)
+ {
+ base.OnEntering(last);
+
+ // finish alpha transforms on entering to avoid gameplay starting in a half-hidden state.
+ // the finish calls are purposefully not propagated to children to avoid messing up their state.
+ FinishTransforms();
+ GameplayClockContainer.FinishTransforms(false, nameof(Alpha));
+ }
+
public override bool OnExiting(IScreen next)
{
musicController.Stop();
+
+ editorState.Time = GameplayClockContainer.CurrentTime;
+ editor.RestoreState(editorState);
return base.OnExiting(next);
}
}
diff --git a/osu.Game/Screens/Edit/GameplayTest/EditorPlayerLoader.cs b/osu.Game/Screens/Edit/GameplayTest/EditorPlayerLoader.cs
new file mode 100644
index 0000000000..addc79ba61
--- /dev/null
+++ b/osu.Game/Screens/Edit/GameplayTest/EditorPlayerLoader.cs
@@ -0,0 +1,44 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Screens;
+using osu.Game.Screens.Menu;
+using osu.Game.Screens.Play;
+
+namespace osu.Game.Screens.Edit.GameplayTest
+{
+ public class EditorPlayerLoader : PlayerLoader
+ {
+ [Resolved]
+ private OsuLogo osuLogo { get; set; }
+
+ public EditorPlayerLoader(Editor editor)
+ : base(() => new EditorPlayer(editor))
+ {
+ }
+
+ public override void OnEntering(IScreen last)
+ {
+ base.OnEntering(last);
+
+ MetadataInfo.FinishTransforms(true);
+ }
+
+ protected override void LogoArriving(OsuLogo logo, bool resuming)
+ {
+ // call base with resuming forcefully set to true to reduce logo movements.
+ base.LogoArriving(logo, true);
+ logo.FinishTransforms(true, nameof(Scale));
+ }
+
+ protected override void ContentOut()
+ {
+ base.ContentOut();
+ osuLogo.FadeOut(CONTENT_OUT_DURATION, Easing.OutQuint);
+ }
+
+ protected override double PlayerPushDelay => 0;
+ }
+}
diff --git a/osu.Game/Screens/Edit/SaveBeforeGameplayTestDialog.cs b/osu.Game/Screens/Edit/GameplayTest/SaveBeforeGameplayTestDialog.cs
similarity index 95%
rename from osu.Game/Screens/Edit/SaveBeforeGameplayTestDialog.cs
rename to osu.Game/Screens/Edit/GameplayTest/SaveBeforeGameplayTestDialog.cs
index 7c03664c66..9334c74706 100644
--- a/osu.Game/Screens/Edit/SaveBeforeGameplayTestDialog.cs
+++ b/osu.Game/Screens/Edit/GameplayTest/SaveBeforeGameplayTestDialog.cs
@@ -5,7 +5,7 @@ using System;
using osu.Framework.Graphics.Sprites;
using osu.Game.Overlays.Dialog;
-namespace osu.Game.Screens.Edit
+namespace osu.Game.Screens.Edit.GameplayTest
{
public class SaveBeforeGameplayTestDialog : PopupDialog
{
diff --git a/osu.Game/Screens/Edit/Timing/IndeterminateSliderWithTextBoxInput.cs b/osu.Game/Screens/Edit/Timing/IndeterminateSliderWithTextBoxInput.cs
new file mode 100644
index 0000000000..14b8c4c9de
--- /dev/null
+++ b/osu.Game/Screens/Edit/Timing/IndeterminateSliderWithTextBoxInput.cs
@@ -0,0 +1,123 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Globalization;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.UserInterface;
+using osu.Framework.Localisation;
+using osu.Game.Graphics.UserInterfaceV2;
+using osu.Game.Overlays.Settings;
+using osu.Game.Utils;
+using osuTK;
+
+namespace osu.Game.Screens.Edit.Timing
+{
+ ///
+ /// Analogous to , but supports scenarios
+ /// where multiple objects with multiple different property values are selected
+ /// by providing an "indeterminate state".
+ ///
+ public class IndeterminateSliderWithTextBoxInput : CompositeDrawable, IHasCurrentValue
+ where T : struct, IEquatable, IComparable, IConvertible
+ {
+ ///
+ /// A custom step value for each key press which actuates a change on this control.
+ ///
+ public float KeyboardStep
+ {
+ get => slider.KeyboardStep;
+ set => slider.KeyboardStep = value;
+ }
+
+ private readonly BindableWithCurrent current = new BindableWithCurrent();
+
+ public Bindable Current
+ {
+ get => current.Current;
+ set => current.Current = value;
+ }
+
+ private readonly SettingsSlider slider;
+ private readonly LabelledTextBox textbox;
+
+ ///
+ /// Creates an .
+ ///
+ /// The label text for the slider and text box.
+ ///
+ /// Bindable to use for the slider until a non-null value is set for .
+ /// In particular, it can be used to control min/max bounds and precision in the case of s.
+ ///
+ public IndeterminateSliderWithTextBoxInput(LocalisableString labelText, Bindable indeterminateValue)
+ {
+ RelativeSizeAxes = Axes.X;
+ AutoSizeAxes = Axes.Y;
+
+ InternalChildren = new Drawable[]
+ {
+ new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Direction = FillDirection.Vertical,
+ Spacing = new Vector2(0, 5),
+ Children = new Drawable[]
+ {
+ textbox = new LabelledTextBox
+ {
+ Label = labelText,
+ },
+ slider = new SettingsSlider
+ {
+ TransferValueOnCommit = true,
+ RelativeSizeAxes = Axes.X,
+ Current = indeterminateValue
+ }
+ }
+ },
+ };
+
+ textbox.OnCommit += (t, isNew) =>
+ {
+ if (!isNew) return;
+
+ try
+ {
+ slider.Current.Parse(t.Text);
+ }
+ catch
+ {
+ // TriggerChange below will restore the previous text value on failure.
+ }
+
+ // This is run regardless of parsing success as the parsed number may not actually trigger a change
+ // due to bindable clamping. Even in such a case we want to update the textbox to a sane visual state.
+ Current.TriggerChange();
+ };
+ slider.Current.BindValueChanged(val => Current.Value = val.NewValue);
+
+ Current.BindValueChanged(_ => updateState(), true);
+ }
+
+ private void updateState()
+ {
+ if (Current.Value is T nonNullValue)
+ {
+ slider.Current.Value = nonNullValue;
+
+ // use the value from the slider to ensure that any precision/min/max set on it via the initial indeterminate value have been applied correctly.
+ decimal decimalValue = slider.Current.Value.ToDecimal(NumberFormatInfo.InvariantInfo);
+ textbox.Text = decimalValue.ToString($@"N{FormatUtils.FindPrecision(decimalValue)}");
+ textbox.PlaceholderText = string.Empty;
+ }
+ else
+ {
+ textbox.Text = null;
+ textbox.PlaceholderText = "(multiple)";
+ }
+ }
+ }
+}
diff --git a/osu.Game/Screens/Play/FailAnimation.cs b/osu.Game/Screens/Play/FailAnimation.cs
index f3676baf80..193e1e4129 100644
--- a/osu.Game/Screens/Play/FailAnimation.cs
+++ b/osu.Game/Screens/Play/FailAnimation.cs
@@ -107,7 +107,8 @@ namespace osu.Game.Screens.Play
this.TransformBindableTo(trackFreq, 0, duration).OnComplete(_ =>
{
- RemoveFilters();
+ // Don't reset frequency as the pause screen may appear post transform, causing a second frequency sweep.
+ RemoveFilters(false);
OnComplete?.Invoke();
});
@@ -137,15 +138,16 @@ namespace osu.Game.Screens.Play
Content.FadeColour(Color4.Gray, duration);
}
- public void RemoveFilters()
+ public void RemoveFilters(bool resetTrackFrequency = true)
{
+ if (resetTrackFrequency)
+ track?.RemoveAdjustment(AdjustableProperty.Frequency, trackFreq);
+
if (filters.Parent == null)
return;
RemoveInternal(filters);
filters.Dispose();
-
- track?.RemoveAdjustment(AdjustableProperty.Frequency, trackFreq);
}
protected override void Update()
diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs
index d852ac2940..57db411571 100644
--- a/osu.Game/Screens/Play/PlayerLoader.cs
+++ b/osu.Game/Screens/Play/PlayerLoader.cs
@@ -5,6 +5,7 @@ using System;
using System.Diagnostics;
using System.Threading.Tasks;
using JetBrains.Annotations;
+using ManagedBass.Fx;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Bindables;
@@ -35,7 +36,9 @@ namespace osu.Game.Screens.Play
{
protected const float BACKGROUND_BLUR = 15;
- private const double content_out_duration = 300;
+ protected const double CONTENT_OUT_DURATION = 300;
+
+ protected virtual double PlayerPushDelay => 1800;
public override bool HideOverlaysOnEnter => hideOverlays;
@@ -67,6 +70,7 @@ namespace osu.Game.Screens.Play
private readonly BindableDouble volumeAdjustment = new BindableDouble(1);
private AudioFilter lowPassFilter;
+ private AudioFilter highPassFilter;
protected bool BackgroundBrightnessReduction
{
@@ -168,7 +172,8 @@ namespace osu.Game.Screens.Play
},
idleTracker = new IdleTracker(750),
}),
- lowPassFilter = new AudioFilter(audio.TrackMixer)
+ lowPassFilter = new AudioFilter(audio.TrackMixer),
+ highPassFilter = new AudioFilter(audio.TrackMixer, BQFType.HighPass)
};
if (Beatmap.Value.BeatmapInfo.EpilepsyWarning)
@@ -210,7 +215,7 @@ namespace osu.Game.Screens.Play
// after an initial delay, start the debounced load check.
// this will continue to execute even after resuming back on restart.
- Scheduler.Add(new ScheduledDelegate(pushWhenLoaded, Clock.CurrentTime + 1800, 0));
+ Scheduler.Add(new ScheduledDelegate(pushWhenLoaded, Clock.CurrentTime + PlayerPushDelay, 0));
showMuteWarningIfNeeded();
showBatteryWarningIfNeeded();
@@ -239,18 +244,19 @@ namespace osu.Game.Screens.Play
Beatmap.Value.Track.Stop();
Beatmap.Value.Track.RemoveAdjustment(AdjustableProperty.Volume, volumeAdjustment);
lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF);
+ highPassFilter.CutoffTo(0);
}
public override bool OnExiting(IScreen next)
{
cancelLoad();
- contentOut();
+ ContentOut();
// If the load sequence was interrupted, the epilepsy warning may already be displayed (or in the process of being displayed).
epilepsyWarning?.Hide();
// Ensure the screen doesn't expire until all the outwards fade operations have completed.
- this.Delay(content_out_duration).FadeOut();
+ this.Delay(CONTENT_OUT_DURATION).FadeOut();
ApplyToBackground(b => b.IgnoreUserSettings.Value = true);
@@ -266,9 +272,9 @@ namespace osu.Game.Screens.Play
const double duration = 300;
- if (!resuming) logo.MoveTo(new Vector2(0.5f), duration, Easing.In);
+ if (!resuming) logo.MoveTo(new Vector2(0.5f), duration, Easing.OutQuint);
- logo.ScaleTo(new Vector2(0.15f), duration, Easing.In);
+ logo.ScaleTo(new Vector2(0.15f), duration, Easing.OutQuint);
logo.FadeIn(350);
Scheduler.AddDelayed(() =>
@@ -352,18 +358,20 @@ namespace osu.Game.Screens.Play
content.FadeInFromZero(400);
content.ScaleTo(1, 650, Easing.OutQuint).Then().Schedule(prepareNewPlayer);
lowPassFilter.CutoffTo(1000, 650, Easing.OutQuint);
+ highPassFilter.CutoffTo(300).Then().CutoffTo(0, 1250); // 1250 is to line up with the appearance of MetadataInfo (750 delay + 500 fade-in)
ApplyToBackground(b => b?.FadeColour(Color4.White, 800, Easing.OutQuint));
}
- private void contentOut()
+ protected virtual void ContentOut()
{
// Ensure the logo is no longer tracking before we scale the content
content.StopTracking();
- content.ScaleTo(0.7f, content_out_duration * 2, Easing.OutQuint);
- content.FadeOut(content_out_duration, Easing.OutQuint);
- lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, content_out_duration);
+ content.ScaleTo(0.7f, CONTENT_OUT_DURATION * 2, Easing.OutQuint);
+ content.FadeOut(CONTENT_OUT_DURATION, Easing.OutQuint);
+ lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, CONTENT_OUT_DURATION);
+ highPassFilter.CutoffTo(0, CONTENT_OUT_DURATION);
}
private void pushWhenLoaded()
@@ -388,9 +396,9 @@ namespace osu.Game.Screens.Play
// ensure that once we have reached this "point of no return", readyForPush will be false for all future checks (until a new player instance is prepared).
var consumedPlayer = consumePlayer();
- contentOut();
+ ContentOut();
- TransformSequence pushSequence = this.Delay(content_out_duration);
+ TransformSequence pushSequence = this.Delay(CONTENT_OUT_DURATION);
// only show if the warning was created (i.e. the beatmap needs it)
// and this is not a restart of the map (the warning expires after first load).
@@ -412,7 +420,7 @@ namespace osu.Game.Screens.Play
else
{
// This goes hand-in-hand with the restoration of low pass filter in contentOut().
- this.TransformBindableTo(volumeAdjustment, 0, content_out_duration, Easing.OutCubic);
+ this.TransformBindableTo(volumeAdjustment, 0, CONTENT_OUT_DURATION, Easing.OutCubic);
}
pushSequence.Schedule(() =>
diff --git a/osu.Game/Screens/Play/SkipOverlay.cs b/osu.Game/Screens/Play/SkipOverlay.cs
index b04fcba0c6..c35548c6b4 100644
--- a/osu.Game/Screens/Play/SkipOverlay.cs
+++ b/osu.Game/Screens/Play/SkipOverlay.cs
@@ -270,7 +270,7 @@ namespace osu.Game.Screens.Play
colourNormal = colours.Yellow;
colourHover = colours.YellowDark;
- sampleConfirm = audio.Samples.Get(@"SongSelect/confirm-selection");
+ sampleConfirm = audio.Samples.Get(@"UI/submit-select");
Children = new Drawable[]
{
diff --git a/osu.Game/Screens/Select/Carousel/CarouselHeader.cs b/osu.Game/Screens/Select/Carousel/CarouselHeader.cs
index 40ca3e0764..ed3aea3445 100644
--- a/osu.Game/Screens/Select/Carousel/CarouselHeader.cs
+++ b/osu.Game/Screens/Select/Carousel/CarouselHeader.cs
@@ -113,7 +113,7 @@ namespace osu.Game.Screens.Select.Carousel
RelativeSizeAxes = Axes.Both,
};
- sampleHover = audio.Samples.Get("SongSelect/song-ping");
+ sampleHover = audio.Samples.Get("UI/default-hover");
}
public bool InsetForBorder
diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs
index a2dea355ac..4da15ee53c 100644
--- a/osu.Game/Screens/Select/SongSelect.cs
+++ b/osu.Game/Screens/Select/SongSelect.cs
@@ -105,6 +105,8 @@ namespace osu.Game.Screens.Select
private readonly Bindable decoupledRuleset = new Bindable();
+ private double audioFeedbackLastPlaybackTime;
+
[Resolved]
private MusicController music { get; set; }
@@ -435,6 +437,7 @@ namespace osu.Game.Screens.Select
}
// We need to keep track of the last selected beatmap ignoring debounce to play the correct selection sounds.
+ private BeatmapInfo beatmapInfoPrevious;
private BeatmapInfo beatmapInfoNoDebounce;
private RulesetInfo rulesetNoDebounce;
@@ -477,6 +480,21 @@ namespace osu.Game.Screens.Select
else
selectionChangedDebounce = Scheduler.AddDelayed(run, 200);
+ if (beatmap != beatmapInfoPrevious)
+ {
+ if (beatmap != null && beatmapInfoPrevious != null && Time.Current - audioFeedbackLastPlaybackTime >= 50)
+ {
+ if (beatmap.BeatmapSetInfoID == beatmapInfoPrevious.BeatmapSetInfoID)
+ sampleChangeDifficulty.Play();
+ else
+ sampleChangeBeatmap.Play();
+
+ audioFeedbackLastPlaybackTime = Time.Current;
+ }
+
+ beatmapInfoPrevious = beatmap;
+ }
+
void run()
{
// clear pending task immediately to track any potential nested debounce operation.
@@ -508,18 +526,7 @@ namespace osu.Game.Screens.Select
if (!EqualityComparer.Default.Equals(beatmap, Beatmap.Value.BeatmapInfo))
{
Logger.Log($"beatmap changed from \"{Beatmap.Value.BeatmapInfo}\" to \"{beatmap}\"");
-
- int? lastSetID = Beatmap.Value?.BeatmapInfo.BeatmapSetInfoID;
-
Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmap);
-
- if (beatmap != null)
- {
- if (beatmap.BeatmapSetInfoID == lastSetID)
- sampleChangeDifficulty.Play();
- else
- sampleChangeBeatmap.Play();
- }
}
if (this.IsCurrentScreen())
diff --git a/osu.Game/Utils/FormatUtils.cs b/osu.Game/Utils/FormatUtils.cs
index d14dbb49f3..799dc75ca9 100644
--- a/osu.Game/Utils/FormatUtils.cs
+++ b/osu.Game/Utils/FormatUtils.cs
@@ -31,5 +31,23 @@ namespace osu.Game.Utils
///
/// The rank/position to be formatted.
public static string FormatRank(this int rank) => rank.ToMetric(decimals: rank < 100_000 ? 1 : 0);
+
+ ///
+ /// Finds the number of digits after the decimal.
+ ///
+ /// The value to find the number of decimal digits for.
+ /// The number decimal digits.
+ public static int FindPrecision(decimal d)
+ {
+ int precision = 0;
+
+ while (d != Math.Round(d))
+ {
+ d *= 10;
+ precision++;
+ }
+
+ return precision;
+ }
}
}
diff --git a/osu.Game/Utils/ZipUtils.cs b/osu.Game/Utils/ZipUtils.cs
index cd4d876451..eb2d2d3b80 100644
--- a/osu.Game/Utils/ZipUtils.cs
+++ b/osu.Game/Utils/ZipUtils.cs
@@ -9,6 +9,34 @@ namespace osu.Game.Utils
{
public static class ZipUtils
{
+ public static bool IsZipArchive(MemoryStream stream)
+ {
+ try
+ {
+ stream.Seek(0, SeekOrigin.Begin);
+
+ using (var arc = ZipArchive.Open(stream))
+ {
+ foreach (var entry in arc.Entries)
+ {
+ using (entry.OpenEntryStream())
+ {
+ }
+ }
+ }
+
+ return true;
+ }
+ catch (Exception)
+ {
+ return false;
+ }
+ finally
+ {
+ stream.Seek(0, SeekOrigin.Begin);
+ }
+ }
+
public static bool IsZipArchive(string path)
{
if (!File.Exists(path))
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index df3c9b355a..93fb729f46 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -37,7 +37,7 @@
-
+
diff --git a/osu.iOS.props b/osu.iOS.props
index 1852957e87..3a2a3fd65e 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -71,7 +71,7 @@
-
+