1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-15 01:02:55 +08:00

Merge branch 'master' into remove-requiredtypes

This commit is contained in:
Dean Herbert 2020-05-16 18:28:14 +09:00
commit be3a0a3c1d
35 changed files with 712 additions and 165 deletions

View File

@ -51,7 +51,7 @@
<Reference Include="Java.Interop" /> <Reference Include="Java.Interop" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.427.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2020.512.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.511.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2020.511.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -24,7 +24,8 @@ namespace osu.Game.Rulesets.Osu.Mods
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(OsuModSpunOut), typeof(ModRelax), typeof(ModSuddenDeath), typeof(ModNoFail), typeof(ModAutoplay) }; 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; public bool RestartOnFail => false;
private OsuInputManager inputManager; private OsuInputManager inputManager;

View File

@ -3,6 +3,7 @@
using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Timing;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Skinning; using osu.Game.Rulesets.Taiko.Skinning;
@ -12,11 +13,30 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
{ {
public class TestSceneTaikoScroller : TaikoSkinnableTestScene public class TestSceneTaikoScroller : TaikoSkinnableTestScene
{ {
private readonly ManualClock clock = new ManualClock();
private bool reversed;
public TestSceneTaikoScroller() 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<LegacyTaikoScroller>().ForEach(s => s.LastResult.Value = AddToggleStep("Toggle passing", passing => this.ChildrenOfType<LegacyTaikoScroller>().ForEach(s => s.LastResult.Value =
new JudgementResult(null, new Judgement()) { Type = passing ? HitResult.Perfect : HitResult.Miss })); 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;
} }
} }
} }

View File

@ -17,6 +17,8 @@ namespace osu.Game.Rulesets.Taiko.Skinning
{ {
public class LegacyTaikoScroller : CompositeDrawable public class LegacyTaikoScroller : CompositeDrawable
{ {
public Bindable<JudgementResult> LastResult = new Bindable<JudgementResult>();
public LegacyTaikoScroller() public LegacyTaikoScroller()
{ {
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
@ -50,37 +52,38 @@ namespace osu.Game.Rulesets.Taiko.Skinning
}, true); }, true);
} }
public Bindable<JudgementResult> LastResult = new Bindable<JudgementResult>();
protected override void Update() protected override void Update()
{ {
base.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;
float? additiveX = null;
// 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) foreach (var sprite in InternalChildren)
{ {
// add the x coordinates and perform re-layout on all sprites as spacing may change with gameplay scale. // 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; sprite.X = currentX;
currentX += sprite.DrawWidth;
additiveX += sprite.DrawWidth - 1;
if (sprite.X + sprite.DrawWidth < 0)
sprite.Expire();
} }
var last = InternalChildren.LastOrDefault(); if (first.ScreenSpaceDrawQuad.TopLeft.X >= ScreenSpaceDrawQuad.TopLeft.X)
// 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 foreach (var internalChild in InternalChildren)
}); internalChild.X -= first.DrawWidth;
}
if (last.ScreenSpaceDrawQuad.TopRight.X <= ScreenSpaceDrawQuad.TopRight.X)
{
foreach (var internalChild in InternalChildren)
internalChild.X += first.DrawWidth;
} }
} }

View File

@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Taiko.UI
{ {
new BarLineGenerator<BarLine>(Beatmap).BarLines.ForEach(bar => Playfield.Add(bar.Major ? new DrawableBarLineMajor(bar) : new DrawableBarLine(bar))); new BarLineGenerator<BarLine>(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, RelativeSizeAxes = Axes.X,
Depth = float.MaxValue Depth = float.MaxValue

View File

@ -13,18 +13,16 @@ namespace osu.Game.Rulesets.Taiko.UI
private const float default_relative_height = TaikoPlayfield.DEFAULT_HEIGHT / 768; private const float default_relative_height = TaikoPlayfield.DEFAULT_HEIGHT / 768;
private const float default_aspect = 16f / 9f; private const float default_aspect = 16f / 9f;
public TaikoPlayfieldAdjustmentContainer()
{
Anchor = Anchor.CentreLeft;
Origin = Anchor.CentreLeft;
}
protected override void Update() protected override void Update()
{ {
base.Update(); base.Update();
float aspectAdjust = Math.Clamp(Parent.ChildSize.X / Parent.ChildSize.Y, 0.4f, 4) / default_aspect; float aspectAdjust = Math.Clamp(Parent.ChildSize.X / Parent.ChildSize.Y, 0.4f, 4) / default_aspect;
Size = new Vector2(1, default_relative_height * aspectAdjust); 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;
} }
} }
} }

