diff --git a/osu.Android.props b/osu.Android.props
index 69f897128c..650ebde54d 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -51,7 +51,7 @@
-
+
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs
index fe46876050..d75f4c70d7 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs
@@ -24,7 +24,8 @@ namespace osu.Game.Rulesets.Osu.Mods
public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(OsuModSpunOut), typeof(ModRelax), typeof(ModSuddenDeath), typeof(ModNoFail), typeof(ModAutoplay) };
- public bool AllowFail => false;
+ public bool PerformFail() => false;
+
public bool RestartOnFail => false;
private OsuInputManager inputManager;
diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoScroller.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoScroller.cs
index 520961d3ce..16ef5b968d 100644
--- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoScroller.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoScroller.cs
@@ -3,6 +3,7 @@
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Testing;
+using osu.Framework.Timing;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Skinning;
@@ -12,11 +13,30 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
{
public class TestSceneTaikoScroller : TaikoSkinnableTestScene
{
+ private readonly ManualClock clock = new ManualClock();
+
+ private bool reversed;
+
public TestSceneTaikoScroller()
{
- AddStep("Load scroller", () => SetContents(() => new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.Scroller), _ => Empty())));
+ AddStep("Load scroller", () => SetContents(() =>
+ new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.Scroller), _ => Empty())
+ {
+ Clock = new FramedClock(clock),
+ Height = 0.4f,
+ }));
+
AddToggleStep("Toggle passing", passing => this.ChildrenOfType().ForEach(s => s.LastResult.Value =
new JudgementResult(null, new Judgement()) { Type = passing ? HitResult.Perfect : HitResult.Miss }));
+
+ AddToggleStep("toggle playback direction", reversed => this.reversed = reversed);
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ clock.CurrentTime += (reversed ? -1 : 1) * Clock.ElapsedFrameTime;
}
}
}
diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs
index 1ecdb839fb..03813e0a99 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs
@@ -17,6 +17,8 @@ namespace osu.Game.Rulesets.Taiko.Skinning
{
public class LegacyTaikoScroller : CompositeDrawable
{
+ public Bindable LastResult = new Bindable();
+
public LegacyTaikoScroller()
{
RelativeSizeAxes = Axes.Both;
@@ -50,37 +52,38 @@ namespace osu.Game.Rulesets.Taiko.Skinning
}, true);
}
- public Bindable LastResult = new Bindable();
-
protected override void Update()
{
base.Update();
- while (true)
+ // store X before checking wide enough so if we perform layout there is no positional discrepancy.
+ float currentX = (InternalChildren?.FirstOrDefault()?.X ?? 0) - (float)Clock.ElapsedFrameTime * 0.1f;
+
+ // ensure we have enough sprites
+ if (!InternalChildren.Any()
+ || InternalChildren.First().ScreenSpaceDrawQuad.Width * InternalChildren.Count < ScreenSpaceDrawQuad.Width * 2)
+ AddInternal(new ScrollerSprite { Passing = passing });
+
+ var first = InternalChildren.First();
+ var last = InternalChildren.Last();
+
+ foreach (var sprite in InternalChildren)
{
- float? additiveX = null;
+ // add the x coordinates and perform re-layout on all sprites as spacing may change with gameplay scale.
+ sprite.X = currentX;
+ currentX += sprite.DrawWidth;
+ }
- foreach (var sprite in InternalChildren)
- {
- // add the x coordinates and perform re-layout on all sprites as spacing may change with gameplay scale.
- sprite.X = additiveX ??= sprite.X - (float)Time.Elapsed * 0.1f;
+ if (first.ScreenSpaceDrawQuad.TopLeft.X >= ScreenSpaceDrawQuad.TopLeft.X)
+ {
+ foreach (var internalChild in InternalChildren)
+ internalChild.X -= first.DrawWidth;
+ }
- additiveX += sprite.DrawWidth - 1;
-
- if (sprite.X + sprite.DrawWidth < 0)
- sprite.Expire();
- }
-
- var last = InternalChildren.LastOrDefault();
-
- // only break from this loop once we have saturated horizontal space completely.
- if (last != null && last.ScreenSpaceDrawQuad.TopRight.X >= ScreenSpaceDrawQuad.TopRight.X)
- break;
-
- AddInternal(new ScrollerSprite
- {
- Passing = passing
- });
+ if (last.ScreenSpaceDrawQuad.TopRight.X <= ScreenSpaceDrawQuad.TopRight.X)
+ {
+ foreach (var internalChild in InternalChildren)
+ internalChild.X += first.DrawWidth;
}
}
diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs
index 9b37af1111..e6aacf34dc 100644
--- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs
+++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs
@@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Taiko.UI
{
new BarLineGenerator(Beatmap).BarLines.ForEach(bar => Playfield.Add(bar.Major ? new DrawableBarLineMajor(bar) : new DrawableBarLine(bar)));
- AddInternal(scroller = new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.Scroller), _ => Empty())
+ FrameStableComponents.Add(scroller = new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.Scroller), _ => Empty())
{
RelativeSizeAxes = Axes.X,
Depth = float.MaxValue
diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs
index 980f5ea340..1041456020 100644
--- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs
+++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs
@@ -13,18 +13,16 @@ namespace osu.Game.Rulesets.Taiko.UI
private const float default_relative_height = TaikoPlayfield.DEFAULT_HEIGHT / 768;
private const float default_aspect = 16f / 9f;
- public TaikoPlayfieldAdjustmentContainer()
- {
- Anchor = Anchor.CentreLeft;
- Origin = Anchor.CentreLeft;
- }
-
protected override void Update()
{
base.Update();
float aspectAdjust = Math.Clamp(Parent.ChildSize.X / Parent.ChildSize.Y, 0.4f, 4) / default_aspect;
Size = new Vector2(1, default_relative_height * aspectAdjust);
+
+ // Position the taiko playfield exactly one playfield from the top of the screen.
+ RelativePositionAxes = Axes.Y;
+ Y = Size.Y;
}
}
}
diff --git a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs
index ef2b20de64..743c924bbd 100644
--- a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs
+++ b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs
@@ -211,7 +211,61 @@ namespace osu.Game.Tests.NonVisual
var osu = loadOsu(host);
Assert.DoesNotThrow(() => osu.Migrate(customPath));
- Assert.Throws(() => osu.Migrate(customPath));
+ Assert.Throws(() => osu.Migrate(customPath));
+ }
+ finally
+ {
+ host.Exit();
+ }
+ }
+ }
+
+ [Test]
+ public void TestMigrationToNestedTargetFails()
+ {
+ using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestMigrationToNestedTargetFails)))
+ {
+ try
+ {
+ var osu = loadOsu(host);
+
+ Assert.DoesNotThrow(() => osu.Migrate(customPath));
+
+ string subFolder = Path.Combine(customPath, "sub");
+
+ if (Directory.Exists(subFolder))
+ Directory.Delete(subFolder, true);
+
+ Directory.CreateDirectory(subFolder);
+
+ Assert.Throws(() => osu.Migrate(subFolder));
+ }
+ finally
+ {
+ host.Exit();
+ }
+ }
+ }
+
+ [Test]
+ public void TestMigrationToSeeminglyNestedTarget()
+ {
+ using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestMigrationToSeeminglyNestedTarget)))
+ {
+ try
+ {
+ var osu = loadOsu(host);
+
+ Assert.DoesNotThrow(() => osu.Migrate(customPath));
+
+ string seeminglySubFolder = customPath + "sub";
+
+ if (Directory.Exists(seeminglySubFolder))
+ Directory.Delete(seeminglySubFolder, true);
+
+ Directory.CreateDirectory(seeminglySubFolder);
+
+ osu.Migrate(seeminglySubFolder);
}
finally
{
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplay.cs
index e82722e7a2..1908988739 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplay.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplay.cs
@@ -33,7 +33,8 @@ namespace osu.Game.Tests.Visual.Gameplay
{
public new ScoreProcessor ScoreProcessor => base.ScoreProcessor;
public new HUDOverlay HUDOverlay => base.HUDOverlay;
- public new bool AllowFail => base.AllowFail;
+
+ public bool AllowFail => base.CheckModsAllowFailure();
protected override bool PauseOnFocusLost => false;
diff --git a/osu.Game.Tests/Visual/Settings/TestSceneMigrationScreens.cs b/osu.Game.Tests/Visual/Settings/TestSceneMigrationScreens.cs
new file mode 100644
index 0000000000..2883e54385
--- /dev/null
+++ b/osu.Game.Tests/Visual/Settings/TestSceneMigrationScreens.cs
@@ -0,0 +1,36 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.IO;
+using System.Threading;
+using osu.Framework.Screens;
+using osu.Game.Overlays.Settings.Sections.Maintenance;
+
+namespace osu.Game.Tests.Visual.Settings
+{
+ public class TestSceneMigrationScreens : ScreenTestScene
+ {
+ public TestSceneMigrationScreens()
+ {
+ AddStep("Push screen", () => Stack.Push(new TestMigrationSelectScreen()));
+ }
+
+ private class TestMigrationSelectScreen : MigrationSelectScreen
+ {
+ protected override void BeginMigration(DirectoryInfo target) => this.Push(new TestMigrationRunScreen());
+
+ private class TestMigrationRunScreen : MigrationRunScreen
+ {
+ protected override void PerformMigration()
+ {
+ Thread.Sleep(3000);
+ }
+
+ public TestMigrationRunScreen()
+ : base(null)
+ {
+ }
+ }
+ }
+ }
+}
diff --git a/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs
index 46bb7b83e3..52f761e50a 100644
--- a/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs
+++ b/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs
@@ -19,7 +19,7 @@ using osuTK;
namespace osu.Game.Tournament.Screens.Editors
{
- public class SeedingEditorScreen : TournamentEditorScreen
+ public class SeedingEditorScreen : TournamentEditorScreen
{
private readonly TournamentTeam team;
@@ -30,14 +30,14 @@ namespace osu.Game.Tournament.Screens.Editors
this.team = team;
}
- public class SeeingResultRow : CompositeDrawable, IModelBacked
+ public class SeedingResultRow : CompositeDrawable, IModelBacked
{
public SeedingResult Model { get; }
[Resolved]
private LadderInfo ladderInfo { get; set; }
- public SeeingResultRow(TournamentTeam team, SeedingResult round)
+ public SeedingResultRow(TournamentTeam team, SeedingResult round)
{
Model = round;
@@ -281,6 +281,6 @@ namespace osu.Game.Tournament.Screens.Editors
}
}
- protected override SeeingResultRow CreateDrawable(SeedingResult model) => new SeeingResultRow(team, model);
+ protected override SeedingResultRow CreateDrawable(SeedingResult model) => new SeedingResultRow(team, model);
}
}
diff --git a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs
index b4c6d589d7..2c4fed8d86 100644
--- a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs
+++ b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs
@@ -42,7 +42,7 @@ namespace osu.Game.Tournament.Screens.MapPool
{
InternalChildren = new Drawable[]
{
- new TourneyVideo("gameplay")
+ new TourneyVideo("mappool")
{
Loop = true,
RelativeSizeAxes = Axes.Both,
diff --git a/osu.Game/Extensions/WebRequestExtensions.cs b/osu.Game/Extensions/WebRequestExtensions.cs
new file mode 100644
index 0000000000..b940c7498b
--- /dev/null
+++ b/osu.Game/Extensions/WebRequestExtensions.cs
@@ -0,0 +1,23 @@
+// 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.IO.Network;
+using osu.Framework.Extensions.IEnumerableExtensions;
+using osu.Game.Online.API.Requests;
+
+namespace osu.Game.Extensions
+{
+ public static class WebRequestExtensions
+ {
+ ///
+ /// Add a pagination cursor to the web request in the format required by osu-web.
+ ///
+ public static void AddCursor(this WebRequest webRequest, Cursor cursor)
+ {
+ cursor?.Properties.ForEach(x =>
+ {
+ webRequest.AddParameter("cursor[" + x.Key + "]", x.Value.ToString());
+ });
+ }
+ }
+}
diff --git a/osu.Game/Graphics/UserInterfaceV2/DirectorySelector.cs b/osu.Game/Graphics/UserInterfaceV2/DirectorySelector.cs
index ee428c0047..ae34281bfb 100644
--- a/osu.Game/Graphics/UserInterfaceV2/DirectorySelector.cs
+++ b/osu.Game/Graphics/UserInterfaceV2/DirectorySelector.cs
@@ -28,11 +28,11 @@ namespace osu.Game.Graphics.UserInterfaceV2
private GameHost host { get; set; }
[Cached]
- private readonly Bindable currentDirectory = new Bindable();
+ public readonly Bindable CurrentDirectory = new Bindable();
public DirectorySelector(string initialPath = null)
{
- currentDirectory.Value = new DirectoryInfo(initialPath ?? Environment.GetFolderPath(Environment.SpecialFolder.UserProfile));
+ CurrentDirectory.Value = new DirectoryInfo(initialPath ?? Environment.GetFolderPath(Environment.SpecialFolder.UserProfile));
}
[BackgroundDependencyLoader]
@@ -40,19 +40,25 @@ namespace osu.Game.Graphics.UserInterfaceV2
{
Padding = new MarginPadding(10);
- InternalChildren = new Drawable[]
+ InternalChild = new GridContainer
{
- new FillFlowContainer
+ RelativeSizeAxes = Axes.Both,
+ RowDimensions = new[]
{
- RelativeSizeAxes = Axes.Both,
- Direction = FillDirection.Vertical,
- Children = new Drawable[]
+ new Dimension(GridSizeMode.Absolute, 50),
+ new Dimension(),
+ },
+ Content = new[]
+ {
+ new Drawable[]
{
new CurrentDirectoryDisplay
{
- RelativeSizeAxes = Axes.X,
- Height = 50,
+ RelativeSizeAxes = Axes.Both,
},
+ },
+ new Drawable[]
+ {
new OsuScrollContainer
{
RelativeSizeAxes = Axes.Both,
@@ -65,10 +71,10 @@ namespace osu.Game.Graphics.UserInterfaceV2
}
}
}
- },
+ }
};
- currentDirectory.BindValueChanged(updateDisplay, true);
+ CurrentDirectory.BindValueChanged(updateDisplay, true);
}
private void updateDisplay(ValueChangedEvent directory)
@@ -86,9 +92,9 @@ namespace osu.Game.Graphics.UserInterfaceV2
}
else
{
- directoryFlow.Add(new ParentDirectoryPiece(currentDirectory.Value.Parent));
+ directoryFlow.Add(new ParentDirectoryPiece(CurrentDirectory.Value.Parent));
- foreach (var dir in currentDirectory.Value.GetDirectories().OrderBy(d => d.Name))
+ foreach (var dir in CurrentDirectory.Value.GetDirectories().OrderBy(d => d.Name))
{
if ((dir.Attributes & FileAttributes.Hidden) == 0)
directoryFlow.Add(new DirectoryPiece(dir));
@@ -97,8 +103,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
}
catch (Exception)
{
- currentDirectory.Value = directory.OldValue;
-
+ CurrentDirectory.Value = directory.OldValue;
this.FlashColour(Color4.Red, 300);
}
}
diff --git a/osu.Game/IO/OsuStorage.cs b/osu.Game/IO/OsuStorage.cs
index 71b01ce479..499bcb4063 100644
--- a/osu.Game/IO/OsuStorage.cs
+++ b/osu.Game/IO/OsuStorage.cs
@@ -48,11 +48,21 @@ namespace osu.Game.IO
var source = new DirectoryInfo(GetFullPath("."));
var destination = new DirectoryInfo(newLocation);
+ // using Uri is the easiest way to check equality and contains (https://stackoverflow.com/a/7710620)
+ var sourceUri = new Uri(source.FullName + Path.DirectorySeparatorChar);
+ var destinationUri = new Uri(destination.FullName + Path.DirectorySeparatorChar);
+
+ if (sourceUri == destinationUri)
+ throw new ArgumentException("Destination provided is already the current location", nameof(newLocation));
+
+ if (sourceUri.IsBaseOf(destinationUri))
+ throw new ArgumentException("Destination provided is inside the source", nameof(newLocation));
+
// ensure the new location has no files present, else hard abort
if (destination.Exists)
{
if (destination.GetFiles().Length > 0 || destination.GetDirectories().Length > 0)
- throw new InvalidOperationException("Migration destination already has files present");
+ throw new ArgumentException("Destination provided already has files or directories present", nameof(newLocation));
deleteRecursive(destination);
}
@@ -74,7 +84,7 @@ namespace osu.Game.IO
if (topLevelExcludes && IGNORE_FILES.Contains(fi.Name))
continue;
- fi.Delete();
+ attemptOperation(() => fi.Delete());
}
foreach (DirectoryInfo dir in target.GetDirectories())
@@ -82,8 +92,11 @@ namespace osu.Game.IO
if (topLevelExcludes && IGNORE_DIRECTORIES.Contains(dir.Name))
continue;
- dir.Delete(true);
+ attemptOperation(() => dir.Delete(true));
}
+
+ if (target.GetFiles().Length == 0 && target.GetDirectories().Length == 0)
+ attemptOperation(target.Delete);
}
private static void copyRecursive(DirectoryInfo source, DirectoryInfo destination, bool topLevelExcludes = true)
@@ -96,7 +109,7 @@ namespace osu.Game.IO
if (topLevelExcludes && IGNORE_FILES.Contains(fi.Name))
continue;
- attemptCopy(fi, Path.Combine(destination.FullName, fi.Name));
+ attemptOperation(() => fi.CopyTo(Path.Combine(destination.FullName, fi.Name), true));
}
foreach (DirectoryInfo dir in source.GetDirectories())
@@ -108,24 +121,27 @@ namespace osu.Game.IO
}
}
- private static void attemptCopy(System.IO.FileInfo fileInfo, string destination)
+ ///
+ /// Attempt an IO operation multiple times and only throw if none of the attempts succeed.
+ ///
+ /// The action to perform.
+ /// The number of attempts (250ms wait between each).
+ private static void attemptOperation(Action action, int attempts = 10)
{
- int tries = 5;
-
while (true)
{
try
{
- fileInfo.CopyTo(destination, true);
+ action();
return;
}
catch (Exception)
{
- if (tries-- == 0)
+ if (attempts-- == 0)
throw;
}
- Thread.Sleep(50);
+ Thread.Sleep(250);
}
}
}
diff --git a/osu.Game/Online/API/Requests/Cursor.cs b/osu.Game/Online/API/Requests/Cursor.cs
new file mode 100644
index 0000000000..f21445ca32
--- /dev/null
+++ b/osu.Game/Online/API/Requests/Cursor.cs
@@ -0,0 +1,20 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using JetBrains.Annotations;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+namespace osu.Game.Online.API.Requests
+{
+ ///
+ /// A collection of parameters which should be passed to the search endpoint to fetch the next page.
+ ///
+ public class Cursor
+ {
+ [UsedImplicitly]
+ [JsonExtensionData]
+ public IDictionary Properties;
+ }
+}
diff --git a/osu.Game/Online/API/Requests/ResponseWithCursor.cs b/osu.Game/Online/API/Requests/ResponseWithCursor.cs
index e38e73dd01..d52e999722 100644
--- a/osu.Game/Online/API/Requests/ResponseWithCursor.cs
+++ b/osu.Game/Online/API/Requests/ResponseWithCursor.cs
@@ -7,10 +7,7 @@ namespace osu.Game.Online.API.Requests
{
public abstract class ResponseWithCursor
{
- ///
- /// A collection of parameters which should be passed to the search endpoint to fetch the next page.
- ///
[JsonProperty("cursor")]
- public dynamic CursorJson;
+ public Cursor Cursor;
}
}
diff --git a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs
index 047496b473..0c3272c7de 100644
--- a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs
+++ b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.IO.Network;
+using osu.Game.Extensions;
using osu.Game.Overlays;
using osu.Game.Overlays.BeatmapListing;
using osu.Game.Rulesets;
@@ -10,29 +11,31 @@ namespace osu.Game.Online.API.Requests
{
public class SearchBeatmapSetsRequest : APIRequest
{
- public SearchCategory SearchCategory { get; set; }
+ public SearchCategory SearchCategory { get; }
- public SortCriteria SortCriteria { get; set; }
+ public SortCriteria SortCriteria { get; }
- public SortDirection SortDirection { get; set; }
+ public SortDirection SortDirection { get; }
- public SearchGenre Genre { get; set; }
+ public SearchGenre Genre { get; }
- public SearchLanguage Language { get; set; }
+ public SearchLanguage Language { get; }
private readonly string query;
private readonly RulesetInfo ruleset;
+ private readonly Cursor cursor;
private string directionString => SortDirection == SortDirection.Descending ? @"desc" : @"asc";
- public SearchBeatmapSetsRequest(string query, RulesetInfo ruleset)
+ public SearchBeatmapSetsRequest(string query, RulesetInfo ruleset, Cursor cursor = null, SearchCategory searchCategory = SearchCategory.Any, SortCriteria sortCriteria = SortCriteria.Ranked, SortDirection sortDirection = SortDirection.Descending)
{
this.query = string.IsNullOrEmpty(query) ? string.Empty : System.Uri.EscapeDataString(query);
this.ruleset = ruleset;
+ this.cursor = cursor;
- SearchCategory = SearchCategory.Any;
- SortCriteria = SortCriteria.Ranked;
- SortDirection = SortDirection.Descending;
+ SearchCategory = searchCategory;
+ SortCriteria = sortCriteria;
+ SortDirection = sortDirection;
Genre = SearchGenre.Any;
Language = SearchLanguage.Any;
}
@@ -55,6 +58,8 @@ namespace osu.Game.Online.API.Requests
req.AddParameter("sort", $"{SortCriteria.ToString().ToLowerInvariant()}_{directionString}");
+ req.AddCursor(cursor);
+
return req;
}
diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs
index 4dd60c7113..41c99d5d03 100644
--- a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs
+++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs
@@ -22,25 +22,46 @@ namespace osu.Game.Overlays.BeatmapListing
{
public class BeatmapListingFilterControl : CompositeDrawable
{
+ ///
+ /// Fired when a search finishes. Contains only new items in the case of pagination.
+ ///
public Action> SearchFinished;
+
+ ///
+ /// Fired when search criteria change.
+ ///
public Action SearchStarted;
+ ///
+ /// True when pagination has reached the end of available results.
+ ///
+ private bool noMoreResults;
+
+ ///
+ /// The current page fetched of results (zero index).
+ ///
+ public int CurrentPage { get; private set; }
+
+ private readonly BeatmapListingSearchControl searchControl;
+ private readonly BeatmapListingSortTabControl sortControl;
+ private readonly Box sortControlBackground;
+
+ private ScheduledDelegate queryChangedDebounce;
+
+ private SearchBeatmapSetsRequest getSetsRequest;
+ private SearchBeatmapSetsResponse lastResponse;
+
[Resolved]
private IAPIProvider api { get; set; }
[Resolved]
private RulesetStore rulesets { get; set; }
- private readonly BeatmapListingSearchControl searchControl;
- private readonly BeatmapListingSortTabControl sortControl;
- private readonly Box sortControlBackground;
-
- private SearchBeatmapSetsRequest getSetsRequest;
-
public BeatmapListingFilterControl()
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
+
InternalChild = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
@@ -114,51 +135,84 @@ namespace osu.Game.Overlays.BeatmapListing
sortDirection.BindValueChanged(_ => queueUpdateSearch());
}
- private ScheduledDelegate queryChangedDebounce;
+ public void TakeFocus() => searchControl.TakeFocus();
+
+ ///
+ /// Fetch the next page of results. May result in a no-op if a fetch is already in progress, or if there are no results left.
+ ///
+ public void FetchNextPage()
+ {
+ // there may be no results left.
+ if (noMoreResults)
+ return;
+
+ // there may already be an active request.
+ if (getSetsRequest != null)
+ return;
+
+ if (lastResponse != null)
+ CurrentPage++;
+
+ performRequest();
+ }
private void queueUpdateSearch(bool queryTextChanged = false)
{
SearchStarted?.Invoke();
- getSetsRequest?.Cancel();
+ resetSearch();
- queryChangedDebounce?.Cancel();
- queryChangedDebounce = Scheduler.AddDelayed(updateSearch, queryTextChanged ? 500 : 100);
+ queryChangedDebounce = Scheduler.AddDelayed(() =>
+ {
+ resetSearch();
+ FetchNextPage();
+ }, queryTextChanged ? 500 : 100);
}
- private void updateSearch()
+ private void performRequest()
{
- getSetsRequest = new SearchBeatmapSetsRequest(searchControl.Query.Value, searchControl.Ruleset.Value)
- {
- SearchCategory = searchControl.Category.Value,
- SortCriteria = sortControl.Current.Value,
- SortDirection = sortControl.SortDirection.Value,
- Genre = searchControl.Genre.Value,
- Language = searchControl.Language.Value
- };
+ getSetsRequest = new SearchBeatmapSetsRequest(
+ searchControl.Query.Value,
+ searchControl.Ruleset.Value,
+ lastResponse?.Cursor,
+ searchControl.Category.Value,
+ sortControl.Current.Value,
+ sortControl.SortDirection.Value);
- getSetsRequest.Success += response => Schedule(() => onSearchFinished(response));
+ getSetsRequest.Success += response =>
+ {
+ var sets = response.BeatmapSets.Select(responseJson => responseJson.ToBeatmapSet(rulesets)).ToList();
+
+ if (sets.Count == 0)
+ noMoreResults = true;
+
+ lastResponse = response;
+ getSetsRequest = null;
+
+ SearchFinished?.Invoke(sets);
+ };
api.Queue(getSetsRequest);
}
- private void onSearchFinished(SearchBeatmapSetsResponse response)
+ private void resetSearch()
{
- var beatmaps = response.BeatmapSets.Select(r => r.ToBeatmapSet(rulesets)).ToList();
+ noMoreResults = false;
+ CurrentPage = 0;
- searchControl.BeatmapSet = response.Total == 0 ? null : beatmaps.First();
+ lastResponse = null;
- SearchFinished?.Invoke(beatmaps);
+ getSetsRequest?.Cancel();
+ getSetsRequest = null;
+
+ queryChangedDebounce?.Cancel();
}
protected override void Dispose(bool isDisposing)
{
- getSetsRequest?.Cancel();
- queryChangedDebounce?.Cancel();
+ resetSearch();
base.Dispose(isDisposing);
}
-
- public void TakeFocus() => searchControl.TakeFocus();
}
}
diff --git a/osu.Game/Overlays/BeatmapListingOverlay.cs b/osu.Game/Overlays/BeatmapListingOverlay.cs
index f680f7c67b..225a8a0578 100644
--- a/osu.Game/Overlays/BeatmapListingOverlay.cs
+++ b/osu.Game/Overlays/BeatmapListingOverlay.cs
@@ -4,7 +4,9 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
+using System.Threading.Tasks;
using osu.Framework.Allocation;
+using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
@@ -30,6 +32,10 @@ namespace osu.Game.Overlays
private Drawable currentContent;
private LoadingLayer loadingLayer;
private Container panelTarget;
+ private FillFlowContainer foundContent;
+ private NotFoundDrawable notFoundContent;
+
+ private OverlayScrollContainer resultScrollContainer;
public BeatmapListingOverlay()
: base(OverlayColourScheme.Blue)
@@ -48,7 +54,7 @@ namespace osu.Game.Overlays
RelativeSizeAxes = Axes.Both,
Colour = ColourProvider.Background6
},
- new OverlayScrollContainer
+ resultScrollContainer = new OverlayScrollContainer
{
RelativeSizeAxes = Axes.Both,
ScrollbarVisible = false,
@@ -80,9 +86,14 @@ namespace osu.Game.Overlays
{
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
- Padding = new MarginPadding { Horizontal = 20 }
- },
- loadingLayer = new LoadingLayer(panelTarget)
+ Padding = new MarginPadding { Horizontal = 20 },
+ Children = new Drawable[]
+ {
+ foundContent = new FillFlowContainer(),
+ notFoundContent = new NotFoundDrawable(),
+ loadingLayer = new LoadingLayer(panelTarget)
+ }
+ }
}
},
}
@@ -110,34 +121,53 @@ namespace osu.Game.Overlays
loadingLayer.Show();
}
+ private Task panelLoadDelegate;
+
private void onSearchFinished(List beatmaps)
{
- if (!beatmaps.Any())
+ var newPanels = beatmaps.Select(b => new GridBeatmapPanel(b)
{
- LoadComponentAsync(new NotFoundDrawable(), addContentToPlaceholder, (cancellationToken = new CancellationTokenSource()).Token);
- return;
- }
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ });
- var newPanels = new FillFlowContainer
+ if (filterControl.CurrentPage == 0)
{
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- Spacing = new Vector2(10),
- Alpha = 0,
- Margin = new MarginPadding { Vertical = 15 },
- ChildrenEnumerable = beatmaps.Select(b => new GridBeatmapPanel(b)
+ //No matches case
+ if (!newPanels.Any())
{
- Anchor = Anchor.TopCentre,
- Origin = Anchor.TopCentre,
- })
- };
+ LoadComponentAsync(notFoundContent, addContentToPlaceholder, (cancellationToken = new CancellationTokenSource()).Token);
+ return;
+ }
- LoadComponentAsync(newPanels, addContentToPlaceholder, (cancellationToken = new CancellationTokenSource()).Token);
+ // spawn new children with the contained so we only clear old content at the last moment.
+ var content = new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Spacing = new Vector2(10),
+ Alpha = 0,
+ Margin = new MarginPadding { Vertical = 15 },
+ ChildrenEnumerable = newPanels
+ };
+
+ panelLoadDelegate = LoadComponentAsync(foundContent = content, addContentToPlaceholder, (cancellationToken = new CancellationTokenSource()).Token);
+ }
+ else
+ {
+ panelLoadDelegate = LoadComponentsAsync(newPanels, loaded =>
+ {
+ lastFetchDisplayedTime = Time.Current;
+ foundContent.AddRange(loaded);
+ loaded.ForEach(p => p.FadeIn(200, Easing.OutQuint));
+ });
+ }
}
private void addContentToPlaceholder(Drawable content)
{
loadingLayer.Hide();
+ lastFetchDisplayedTime = Time.Current;
var lastContent = currentContent;
@@ -149,11 +179,14 @@ namespace osu.Game.Overlays
// If the auto-size computation is delayed until fade out completes, the background remain high for too long making the resulting transition to the smaller height look weird.
// At the same time, if the last content's height is bypassed immediately, there is a period where the new content is at Alpha = 0 when the auto-sized height will be 0.
// To resolve both of these issues, the bypass is delayed until a point when the content transitions (fade-in and fade-out) overlap and it looks good to do so.
- lastContent.Delay(25).Schedule(() => lastContent.BypassAutoSizeAxes = Axes.Y);
+ lastContent.Delay(25).Schedule(() => lastContent.BypassAutoSizeAxes = Axes.Y).Then().Schedule(() => panelTarget.Remove(lastContent));
}
- panelTarget.Add(currentContent = content);
- currentContent.FadeIn(200, Easing.OutQuint);
+ if (!content.IsAlive)
+ panelTarget.Add(content);
+ content.FadeIn(200, Easing.OutQuint);
+
+ currentContent = content;
}
protected override void Dispose(bool isDisposing)
@@ -203,5 +236,23 @@ namespace osu.Game.Overlays
});
}
}
+
+ private const double time_between_fetches = 500;
+
+ private double lastFetchDisplayedTime;
+
+ protected override void Update()
+ {
+ base.Update();
+
+ const int pagination_scroll_distance = 500;
+
+ bool shouldShowMore = panelLoadDelegate?.IsCompleted != false
+ && Time.Current - lastFetchDisplayedTime > time_between_fetches
+ && (resultScrollContainer.ScrollableExtent > 0 && resultScrollContainer.IsScrolledToEnd(pagination_scroll_distance));
+
+ if (shouldShowMore)
+ filterControl.FetchNextPage();
+ }
}
}
diff --git a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs
index 188c9c05ef..95a1868392 100644
--- a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs
@@ -4,7 +4,9 @@
using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Platform;
+using osu.Framework.Screens;
using osu.Game.Configuration;
+using osu.Game.Overlays.Settings.Sections.Maintenance;
namespace osu.Game.Overlays.Settings.Sections.General
{
@@ -12,8 +14,8 @@ namespace osu.Game.Overlays.Settings.Sections.General
{
protected override string Header => "Updates";
- [BackgroundDependencyLoader]
- private void load(Storage storage, OsuConfigManager config)
+ [BackgroundDependencyLoader(true)]
+ private void load(Storage storage, OsuConfigManager config, OsuGame game)
{
Add(new SettingsEnumDropdown
{
@@ -28,6 +30,12 @@ namespace osu.Game.Overlays.Settings.Sections.General
Text = "Open osu! folder",
Action = storage.OpenInNativeExplorer,
});
+
+ Add(new SettingsButton
+ {
+ Text = "Change folder location...",
+ Action = () => game?.PerformFromScreen(menu => menu.Push(new MigrationSelectScreen()))
+ });
}
}
}
diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationRunScreen.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationRunScreen.cs
new file mode 100644
index 0000000000..b0b61554eb
--- /dev/null
+++ b/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationRunScreen.cs
@@ -0,0 +1,115 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.IO;
+using System.Threading.Tasks;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Logging;
+using osu.Framework.Screens;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Screens;
+using osuTK;
+
+namespace osu.Game.Overlays.Settings.Sections.Maintenance
+{
+ public class MigrationRunScreen : OsuScreen
+ {
+ private readonly DirectoryInfo destination;
+
+ [Resolved(canBeNull: true)]
+ private OsuGame game { get; set; }
+
+ public override bool AllowBackButton => false;
+
+ public override bool AllowExternalScreenChange => false;
+
+ public override bool DisallowExternalBeatmapRulesetChanges => true;
+
+ public override bool HideOverlaysOnEnter => true;
+
+ private Task migrationTask;
+
+ public MigrationRunScreen(DirectoryInfo destination)
+ {
+ this.destination = destination;
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ InternalChildren = new Drawable[]
+ {
+ new FillFlowContainer
+ {
+ AutoSizeAxes = Axes.Both,
+ Direction = FillDirection.Vertical,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Spacing = new Vector2(10),
+ Children = new Drawable[]
+ {
+ new OsuSpriteText
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Text = "Migration in progress",
+ Font = OsuFont.Default.With(size: 40)
+ },
+ new OsuSpriteText
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Text = "This could take a few minutes depending on the speed of your disk(s).",
+ Font = OsuFont.Default.With(size: 30)
+ },
+ new LoadingSpinner(true)
+ {
+ State = { Value = Visibility.Visible }
+ },
+ new OsuSpriteText
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Text = "Please avoid interacting with the game!",
+ Font = OsuFont.Default.With(size: 30)
+ },
+ }
+ },
+ };
+
+ Beatmap.Value = Beatmap.Default;
+
+ migrationTask = Task.Run(PerformMigration)
+ .ContinueWith(t =>
+ {
+ if (t.IsFaulted)
+ Logger.Log($"Error during migration: {t.Exception?.Message}", level: LogLevel.Error);
+
+ Schedule(this.Exit);
+ });
+ }
+
+ protected virtual void PerformMigration() => game?.Migrate(destination.FullName);
+
+ public override void OnEntering(IScreen last)
+ {
+ base.OnEntering(last);
+
+ this.FadeOut().Delay(250).Then().FadeIn(250);
+ }
+
+ public override bool OnExiting(IScreen next)
+ {
+ // block until migration is finished
+ if (migrationTask?.IsCompleted == false)
+ return true;
+
+ return base.OnExiting(next);
+ }
+ }
+}
diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationSelectScreen.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationSelectScreen.cs
new file mode 100644
index 0000000000..79d842a617
--- /dev/null
+++ b/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationSelectScreen.cs
@@ -0,0 +1,128 @@
+// 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.IO;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Logging;
+using osu.Framework.Platform;
+using osu.Framework.Screens;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Graphics.UserInterfaceV2;
+using osu.Game.Screens;
+using osuTK;
+
+namespace osu.Game.Overlays.Settings.Sections.Maintenance
+{
+ public class MigrationSelectScreen : OsuScreen
+ {
+ private DirectorySelector directorySelector;
+
+ public override bool AllowExternalScreenChange => false;
+
+ public override bool DisallowExternalBeatmapRulesetChanges => true;
+
+ public override bool HideOverlaysOnEnter => true;
+
+ [BackgroundDependencyLoader(true)]
+ private void load(OsuGame game, Storage storage, OsuColour colours)
+ {
+ game?.Toolbar.Hide();
+
+ // begin selection in the parent directory of the current storage location
+ var initialPath = new DirectoryInfo(storage.GetFullPath(string.Empty)).Parent?.FullName;
+
+ InternalChild = new Container
+ {
+ Masking = true,
+ CornerRadius = 10,
+ RelativeSizeAxes = Axes.Both,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Size = new Vector2(0.5f, 0.8f),
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ Colour = colours.GreySeafoamDark,
+ RelativeSizeAxes = Axes.Both,
+ },
+ new GridContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ RowDimensions = new[]
+ {
+ new Dimension(),
+ new Dimension(GridSizeMode.Relative, 0.8f),
+ new Dimension(),
+ },
+ Content = new[]
+ {
+ new Drawable[]
+ {
+ new OsuSpriteText
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Text = "Please select a new location",
+ Font = OsuFont.Default.With(size: 40)
+ },
+ },
+ new Drawable[]
+ {
+ directorySelector = new DirectorySelector(initialPath)
+ {
+ RelativeSizeAxes = Axes.Both,
+ }
+ },
+ new Drawable[]
+ {
+ new TriangleButton
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Width = 300,
+ Text = "Begin folder migration",
+ Action = start
+ },
+ }
+ }
+ }
+ }
+ };
+ }
+
+ public override void OnSuspending(IScreen next)
+ {
+ base.OnSuspending(next);
+
+ this.FadeOut(250);
+ }
+
+ private void start()
+ {
+ var target = directorySelector.CurrentDirectory.Value;
+
+ try
+ {
+ if (target.GetDirectories().Length > 0 || target.GetFiles().Length > 0)
+ target = target.CreateSubdirectory("osu-lazer");
+ }
+ catch (Exception e)
+ {
+ Logger.Log($"Error during migration: {e.Message}", level: LogLevel.Error);
+ return;
+ }
+
+ ValidForResume = false;
+ BeginMigration(target);
+ }
+
+ protected virtual void BeginMigration(DirectoryInfo target) => this.Push(new MigrationRunScreen(target));
+ }
+}
diff --git a/osu.Game/Overlays/VolumeOverlay.cs b/osu.Game/Overlays/VolumeOverlay.cs
index 676d2c941a..eb639431ae 100644
--- a/osu.Game/Overlays/VolumeOverlay.cs
+++ b/osu.Game/Overlays/VolumeOverlay.cs
@@ -46,6 +46,13 @@ namespace osu.Game.Overlays
Width = 300,
Colour = ColourInfo.GradientHorizontal(Color4.Black.Opacity(0.75f), Color4.Black.Opacity(0))
},
+ muteButton = new MuteButton
+ {
+ Anchor = Anchor.BottomLeft,
+ Origin = Anchor.BottomLeft,
+ Margin = new MarginPadding(10),
+ Current = { BindTarget = IsMuted }
+ },
new FillFlowContainer
{
Direction = FillDirection.Vertical,
@@ -56,19 +63,11 @@ namespace osu.Game.Overlays
Margin = new MarginPadding { Left = offset },
Children = new Drawable[]
{
- volumeMeterEffect = new VolumeMeter("EFFECTS", 125, colours.BlueDarker)
- {
- Margin = new MarginPadding { Top = 100 + MuteButton.HEIGHT } // to counter the mute button and re-center the volume meters
- },
+ volumeMeterEffect = new VolumeMeter("EFFECTS", 125, colours.BlueDarker),
volumeMeterMaster = new VolumeMeter("MASTER", 150, colours.PinkDarker),
volumeMeterMusic = new VolumeMeter("MUSIC", 125, colours.BlueDarker),
- muteButton = new MuteButton
- {
- Margin = new MarginPadding { Top = 100 },
- Current = { BindTarget = IsMuted }
- }
}
- },
+ }
});
volumeMeterMaster.Bindable.BindTo(audio.Volume);
diff --git a/osu.Game/Rulesets/Mods/IApplicableFailOverride.cs b/osu.Game/Rulesets/Mods/IApplicableFailOverride.cs
index 120bfc9a23..8c99d739cb 100644
--- a/osu.Game/Rulesets/Mods/IApplicableFailOverride.cs
+++ b/osu.Game/Rulesets/Mods/IApplicableFailOverride.cs
@@ -11,10 +11,11 @@ namespace osu.Game.Rulesets.Mods
///
/// Whether we should allow failing at the current point in time.
///
- bool AllowFail { get; }
+ /// Whether the fail should be allowed to proceed. Return false to block.
+ bool PerformFail();
///
- /// Whether we want to restart on fail. Only used if is true.
+ /// Whether we want to restart on fail. Only used if returns true.
///
bool RestartOnFail { get; }
}
diff --git a/osu.Game/Rulesets/Mods/ModAutoplay.cs b/osu.Game/Rulesets/Mods/ModAutoplay.cs
index e51b8b6457..945dd444be 100644
--- a/osu.Game/Rulesets/Mods/ModAutoplay.cs
+++ b/osu.Game/Rulesets/Mods/ModAutoplay.cs
@@ -27,7 +27,8 @@ namespace osu.Game.Rulesets.Mods
public override string Description => "Watch a perfect automated play through the song.";
public override double ScoreMultiplier => 1;
- public bool AllowFail => false;
+ public bool PerformFail() => false;
+
public bool RestartOnFail => false;
public override Type[] IncompatibleMods => new[] { typeof(ModRelax), typeof(ModSuddenDeath), typeof(ModNoFail) };
diff --git a/osu.Game/Rulesets/Mods/ModBlockFail.cs b/osu.Game/Rulesets/Mods/ModBlockFail.cs
index 7d7ecfa416..1fde5abad4 100644
--- a/osu.Game/Rulesets/Mods/ModBlockFail.cs
+++ b/osu.Game/Rulesets/Mods/ModBlockFail.cs
@@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Mods
///
/// We never fail, 'yo.
///
- public bool AllowFail => false;
+ public bool PerformFail() => false;
public bool RestartOnFail => false;
diff --git a/osu.Game/Rulesets/Mods/ModEasy.cs b/osu.Game/Rulesets/Mods/ModEasy.cs
index c1c4124b98..7cf9656810 100644
--- a/osu.Game/Rulesets/Mods/ModEasy.cs
+++ b/osu.Game/Rulesets/Mods/ModEasy.cs
@@ -48,17 +48,14 @@ namespace osu.Game.Rulesets.Mods
retries = Retries.Value;
}
- public bool AllowFail
+ public bool PerformFail()
{
- get
- {
- if (retries == 0) return true;
+ if (retries == 0) return true;
- health.Value = health.MaxValue;
- retries--;
+ health.Value = health.MaxValue;
+ retries--;
- return false;
- }
+ return false;
}
public bool RestartOnFail => false;
diff --git a/osu.Game/Rulesets/Mods/ModSuddenDeath.cs b/osu.Game/Rulesets/Mods/ModSuddenDeath.cs
index 8799431f1d..df10262845 100644
--- a/osu.Game/Rulesets/Mods/ModSuddenDeath.cs
+++ b/osu.Game/Rulesets/Mods/ModSuddenDeath.cs
@@ -20,7 +20,8 @@ namespace osu.Game.Rulesets.Mods
public override bool Ranked => true;
public override Type[] IncompatibleMods => new[] { typeof(ModNoFail), typeof(ModRelax), typeof(ModAutoplay) };
- public bool AllowFail => true;
+ public bool PerformFail() => true;
+
public bool RestartOnFail => true;
public void ApplyToHealthProcessor(HealthProcessor healthProcessor)
diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs
index 591e969ad8..2f85d6ad1e 100644
--- a/osu.Game/Screens/Play/GameplayClockContainer.cs
+++ b/osu.Game/Screens/Play/GameplayClockContainer.cs
@@ -78,10 +78,10 @@ namespace osu.Game.Screens.Play
// Lazer's audio timings in general doesn't match stable. This is the result of user testing, albeit limited.
// This only seems to be required on windows. We need to eventually figure out why, with a bit of luck.
- platformOffsetClock = new FramedOffsetClock(adjustableClock) { Offset = RuntimeInfo.OS == RuntimeInfo.Platform.Windows ? 15 : 0 };
+ platformOffsetClock = new HardwareCorrectionOffsetClock(adjustableClock) { Offset = RuntimeInfo.OS == RuntimeInfo.Platform.Windows ? 15 : 0 };
// the final usable gameplay clock with user-set offsets applied.
- userOffsetClock = new FramedOffsetClock(platformOffsetClock);
+ userOffsetClock = new HardwareCorrectionOffsetClock(platformOffsetClock);
// the clock to be exposed via DI to children.
GameplayClock = new GameplayClock(userOffsetClock);
@@ -248,5 +248,16 @@ namespace osu.Game.Screens.Play
speedAdjustmentsApplied = false;
}
}
+
+ private class HardwareCorrectionOffsetClock : FramedOffsetClock
+ {
+ // we always want to apply the same real-time offset, so it should be adjusted by the playback rate to achieve this.
+ public override double CurrentTime => SourceTime + Offset * Rate;
+
+ public HardwareCorrectionOffsetClock(IClock source, bool processSource = true)
+ : base(source, processSource)
+ {
+ }
+ }
}
}
diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs
index a2735c8c55..1ec3a69b24 100644
--- a/osu.Game/Screens/Play/Player.cs
+++ b/osu.Game/Screens/Play/Player.cs
@@ -105,7 +105,7 @@ namespace osu.Game.Screens.Play
/// Whether failing should be allowed.
/// By default, this checks whether all selected mods allow failing.
///
- protected virtual bool AllowFail => Mods.Value.OfType().All(m => m.AllowFail);
+ protected virtual bool CheckModsAllowFailure() => Mods.Value.OfType().All(m => m.PerformFail());
private readonly bool allowPause;
private readonly bool showResults;
@@ -485,7 +485,7 @@ namespace osu.Game.Screens.Play
private bool onFail()
{
- if (!AllowFail)
+ if (!CheckModsAllowFailure())
return false;
HasFailed = true;
diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs
index 0d2ddb7b01..f0c76163f1 100644
--- a/osu.Game/Screens/Play/ReplayPlayer.cs
+++ b/osu.Game/Screens/Play/ReplayPlayer.cs
@@ -12,7 +12,7 @@ namespace osu.Game.Screens.Play
private readonly Score score;
// Disallow replays from failing. (see https://github.com/ppy/osu/issues/6108)
- protected override bool AllowFail => false;
+ protected override bool CheckModsAllowFailure() => false;
public ReplayPlayer(Score score, bool allowPause = true, bool showResults = true)
: base(allowPause, showResults)
diff --git a/osu.Game/Tests/Visual/ModPerfectTestScene.cs b/osu.Game/Tests/Visual/ModPerfectTestScene.cs
index 5948283428..95a62bbf65 100644
--- a/osu.Game/Tests/Visual/ModPerfectTestScene.cs
+++ b/osu.Game/Tests/Visual/ModPerfectTestScene.cs
@@ -41,7 +41,7 @@ namespace osu.Game.Tests.Visual
{
}
- protected override bool AllowFail => true;
+ protected override bool CheckModsAllowFailure() => true;
public bool CheckFailed(bool failed)
{
diff --git a/osu.Game/Tests/Visual/ModTestScene.cs b/osu.Game/Tests/Visual/ModTestScene.cs
index 1fa638b3d8..d36a7d8bad 100644
--- a/osu.Game/Tests/Visual/ModTestScene.cs
+++ b/osu.Game/Tests/Visual/ModTestScene.cs
@@ -59,12 +59,14 @@ namespace osu.Game.Tests.Visual
protected class ModTestPlayer : TestPlayer
{
- protected override bool AllowFail { get; }
+ private readonly bool allowFail;
+
+ protected override bool CheckModsAllowFailure() => allowFail;
public ModTestPlayer(bool allowFail)
: base(false, false)
{
- AllowFail = allowFail;
+ this.allowFail = allowFail;
}
}
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index c6dba8da13..ee6206e166 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -25,7 +25,7 @@
-
+
diff --git a/osu.iOS.props b/osu.iOS.props
index f78fd2e4ff..cbf8600c62 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -71,7 +71,7 @@
-
+