View File

@ -211,7 +211,61 @@ namespace osu.Game.Tests.NonVisual
var osu = loadOsu(host); var osu = loadOsu(host);
Assert.DoesNotThrow(() => osu.Migrate(customPath)); Assert.DoesNotThrow(() => osu.Migrate(customPath));
Assert.Throws<InvalidOperationException>(() => osu.Migrate(customPath)); Assert.Throws<ArgumentException>(() => 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<ArgumentException>(() => 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 finally
{ {

View File

@ -33,7 +33,8 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
public new ScoreProcessor ScoreProcessor => base.ScoreProcessor; public new ScoreProcessor ScoreProcessor => base.ScoreProcessor;
public new HUDOverlay HUDOverlay => base.HUDOverlay; public new HUDOverlay HUDOverlay => base.HUDOverlay;
public new bool AllowFail => base.AllowFail;
public bool AllowFail => base.CheckModsAllowFailure();
protected override bool PauseOnFocusLost => false; protected override bool PauseOnFocusLost => false;

View File

@ -0,0 +1,36 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.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)
{
}
}
}
}
}

View File

@ -19,7 +19,7 @@ using osuTK;
namespace osu.Game.Tournament.Screens.Editors namespace osu.Game.Tournament.Screens.Editors
{ {
public class SeedingEditorScreen : TournamentEditorScreen<SeedingEditorScreen.SeeingResultRow, SeedingResult> public class SeedingEditorScreen : TournamentEditorScreen<SeedingEditorScreen.SeedingResultRow, SeedingResult>
{ {
private readonly TournamentTeam team; private readonly TournamentTeam team;
@ -30,14 +30,14 @@ namespace osu.Game.Tournament.Screens.Editors
this.team = team; this.team = team;
} }
public class SeeingResultRow : CompositeDrawable, IModelBacked<SeedingResult> public class SeedingResultRow : CompositeDrawable, IModelBacked<SeedingResult>
{ {
public SeedingResult Model { get; } public SeedingResult Model { get; }
[Resolved] [Resolved]
private LadderInfo ladderInfo { get; set; } private LadderInfo ladderInfo { get; set; }
public SeeingResultRow(TournamentTeam team, SeedingResult round) public SeedingResultRow(TournamentTeam team, SeedingResult round)
{ {
Model = 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);
} }
} }

View File

@ -42,7 +42,7 @@ namespace osu.Game.Tournament.Screens.MapPool
{ {
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
new TourneyVideo("gameplay") new TourneyVideo("mappool")
{ {
Loop = true, Loop = true,
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,

View File

@ -0,0 +1,23 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.IO.Network;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Game.Online.API.Requests;
namespace osu.Game.Extensions
{
public static class WebRequestExtensions
{
/// <summary>
/// Add a pagination cursor to the web request in the format required by osu-web.
/// </summary>
public static void AddCursor(this WebRequest webRequest, Cursor cursor)
{
cursor?.Properties.ForEach(x =>
{
webRequest.AddParameter("cursor[" + x.Key + "]", x.Value.ToString());
});
}
}
}

View File

@ -28,11 +28,11 @@ namespace osu.Game.Graphics.UserInterfaceV2
private GameHost host { get; set; } private GameHost host { get; set; }
[Cached] [Cached]
private readonly Bindable<DirectoryInfo> currentDirectory = new Bindable<DirectoryInfo>(); public readonly Bindable<DirectoryInfo> CurrentDirectory = new Bindable<DirectoryInfo>();
public DirectorySelector(string initialPath = null) 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] [BackgroundDependencyLoader]
@ -40,19 +40,25 @@ namespace osu.Game.Graphics.UserInterfaceV2
{ {
Padding = new MarginPadding(10); Padding = new MarginPadding(10);
InternalChildren = new Drawable[] InternalChild = new GridContainer
{
new FillFlowContainer
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Direction = FillDirection.Vertical, RowDimensions = new[]
Children = new Drawable[] {
new Dimension(GridSizeMode.Absolute, 50),
new Dimension(),
},
Content = new[]
{
new Drawable[]
{ {
new CurrentDirectoryDisplay new CurrentDirectoryDisplay
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.Both,
Height = 50,
}, },
},
new Drawable[]
{
new OsuScrollContainer new OsuScrollContainer
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
@ -65,10 +71,10 @@ namespace osu.Game.Graphics.UserInterfaceV2
} }
} }
} }
}, }
}; };
currentDirectory.BindValueChanged(updateDisplay, true); CurrentDirectory.BindValueChanged(updateDisplay, true);
} }
private void updateDisplay(ValueChangedEvent<DirectoryInfo> directory) private void updateDisplay(ValueChangedEvent<DirectoryInfo> directory)
@ -86,9 +92,9 @@ namespace osu.Game.Graphics.UserInterfaceV2
} }
else 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) if ((dir.Attributes & FileAttributes.Hidden) == 0)
directoryFlow.Add(new DirectoryPiece(dir)); directoryFlow.Add(new DirectoryPiece(dir));
@ -97,8 +103,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
} }
catch (Exception) catch (Exception)
{ {
currentDirectory.Value = directory.OldValue; CurrentDirectory.Value = directory.OldValue;
this.FlashColour(Color4.Red, 300); this.FlashColour(Color4.Red, 300);
} }
} }

View File

@ -48,11 +48,21 @@ namespace osu.Game.IO
var source = new DirectoryInfo(GetFullPath(".")); var source = new DirectoryInfo(GetFullPath("."));
var destination = new DirectoryInfo(newLocation); 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 // ensure the new location has no files present, else hard abort
if (destination.Exists) if (destination.Exists)
{ {
if (destination.GetFiles().Length > 0 || destination.GetDirectories().Length > 0) 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); deleteRecursive(destination);
} }
@ -74,7 +84,7 @@ namespace osu.Game.IO
if (topLevelExcludes && IGNORE_FILES.Contains(fi.Name)) if (topLevelExcludes && IGNORE_FILES.Contains(fi.Name))
continue; continue;
fi.Delete(); attemptOperation(() => fi.Delete());
} }
foreach (DirectoryInfo dir in target.GetDirectories()) foreach (DirectoryInfo dir in target.GetDirectories())
@ -82,8 +92,11 @@ namespace osu.Game.IO
if (topLevelExcludes && IGNORE_DIRECTORIES.Contains(dir.Name)) if (topLevelExcludes && IGNORE_DIRECTORIES.Contains(dir.Name))
continue; 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) 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)) if (topLevelExcludes && IGNORE_FILES.Contains(fi.Name))
continue; 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()) foreach (DirectoryInfo dir in source.GetDirectories())
@ -108,24 +121,27 @@ namespace osu.Game.IO
} }
} }
private static void attemptCopy(System.IO.FileInfo fileInfo, string destination) /// <summary>
/// Attempt an IO operation multiple times and only throw if none of the attempts succeed.
/// </summary>
/// <param name="action">The action to perform.</param>
/// <param name="attempts">The number of attempts (250ms wait between each).</param>
private static void attemptOperation(Action action, int attempts = 10)
{ {
int tries = 5;
while (true) while (true)
{ {
try try
{ {
fileInfo.CopyTo(destination, true); action();
return; return;
} }
catch (Exception) catch (Exception)
{ {
if (tries-- == 0) if (attempts-- == 0)
throw; throw;
} }
Thread.Sleep(50); Thread.Sleep(250);
} }
} }
} }

View File

@ -0,0 +1,20 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using JetBrains.Annotations;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace osu.Game.Online.API.Requests
{
/// <summary>
/// A collection of parameters which should be passed to the search endpoint to fetch the next page.
/// </summary>
public class Cursor
{
[UsedImplicitly]
[JsonExtensionData]
public IDictionary<string, JToken> Properties;
}
}

View File

@ -7,10 +7,7 @@ namespace osu.Game.Online.API.Requests
{ {
public abstract class ResponseWithCursor public abstract class ResponseWithCursor
{ {
/// <summary>
/// A collection of parameters which should be passed to the search endpoint to fetch the next page.
/// </summary>
[JsonProperty("cursor")] [JsonProperty("cursor")]
public dynamic CursorJson; public Cursor Cursor;
} }
} }

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.IO.Network; using osu.Framework.IO.Network;
using osu.Game.Extensions;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.BeatmapListing; using osu.Game.Overlays.BeatmapListing;
using osu.Game.Rulesets; using osu.Game.Rulesets;
@ -10,29 +11,31 @@ namespace osu.Game.Online.API.Requests
{ {
public class SearchBeatmapSetsRequest : APIRequest<SearchBeatmapSetsResponse> public class SearchBeatmapSetsRequest : APIRequest<SearchBeatmapSetsResponse>
{ {
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 string query;
private readonly RulesetInfo ruleset; private readonly RulesetInfo ruleset;
private readonly Cursor cursor;
private string directionString => SortDirection == SortDirection.Descending ? @"desc" : @"asc"; 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.query = string.IsNullOrEmpty(query) ? string.Empty : System.Uri.EscapeDataString(query);
this.ruleset = ruleset; this.ruleset = ruleset;
this.cursor = cursor;
SearchCategory = SearchCategory.Any; SearchCategory = searchCategory;
SortCriteria = SortCriteria.Ranked; SortCriteria = sortCriteria;
SortDirection = SortDirection.Descending; SortDirection = sortDirection;
Genre = SearchGenre.Any; Genre = SearchGenre.Any;
Language = SearchLanguage.Any; Language = SearchLanguage.Any;
} }
@ -55,6 +58,8 @@ namespace osu.Game.Online.API.Requests
req.AddParameter("sort", $"{SortCriteria.ToString().ToLowerInvariant()}_{directionString}"); req.AddParameter("sort", $"{SortCriteria.ToString().ToLowerInvariant()}_{directionString}");
req.AddCursor(cursor);
return req; return req;
} }

View File

@ -22,25 +22,46 @@ namespace osu.Game.Overlays.BeatmapListing
{ {
public class BeatmapListingFilterControl : CompositeDrawable public class BeatmapListingFilterControl : CompositeDrawable
{ {
/// <summary>
/// Fired when a search finishes. Contains only new items in the case of pagination.
/// </summary>
public Action<List<BeatmapSetInfo>> SearchFinished; public Action<List<BeatmapSetInfo>> SearchFinished;
/// <summary>
/// Fired when search criteria change.
/// </summary>
public Action SearchStarted; public Action SearchStarted;
/// <summary>
/// True when pagination has reached the end of available results.
/// </summary>
private bool noMoreResults;
/// <summary>
/// The current page fetched of results (zero index).
/// </summary>
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] [Resolved]
private IAPIProvider api { get; set; } private IAPIProvider api { get; set; }
[Resolved] [Resolved]
private RulesetStore rulesets { get; set; } private RulesetStore rulesets { get; set; }
private readonly BeatmapListingSearchControl searchControl;
private readonly BeatmapListingSortTabControl sortControl;
private readonly Box sortControlBackground;
private SearchBeatmapSetsRequest getSetsRequest;
public BeatmapListingFilterControl() public BeatmapListingFilterControl()
{ {
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y; AutoSizeAxes = Axes.Y;
InternalChild = new FillFlowContainer InternalChild = new FillFlowContainer
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
@ -114,51 +135,84 @@ namespace osu.Game.Overlays.BeatmapListing
sortDirection.BindValueChanged(_ => queueUpdateSearch()); sortDirection.BindValueChanged(_ => queueUpdateSearch());
} }
private ScheduledDelegate queryChangedDebounce; public void TakeFocus() => searchControl.TakeFocus();
/// <summary>
/// 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.
/// </summary>
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) private void queueUpdateSearch(bool queryTextChanged = false)
{ {
SearchStarted?.Invoke(); SearchStarted?.Invoke();
getSetsRequest?.Cancel(); resetSearch();
queryChangedDebounce?.Cancel(); queryChangedDebounce = Scheduler.AddDelayed(() =>
queryChangedDebounce = Scheduler.AddDelayed(updateSearch, queryTextChanged ? 500 : 100); {
resetSearch();
FetchNextPage();
}, queryTextChanged ? 500 : 100);
} }
private void updateSearch() private void performRequest()
{ {
getSetsRequest = new SearchBeatmapSetsRequest(searchControl.Query.Value, searchControl.Ruleset.Value) getSetsRequest = new SearchBeatmapSetsRequest(
{ searchControl.Query.Value,
SearchCategory = searchControl.Category.Value, searchControl.Ruleset.Value,
SortCriteria = sortControl.Current.Value, lastResponse?.Cursor,
SortDirection = sortControl.SortDirection.Value, searchControl.Category.Value,
Genre = searchControl.Genre.Value, sortControl.Current.Value,
Language = searchControl.Language.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); 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) protected override void Dispose(bool isDisposing)
{ {
getSetsRequest?.Cancel(); resetSearch();
queryChangedDebounce?.Cancel();
base.Dispose(isDisposing); base.Dispose(isDisposing);
} }
public void TakeFocus() => searchControl.TakeFocus();
} }
} }

View File

@ -4,7 +4,9 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
@ -30,6 +32,10 @@ namespace osu.Game.Overlays
private Drawable currentContent; private Drawable currentContent;
private LoadingLayer loadingLayer; private LoadingLayer loadingLayer;
private Container panelTarget; private Container panelTarget;
private FillFlowContainer<BeatmapPanel> foundContent;
private NotFoundDrawable notFoundContent;
private OverlayScrollContainer resultScrollContainer;
public BeatmapListingOverlay() public BeatmapListingOverlay()
: base(OverlayColourScheme.Blue) : base(OverlayColourScheme.Blue)
@ -48,7 +54,7 @@ namespace osu.Game.Overlays
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Colour = ColourProvider.Background6 Colour = ColourProvider.Background6
}, },
new OverlayScrollContainer resultScrollContainer = new OverlayScrollContainer
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
ScrollbarVisible = false, ScrollbarVisible = false,
@ -80,10 +86,15 @@ namespace osu.Game.Overlays
{ {
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Padding = new MarginPadding { Horizontal = 20 } Padding = new MarginPadding { Horizontal = 20 },
}, Children = new Drawable[]
{
foundContent = new FillFlowContainer<BeatmapPanel>(),
notFoundContent = new NotFoundDrawable(),
loadingLayer = new LoadingLayer(panelTarget) loadingLayer = new LoadingLayer(panelTarget)
} }
}
}
}, },
} }
} }
@ -110,34 +121,53 @@ namespace osu.Game.Overlays
loadingLayer.Show(); loadingLayer.Show();
} }
private Task panelLoadDelegate;
private void onSearchFinished(List<BeatmapSetInfo> beatmaps) private void onSearchFinished(List<BeatmapSetInfo> beatmaps)
{ {
if (!beatmaps.Any()) var newPanels = beatmaps.Select<BeatmapSetInfo, BeatmapPanel>(b => new GridBeatmapPanel(b)
{ {
LoadComponentAsync(new NotFoundDrawable(), addContentToPlaceholder, (cancellationToken = new CancellationTokenSource()).Token); Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
});
if (filterControl.CurrentPage == 0)
{
//No matches case
if (!newPanels.Any())
{
LoadComponentAsync(notFoundContent, addContentToPlaceholder, (cancellationToken = new CancellationTokenSource()).Token);
return; return;
} }
var newPanels = new FillFlowContainer<BeatmapPanel> // spawn new children with the contained so we only clear old content at the last moment.
var content = new FillFlowContainer<BeatmapPanel>
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
Spacing = new Vector2(10), Spacing = new Vector2(10),
Alpha = 0, Alpha = 0,
Margin = new MarginPadding { Vertical = 15 }, Margin = new MarginPadding { Vertical = 15 },
ChildrenEnumerable = beatmaps.Select<BeatmapSetInfo, BeatmapPanel>(b => new GridBeatmapPanel(b) ChildrenEnumerable = newPanels
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
})
}; };
LoadComponentAsync(newPanels, addContentToPlaceholder, (cancellationToken = new CancellationTokenSource()).Token); 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) private void addContentToPlaceholder(Drawable content)
{ {
loadingLayer.Hide(); loadingLayer.Hide();
lastFetchDisplayedTime = Time.Current;
var lastContent = currentContent; 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. // 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. // 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. // 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); if (!content.IsAlive)
currentContent.FadeIn(200, Easing.OutQuint); panelTarget.Add(content);
content.FadeIn(200, Easing.OutQuint);
currentContent = content;
} }
protected override void Dispose(bool isDisposing) 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();
}
} }
} }

View File

@ -4,7 +4,9 @@
using osu.Framework; using osu.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Framework.Screens;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Overlays.Settings.Sections.Maintenance;
namespace osu.Game.Overlays.Settings.Sections.General namespace osu.Game.Overlays.Settings.Sections.General
{ {
@ -12,8 +14,8 @@ namespace osu.Game.Overlays.Settings.Sections.General
{ {
protected override string Header => "Updates"; protected override string Header => "Updates";
[BackgroundDependencyLoader] [BackgroundDependencyLoader(true)]
private void load(Storage storage, OsuConfigManager config) private void load(Storage storage, OsuConfigManager config, OsuGame game)
{ {
Add(new SettingsEnumDropdown<ReleaseStream> Add(new SettingsEnumDropdown<ReleaseStream>
{ {
@ -28,6 +30,12 @@ namespace osu.Game.Overlays.Settings.Sections.General
Text = "Open osu! folder", Text = "Open osu! folder",
Action = storage.OpenInNativeExplorer, Action = storage.OpenInNativeExplorer,
}); });
Add(new SettingsButton
{
Text = "Change folder location...",
Action = () => game?.PerformFromScreen(menu => menu.Push(new MigrationSelectScreen()))
});
} }
} }
} }

View File

@ -0,0 +1,115 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.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);
}
}
}

View File

@ -0,0 +1,128 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.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));
}
}

View File

@ -46,6 +46,13 @@ namespace osu.Game.Overlays
Width = 300, Width = 300,
Colour = ColourInfo.GradientHorizontal(Color4.Black.Opacity(0.75f), Color4.Black.Opacity(0)) 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 new FillFlowContainer
{ {
Direction = FillDirection.Vertical, Direction = FillDirection.Vertical,
@ -56,19 +63,11 @@ namespace osu.Game.Overlays
Margin = new MarginPadding { Left = offset }, Margin = new MarginPadding { Left = offset },
Children = new Drawable[] Children = new Drawable[]
{ {
volumeMeterEffect = new VolumeMeter("EFFECTS", 125, colours.BlueDarker) 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
},
volumeMeterMaster = new VolumeMeter("MASTER", 150, colours.PinkDarker), volumeMeterMaster = new VolumeMeter("MASTER", 150, colours.PinkDarker),
volumeMeterMusic = new VolumeMeter("MUSIC", 125, colours.BlueDarker), volumeMeterMusic = new VolumeMeter("MUSIC", 125, colours.BlueDarker),
muteButton = new MuteButton
{
Margin = new MarginPadding { Top = 100 },
Current = { BindTarget = IsMuted }
} }
} }
},
}); });
volumeMeterMaster.Bindable.BindTo(audio.Volume); volumeMeterMaster.Bindable.BindTo(audio.Volume);

View File

@ -11,10 +11,11 @@ namespace osu.Game.Rulesets.Mods
/// <summary> /// <summary>
/// Whether we should allow failing at the current point in time. /// Whether we should allow failing at the current point in time.
/// </summary> /// </summary>
bool AllowFail { get; } /// <returns>Whether the fail should be allowed to proceed. Return false to block.</returns>
bool PerformFail();
/// <summary> /// <summary>
/// Whether we want to restart on fail. Only used if <see cref="AllowFail"/> is true. /// Whether we want to restart on fail. Only used if <see cref="PerformFail"/> returns true.
/// </summary> /// </summary>
bool RestartOnFail { get; } bool RestartOnFail { get; }
} }

View File

@ -27,7 +27,8 @@ namespace osu.Game.Rulesets.Mods
public override string Description => "Watch a perfect automated play through the song."; public override string Description => "Watch a perfect automated play through the song.";
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 1;
public bool AllowFail => false; public bool PerformFail() => false;
public bool RestartOnFail => false; public bool RestartOnFail => false;
public override Type[] IncompatibleMods => new[] { typeof(ModRelax), typeof(ModSuddenDeath), typeof(ModNoFail) }; public override Type[] IncompatibleMods => new[] { typeof(ModRelax), typeof(ModSuddenDeath), typeof(ModNoFail) };

View File

@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Mods
/// <summary> /// <summary>
/// We never fail, 'yo. /// We never fail, 'yo.
/// </summary> /// </summary>
public bool AllowFail => false; public bool PerformFail() => false;
public bool RestartOnFail => false; public bool RestartOnFail => false;

View File

@ -48,9 +48,7 @@ namespace osu.Game.Rulesets.Mods
retries = Retries.Value; retries = Retries.Value;
} }
public bool AllowFail public bool PerformFail()
{
get
{ {
if (retries == 0) return true; if (retries == 0) return true;
@ -59,7 +57,6 @@ namespace osu.Game.Rulesets.Mods
return false; return false;
} }
}
public bool RestartOnFail => false; public bool RestartOnFail => false;

View File

@ -20,7 +20,8 @@ namespace osu.Game.Rulesets.Mods
public override bool Ranked => true; public override bool Ranked => true;
public override Type[] IncompatibleMods => new[] { typeof(ModNoFail), typeof(ModRelax), typeof(ModAutoplay) }; public override Type[] IncompatibleMods => new[] { typeof(ModNoFail), typeof(ModRelax), typeof(ModAutoplay) };
public bool AllowFail => true; public bool PerformFail() => true;
public bool RestartOnFail => true; public bool RestartOnFail => true;
public void ApplyToHealthProcessor(HealthProcessor healthProcessor) public void ApplyToHealthProcessor(HealthProcessor healthProcessor)

View File

@ -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. // 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. // 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. // 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. // the clock to be exposed via DI to children.
GameplayClock = new GameplayClock(userOffsetClock); GameplayClock = new GameplayClock(userOffsetClock);
@ -248,5 +248,16 @@ namespace osu.Game.Screens.Play
speedAdjustmentsApplied = false; 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)
{
}
}
} }
} }

View File

@ -105,7 +105,7 @@ namespace osu.Game.Screens.Play
/// Whether failing should be allowed. /// Whether failing should be allowed.
/// By default, this checks whether all selected mods allow failing. /// By default, this checks whether all selected mods allow failing.
/// </summary> /// </summary>
protected virtual bool AllowFail => Mods.Value.OfType<IApplicableFailOverride>().All(m => m.AllowFail); protected virtual bool CheckModsAllowFailure() => Mods.Value.OfType<IApplicableFailOverride>().All(m => m.PerformFail());
private readonly bool allowPause; private readonly bool allowPause;
private readonly bool showResults; private readonly bool showResults;
@ -485,7 +485,7 @@ namespace osu.Game.Screens.Play
private bool onFail() private bool onFail()
{ {
if (!AllowFail) if (!CheckModsAllowFailure())
return false; return false;
HasFailed = true; HasFailed = true;

View File

@ -12,7 +12,7 @@ namespace osu.Game.Screens.Play
private readonly Score score; private readonly Score score;
// Disallow replays from failing. (see https://github.com/ppy/osu/issues/6108) // 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) public ReplayPlayer(Score score, bool allowPause = true, bool showResults = true)
: base(allowPause, showResults) : base(allowPause, showResults)

View File

@ -41,7 +41,7 @@ namespace osu.Game.Tests.Visual
{ {
} }
protected override bool AllowFail => true; protected override bool CheckModsAllowFailure() => true;
public bool CheckFailed(bool failed) public bool CheckFailed(bool failed)
{ {

View File

@ -59,12 +59,14 @@ namespace osu.Game.Tests.Visual
protected class ModTestPlayer : TestPlayer protected class ModTestPlayer : TestPlayer
{ {
protected override bool AllowFail { get; } private readonly bool allowFail;
protected override bool CheckModsAllowFailure() => allowFail;
public ModTestPlayer(bool allowFail) public ModTestPlayer(bool allowFail)
: base(false, false) : base(false, false)
{ {
AllowFail = allowFail; this.allowFail = allowFail;
} }
} }

View File

@ -25,7 +25,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="ppy.osu.Framework" Version="2020.511.0" /> <PackageReference Include="ppy.osu.Framework" Version="2020.511.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.427.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2020.512.0" />
<PackageReference Include="Sentry" Version="2.1.1" /> <PackageReference Include="Sentry" Version="2.1.1" />
<PackageReference Include="SharpCompress" Version="0.25.0" /> <PackageReference Include="SharpCompress" Version="0.25.0" />
<PackageReference Include="NUnit" Version="3.12.0" /> <PackageReference Include="NUnit" Version="3.12.0" />

View File

@ -71,7 +71,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="ppy.osu.Framework.iOS" Version="2020.511.0" /> <PackageReference Include="ppy.osu.Framework.iOS" Version="2020.511.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.427.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2020.512.0" />
</ItemGroup> </ItemGroup>
<!-- Xamarin.iOS does not automatically handle transitive dependencies from NuGet packages. --> <!-- Xamarin.iOS does not automatically handle transitive dependencies from NuGet packages. -->
<ItemGroup Label="Transitive Dependencies"> <ItemGroup Label="Transitive Dependencies">