From 35e7cee458f66391d6bb7e705a34fdfeb2bb65a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=80=8C=E7=A9=BA=E7=99=BD=E3=80=8D?= <「空白」> Date: Tue, 12 May 2020 03:18:47 +0900 Subject: [PATCH 01/45] Squash commits from private fork Temporary comments left to-remove later --- .../Online/API/Requests/ResponseWithCursor.cs | 9 ++ .../API/Requests/SearchBeatmapSetsRequest.cs | 27 +++++- .../API/Requests/SearchBeatmapSetsResponse.cs | 2 +- .../BeatmapListingFilterControl.cs | 57 +++++++---- .../BeatmapListing/BeatmapListingPager.cs | 92 ++++++++++++++++++ osu.Game/Overlays/BeatmapListingOverlay.cs | 94 ++++++++++++++----- 6 files changed, 236 insertions(+), 45 deletions(-) create mode 100644 osu.Game/Overlays/BeatmapListing/BeatmapListingPager.cs diff --git a/osu.Game/Online/API/Requests/ResponseWithCursor.cs b/osu.Game/Online/API/Requests/ResponseWithCursor.cs index e38e73dd01..51e88ca52b 100644 --- a/osu.Game/Online/API/Requests/ResponseWithCursor.cs +++ b/osu.Game/Online/API/Requests/ResponseWithCursor.cs @@ -13,4 +13,13 @@ namespace osu.Game.Online.API.Requests [JsonProperty("cursor")] public dynamic CursorJson; } + + public abstract class ResponseWithCursor : ResponseWithCursor where T : class + { + /// + /// Cursor deserialized into T class type (cannot implicitly convert type to object using raw Cursor) + /// + [JsonProperty("cursor")] + public T Cursor; + } } diff --git a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs index 047496b473..fb2cc66dd8 100644 --- a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs +++ b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs @@ -5,11 +5,21 @@ using osu.Framework.IO.Network; using osu.Game.Overlays; using osu.Game.Overlays.BeatmapListing; using osu.Game.Rulesets; +using Newtonsoft.Json; namespace osu.Game.Online.API.Requests { public class SearchBeatmapSetsRequest : APIRequest { + public class Cursor + { + [JsonProperty("approved_date")] + public string ApprovedDate; + + [JsonProperty("_id")] + public string Id; + } + public SearchCategory SearchCategory { get; set; } public SortCriteria SortCriteria { get; set; } @@ -22,17 +32,20 @@ namespace osu.Game.Online.API.Requests 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 +68,12 @@ namespace osu.Game.Online.API.Requests req.AddParameter("sort", $"{SortCriteria.ToString().ToLowerInvariant()}_{directionString}"); + if (cursor != null) + { + req.AddParameter("cursor[_id]", cursor.Id); + req.AddParameter("cursor[approved_date]", cursor.ApprovedDate); + } + return req; } diff --git a/osu.Game/Online/API/Requests/SearchBeatmapSetsResponse.cs b/osu.Game/Online/API/Requests/SearchBeatmapSetsResponse.cs index 3c4fb11ed1..2adf7004e8 100644 --- a/osu.Game/Online/API/Requests/SearchBeatmapSetsResponse.cs +++ b/osu.Game/Online/API/Requests/SearchBeatmapSetsResponse.cs @@ -7,7 +7,7 @@ using osu.Game.Online.API.Requests.Responses; namespace osu.Game.Online.API.Requests { - public class SearchBeatmapSetsResponse : ResponseWithCursor + public class SearchBeatmapSetsResponse : ResponseWithCursor { [JsonProperty("beatmapsets")] public IEnumerable BeatmapSets; diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs index 4dd60c7113..c3e8505ddc 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Linq; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -13,7 +12,6 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Threading; using osu.Game.Beatmaps; using osu.Game.Online.API; -using osu.Game.Online.API.Requests; using osu.Game.Rulesets; using osuTK; using osuTK.Graphics; @@ -24,6 +22,8 @@ namespace osu.Game.Overlays.BeatmapListing { public Action> SearchFinished; public Action SearchStarted; + /// List of currently displayed beatmap entries + private List currentBeatmaps; [Resolved] private IAPIProvider api { get; set; } @@ -35,7 +35,7 @@ namespace osu.Game.Overlays.BeatmapListing private readonly BeatmapListingSortTabControl sortControl; private readonly Box sortControlBackground; - private SearchBeatmapSetsRequest getSetsRequest; + private BeatmapListingPager beatmapListingPager; public BeatmapListingFilterControl() { @@ -115,12 +115,13 @@ namespace osu.Game.Overlays.BeatmapListing } private ScheduledDelegate queryChangedDebounce; + private ScheduledDelegate queryPagingDebounce; private void queueUpdateSearch(bool queryTextChanged = false) { SearchStarted?.Invoke(); - getSetsRequest?.Cancel(); + beatmapListingPager?.Reset(); queryChangedDebounce?.Cancel(); queryChangedDebounce = Scheduler.AddDelayed(updateSearch, queryTextChanged ? 500 : 100); @@ -128,37 +129,55 @@ namespace osu.Game.Overlays.BeatmapListing private void updateSearch() { - 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 - }; + beatmapListingPager = new BeatmapListingPager( + api, + rulesets, + searchControl.Query.Value, + searchControl.Ruleset.Value, + searchControl.Category.Value, + sortControl.Current.Value, + sortControl.SortDirection.Value + ); - getSetsRequest.Success += response => Schedule(() => onSearchFinished(response)); + queryPagingDebounce?.Cancel(); + queryPagingDebounce = null; + beatmapListingPager.PageFetched += onSearchFinished; - api.Queue(getSetsRequest); + AddPageToResult(); } - private void onSearchFinished(SearchBeatmapSetsResponse response) + private void onSearchFinished(List beatmaps) { - var beatmaps = response.BeatmapSets.Select(r => r.ToBeatmapSet(rulesets)).ToList(); - - searchControl.BeatmapSet = response.Total == 0 ? null : beatmaps.First(); + queryPagingDebounce = Scheduler.AddDelayed(() => queryPagingDebounce = null, 1000); + if (currentBeatmaps == null || !beatmapListingPager.IsPastFirstPage) + currentBeatmaps = beatmaps; + else + currentBeatmaps.AddRange(beatmaps); + SearchFinished?.Invoke(beatmaps); } protected override void Dispose(bool isDisposing) { - getSetsRequest?.Cancel(); + beatmapListingPager?.Reset(); queryChangedDebounce?.Cancel(); + queryPagingDebounce?.Cancel(); base.Dispose(isDisposing); } public void TakeFocus() => searchControl.TakeFocus(); + + /// Request next 50 matches if available + public void AddPageToResult() + { + if (beatmapListingPager == null || !beatmapListingPager.CanFetchNextPage) + return; + if (queryPagingDebounce != null) + return; + + beatmapListingPager.FetchNextPage(); + } } } diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingPager.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingPager.cs new file mode 100644 index 0000000000..66faf8df7a --- /dev/null +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingPager.cs @@ -0,0 +1,92 @@ +// 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 System.Linq; +using osu.Game.Beatmaps; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Rulesets; + +namespace osu.Game.Overlays.BeatmapListing +{ + public class BeatmapListingPager + { + private readonly IAPIProvider api; + private readonly RulesetStore rulesets; + private readonly string query; + private readonly RulesetInfo ruleset; + private readonly SearchCategory searchCategory; + private readonly SortCriteria sortCriteria; + private readonly SortDirection sortDirection; + + public event PageFetchHandler PageFetched; + private SearchBeatmapSetsRequest getSetsRequest; + private SearchBeatmapSetsResponse lastResponse; + + /// Reports end of results + private bool isLastPageFetched = false; + /// Job in process lock flag + private bool isFetching => getSetsRequest != null; + /// Whether beatmaps should be appended or replaced + public bool IsPastFirstPage { get; private set; } = false; + /// call FetchNextPage() safe-check + public bool CanFetchNextPage => !isLastPageFetched && !isFetching; + + public BeatmapListingPager(IAPIProvider api, RulesetStore rulesets, string query, RulesetInfo ruleset, SearchCategory searchCategory = SearchCategory.Any, SortCriteria sortCriteria = SortCriteria.Ranked, SortDirection sortDirection = SortDirection.Descending) + { + this.api = api; + this.rulesets = rulesets; + this.query = query; + this.ruleset = ruleset; + this.searchCategory = searchCategory; + this.sortCriteria = sortCriteria; + this.sortDirection = sortDirection; + } + + public void FetchNextPage() + { + if (isFetching) + return; + + if (lastResponse != null) + IsPastFirstPage = true; + + getSetsRequest = new SearchBeatmapSetsRequest( + query, + ruleset, + lastResponse?.Cursor, + searchCategory, + sortCriteria, + sortDirection); + + getSetsRequest.Success += response => + { + var sets = response.BeatmapSets.Select(responseJson => responseJson.ToBeatmapSet(rulesets)).ToList(); + + if (sets.Count == 0) + isLastPageFetched = true; + + lastResponse = response; + getSetsRequest = null; + + PageFetched?.Invoke(sets); + }; + + api.Queue(getSetsRequest); + } + + public void Reset() + { + isLastPageFetched = false; + IsPastFirstPage = false; + + lastResponse = null; + + getSetsRequest?.Cancel(); + getSetsRequest = null; + } + + public delegate void PageFetchHandler(List sets); + } +} diff --git a/osu.Game/Overlays/BeatmapListingOverlay.cs b/osu.Game/Overlays/BeatmapListingOverlay.cs index f680f7c67b..c495c8d21b 100644 --- a/osu.Game/Overlays/BeatmapListingOverlay.cs +++ b/osu.Game/Overlays/BeatmapListingOverlay.cs @@ -30,13 +30,21 @@ namespace osu.Game.Overlays private Drawable currentContent; private LoadingLayer loadingLayer; private Container panelTarget; + private FillFlowContainer foundContent; + private NotFoundDrawable notFoundContent; + + private OverlayScrollContainer resultScrollContainer; + /// Scroll distance threshold from results tail, higher means sooner + private const int pagination_scroll_distance = 500; + /// This is paging event flag + private bool shouldAddNextPage => resultScrollContainer.ScrollableExtent > 0 && resultScrollContainer.IsScrolledToEnd(pagination_scroll_distance); public BeatmapListingOverlay() : base(OverlayColourScheme.Blue) { } - private BeatmapListingFilterControl filterControl; + private BeatmapListingFilterControl filterControl;//actual search settings [BackgroundDependencyLoader] private void load() @@ -48,7 +56,7 @@ namespace osu.Game.Overlays RelativeSizeAxes = Axes.Both, Colour = ColourProvider.Background6 }, - new OverlayScrollContainer + resultScrollContainer = new OverlayScrollContainer { RelativeSizeAxes = Axes.Both, ScrollbarVisible = false, @@ -80,9 +88,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) + } + } } }, } @@ -112,27 +125,52 @@ namespace osu.Game.Overlays private void onSearchFinished(List beatmaps) { + //No matches case if (!beatmaps.Any()) { - LoadComponentAsync(new NotFoundDrawable(), addContentToPlaceholder, (cancellationToken = new CancellationTokenSource()).Token); + LoadComponentAsync(notFoundContent, addContentToPlaceholder, (cancellationToken = new CancellationTokenSource()).Token); return; } - var newPanels = new FillFlowContainer + //New query case + if (!shouldAddNextPage) { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Spacing = new Vector2(10), - Alpha = 0, - Margin = new MarginPadding { Vertical = 15 }, - ChildrenEnumerable = beatmaps.Select(b => new GridBeatmapPanel(b) + //Spawn new child + var newPanels = new FillFlowContainer { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - }) - }; + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(10), + Alpha = 0, + Margin = new MarginPadding { Vertical = 15 }, + ChildrenEnumerable = beatmaps.Select(b => new GridBeatmapPanel(b) + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + }) + }; - LoadComponentAsync(newPanels, addContentToPlaceholder, (cancellationToken = new CancellationTokenSource()).Token); + foundContent = newPanels; + LoadComponentAsync(foundContent, addContentToPlaceholder, (cancellationToken = new CancellationTokenSource()).Token); + } + + //Pagination case + else + { + + beatmaps.ForEach(x => + { + LoadComponentAsync(new GridBeatmapPanel(x) + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + }, loaded => + { + foundContent.Add(loaded); + loaded.FadeIn(200, Easing.OutQuint); + }); + }); + } } private void addContentToPlaceholder(Drawable content) @@ -149,13 +187,18 @@ 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) { cancellationToken?.Cancel(); @@ -203,5 +246,14 @@ namespace osu.Game.Overlays }); } } + + protected override void Update() + { + base.Update(); + + if (shouldAddNextPage) + filterControl.AddPageToResult(); + + } } } From 3b1680583e2ff39976485b1991355daa7c9ec13e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 12 May 2020 11:31:08 +0900 Subject: [PATCH 02/45] Fix taiko scroller not following gameplay time --- osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs index 1ecdb839fb..4cf1af3b8f 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs @@ -17,6 +17,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning { public class LegacyTaikoScroller : CompositeDrawable { + [Resolved(canBeNull: true)] + private GameplayClock gameplayClock { get; set; } + public LegacyTaikoScroller() { RelativeSizeAxes = Axes.Both; @@ -63,7 +66,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning 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; + sprite.X = additiveX ??= sprite.X - (float)(gameplayClock ?? Clock).ElapsedFrameTime * 0.1f; additiveX += sprite.DrawWidth - 1; From e9804bf11be0c535540bde81c09909ba2ecdb132 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 12 May 2020 11:55:12 +0900 Subject: [PATCH 03/45] Update resources --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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/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 @@ - + From 949e17cc0e8f4666d87b9be541b5c31b3e5686a4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 12 May 2020 15:23:47 +0900 Subject: [PATCH 04/45] Rework scroller to support backwards playback --- .../Skinning/TestSceneTaikoScroller.cs | 20 ++++++- .../Skinning/LegacyTaikoScroller.cs | 52 +++++++++++-------- .../UI/DrawableTaikoRuleset.cs | 2 +- 3 files changed, 50 insertions(+), 24 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoScroller.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoScroller.cs index e26f410b71..9a2ada7f72 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,28 @@ 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.TaikoScroller), _ => Empty()))); + AddStep("Load scroller", () => SetContents(() => + new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.TaikoScroller), _ => Empty()) + { + Clock = new FramedClock(clock) + })); 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 4cf1af3b8f..b3a325ea68 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs @@ -17,9 +17,6 @@ namespace osu.Game.Rulesets.Taiko.Skinning { public class LegacyTaikoScroller : CompositeDrawable { - [Resolved(canBeNull: true)] - private GameplayClock gameplayClock { get; set; } - public LegacyTaikoScroller() { RelativeSizeAxes = Axes.Both; @@ -59,31 +56,42 @@ namespace osu.Game.Rulesets.Taiko.Skinning { base.Update(); - while (true) + bool wideEnough() => + InternalChildren.Any() + && InternalChildren.First().ScreenSpaceDrawQuad.Width * InternalChildren.Count >= ScreenSpaceDrawQuad.Width * 2; + + // 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 (!wideEnough()) { - float? additiveX = null; + ClearInternal(); - 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)(gameplayClock ?? Clock).ElapsedFrameTime * 0.1f; + while (!wideEnough()) + AddInternal(new ScrollerSprite { Passing = passing }); + } - additiveX += sprite.DrawWidth - 1; + var first = InternalChildren.First(); + var last = InternalChildren.Last(); - if (sprite.X + sprite.DrawWidth < 0) - sprite.Expire(); - } + 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 = currentX; + currentX += sprite.DrawWidth - 1; + } - var last = InternalChildren.LastOrDefault(); + if (first.ScreenSpaceDrawQuad.TopLeft.X >= ScreenSpaceDrawQuad.TopLeft.X) + { + foreach (var internalChild in InternalChildren) + internalChild.X -= first.DrawWidth; + } - // 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 c0a6c4582c..21d595a97a 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.TaikoScroller), _ => Empty()) + FrameStableComponents.Add(scroller = new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.TaikoScroller), _ => Empty()) { RelativeSizeAxes = Axes.X, Depth = float.MaxValue From c7d8793c1d96bd817d36ad1ae918ab4c6048fa33 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 12 May 2020 15:44:14 +0900 Subject: [PATCH 05/45] Remove unnecessary overlap --- osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs index b3a325ea68..28114ac74f 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs @@ -79,7 +79,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning { // add the x coordinates and perform re-layout on all sprites as spacing may change with gameplay scale. sprite.X = currentX; - currentX += sprite.DrawWidth - 1; + currentX += sprite.DrawWidth; } if (first.ScreenSpaceDrawQuad.TopLeft.X >= ScreenSpaceDrawQuad.TopLeft.X) From c04f2b0840ded76704dc37210273285d2bf8200a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 12 May 2020 15:51:59 +0900 Subject: [PATCH 06/45] Reposition taiko playfield to be closer to the top of the screen --- .../UI/TaikoPlayfieldAdjustmentContainer.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) 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; } } } From e28e89213f07767c12b2eed7d0a5b3063a5ef82b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 12 May 2020 16:58:47 +0900 Subject: [PATCH 07/45] Fix incorrect spawning when scale adjustments are applied to child sprites --- .../Skinning/TestSceneTaikoScroller.cs | 3 ++- osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs | 9 ++------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoScroller.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoScroller.cs index 9a2ada7f72..39e6bc2d6d 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoScroller.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoScroller.cs @@ -22,7 +22,8 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning AddStep("Load scroller", () => SetContents(() => new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.TaikoScroller), _ => Empty()) { - Clock = new FramedClock(clock) + 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 })); diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs index 28114ac74f..3ec6be8a6c 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs @@ -64,13 +64,8 @@ namespace osu.Game.Rulesets.Taiko.Skinning float currentX = (InternalChildren?.FirstOrDefault()?.X ?? 0) - (float)Clock.ElapsedFrameTime * 0.1f; // ensure we have enough sprites - if (!wideEnough()) - { - ClearInternal(); - - while (!wideEnough()) - AddInternal(new ScrollerSprite { Passing = passing }); - } + while (!wideEnough()) + AddInternal(new ScrollerSprite { Passing = passing }); var first = InternalChildren.First(); var last = InternalChildren.Last(); From de50b725d59be235c0be5ccee68d20f648c0c045 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 12 May 2020 20:08:35 +0900 Subject: [PATCH 08/45] Fix mod failure checks executing actual game logic --- osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs | 3 ++- osu.Game.Tests/Visual/Gameplay/TestSceneReplay.cs | 3 ++- osu.Game/Rulesets/Mods/IApplicableFailOverride.cs | 5 +++-- osu.Game/Rulesets/Mods/ModAutoplay.cs | 3 ++- osu.Game/Rulesets/Mods/ModBlockFail.cs | 2 +- osu.Game/Rulesets/Mods/ModEasy.cs | 13 +++++-------- osu.Game/Rulesets/Mods/ModSuddenDeath.cs | 3 ++- osu.Game/Screens/Play/Player.cs | 4 ++-- osu.Game/Screens/Play/ReplayPlayer.cs | 2 +- osu.Game/Tests/Visual/ModPerfectTestScene.cs | 2 +- osu.Game/Tests/Visual/ModTestScene.cs | 6 ++++-- 11 files changed, 25 insertions(+), 21 deletions(-) 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.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/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/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..c16352bead 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() => false; public bool CheckFailed(bool failed) { diff --git a/osu.Game/Tests/Visual/ModTestScene.cs b/osu.Game/Tests/Visual/ModTestScene.cs index 8b41fb5075..b5b3084097 100644 --- a/osu.Game/Tests/Visual/ModTestScene.cs +++ b/osu.Game/Tests/Visual/ModTestScene.cs @@ -64,12 +64,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; } } From 44319c1b719c64468de0594e22c48c357c3c62ac Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 12 May 2020 20:26:34 +0900 Subject: [PATCH 09/45] Commit missed change --- osu.Game/Tests/Visual/ModPerfectTestScene.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Tests/Visual/ModPerfectTestScene.cs b/osu.Game/Tests/Visual/ModPerfectTestScene.cs index c16352bead..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 CheckModsAllowFailure() => false; + protected override bool CheckModsAllowFailure() => true; public bool CheckFailed(bool failed) { From 0c60b10757b1d605cb1d15c20c510a3b9a63ddbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=80=8C=E7=A9=BA=E7=99=BD=E3=80=8D?= <「空白」> Date: Wed, 13 May 2020 01:14:11 +0900 Subject: [PATCH 10/45] Fix code factor issues > ran "dotnet format --check", shouldn't return whitespace errors anymore --- .../Overlays/BeatmapListing/BeatmapListingFilterControl.cs | 2 +- osu.Game/Overlays/BeatmapListingOverlay.cs | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs index c3e8505ddc..3f06bc20ed 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs @@ -154,7 +154,7 @@ namespace osu.Game.Overlays.BeatmapListing currentBeatmaps = beatmaps; else currentBeatmaps.AddRange(beatmaps); - + SearchFinished?.Invoke(beatmaps); } diff --git a/osu.Game/Overlays/BeatmapListingOverlay.cs b/osu.Game/Overlays/BeatmapListingOverlay.cs index c495c8d21b..115fe02999 100644 --- a/osu.Game/Overlays/BeatmapListingOverlay.cs +++ b/osu.Game/Overlays/BeatmapListingOverlay.cs @@ -44,7 +44,7 @@ namespace osu.Game.Overlays { } - private BeatmapListingFilterControl filterControl;//actual search settings + private BeatmapListingFilterControl filterControl; [BackgroundDependencyLoader] private void load() @@ -157,7 +157,6 @@ namespace osu.Game.Overlays //Pagination case else { - beatmaps.ForEach(x => { LoadComponentAsync(new GridBeatmapPanel(x) @@ -198,7 +197,6 @@ namespace osu.Game.Overlays currentContent = content; } - protected override void Dispose(bool isDisposing) { cancellationToken?.Cancel(); @@ -253,7 +251,6 @@ namespace osu.Game.Overlays if (shouldAddNextPage) filterControl.AddPageToResult(); - } } } From 82190a07b82f439c8e221c2420ca9b61e7d7e944 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=80=8C=E7=A9=BA=E7=99=BD=E3=80=8D?= <「空白」> Date: Wed, 13 May 2020 02:01:38 +0900 Subject: [PATCH 11/45] Remove temporary comments > Removes unnecessary xmldoc comments --- osu.Game/Online/API/Requests/ResponseWithCursor.cs | 3 --- .../Overlays/BeatmapListing/BeatmapListingFilterControl.cs | 2 -- osu.Game/Overlays/BeatmapListing/BeatmapListingPager.cs | 4 ---- osu.Game/Overlays/BeatmapListingOverlay.cs | 2 -- 4 files changed, 11 deletions(-) diff --git a/osu.Game/Online/API/Requests/ResponseWithCursor.cs b/osu.Game/Online/API/Requests/ResponseWithCursor.cs index 51e88ca52b..b0fe9eea28 100644 --- a/osu.Game/Online/API/Requests/ResponseWithCursor.cs +++ b/osu.Game/Online/API/Requests/ResponseWithCursor.cs @@ -16,9 +16,6 @@ namespace osu.Game.Online.API.Requests public abstract class ResponseWithCursor : ResponseWithCursor where T : class { - /// - /// Cursor deserialized into T class type (cannot implicitly convert type to object using raw Cursor) - /// [JsonProperty("cursor")] public T Cursor; } diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs index 3f06bc20ed..ac5ad96f7c 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs @@ -22,7 +22,6 @@ namespace osu.Game.Overlays.BeatmapListing { public Action> SearchFinished; public Action SearchStarted; - /// List of currently displayed beatmap entries private List currentBeatmaps; [Resolved] @@ -169,7 +168,6 @@ namespace osu.Game.Overlays.BeatmapListing public void TakeFocus() => searchControl.TakeFocus(); - /// Request next 50 matches if available public void AddPageToResult() { if (beatmapListingPager == null || !beatmapListingPager.CanFetchNextPage) diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingPager.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingPager.cs index 66faf8df7a..4c8902d314 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingPager.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingPager.cs @@ -24,13 +24,9 @@ namespace osu.Game.Overlays.BeatmapListing private SearchBeatmapSetsRequest getSetsRequest; private SearchBeatmapSetsResponse lastResponse; - /// Reports end of results private bool isLastPageFetched = false; - /// Job in process lock flag private bool isFetching => getSetsRequest != null; - /// Whether beatmaps should be appended or replaced public bool IsPastFirstPage { get; private set; } = false; - /// call FetchNextPage() safe-check public bool CanFetchNextPage => !isLastPageFetched && !isFetching; public BeatmapListingPager(IAPIProvider api, RulesetStore rulesets, string query, RulesetInfo ruleset, SearchCategory searchCategory = SearchCategory.Any, SortCriteria sortCriteria = SortCriteria.Ranked, SortDirection sortDirection = SortDirection.Descending) diff --git a/osu.Game/Overlays/BeatmapListingOverlay.cs b/osu.Game/Overlays/BeatmapListingOverlay.cs index 115fe02999..ba92181ac5 100644 --- a/osu.Game/Overlays/BeatmapListingOverlay.cs +++ b/osu.Game/Overlays/BeatmapListingOverlay.cs @@ -34,9 +34,7 @@ namespace osu.Game.Overlays private NotFoundDrawable notFoundContent; private OverlayScrollContainer resultScrollContainer; - /// Scroll distance threshold from results tail, higher means sooner private const int pagination_scroll_distance = 500; - /// This is paging event flag private bool shouldAddNextPage => resultScrollContainer.ScrollableExtent > 0 && resultScrollContainer.IsScrolledToEnd(pagination_scroll_distance); public BeatmapListingOverlay() From e321494f15c182beec3e8c911341968cbfa73417 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=80=8C=E7=A9=BA=E7=99=BD=E3=80=8D?= <「空白」> Date: Wed, 13 May 2020 02:15:24 +0900 Subject: [PATCH 12/45] Fix sort-by handling > Add other cursor fields for paging different sortings > Sorted as they show in GUI code-wise for more readability for now --- .../API/Requests/SearchBeatmapSetsRequest.cs | 41 ++++++++++++++++++- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs index fb2cc66dd8..2987d554af 100644 --- a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs +++ b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs @@ -13,8 +13,29 @@ namespace osu.Game.Online.API.Requests { public class Cursor { + [JsonProperty("title.raw")] + public string Title; + + [JsonProperty("artist.raw")] + public string Artist; + + [JsonProperty("beatmaps.difficultyrating")] + public string Difficulty; + [JsonProperty("approved_date")] - public string ApprovedDate; + public string Ranked; + + [JsonProperty("rating")] + public string Rating; + + [JsonProperty("play_count")] + public string Plays; + + [JsonProperty("favourite_count")] + public string Favourites; + + [JsonProperty("_score")] + public string Relevance; [JsonProperty("_id")] public string Id; @@ -70,8 +91,24 @@ namespace osu.Game.Online.API.Requests if (cursor != null) { + if (cursor.Title != null) + req.AddParameter("cursor[title.raw]", cursor.Title); + if (cursor.Artist != null) + req.AddParameter("cursor[artist.raw]", cursor.Artist); + if (cursor.Difficulty != null) + req.AddParameter("cursor[beatmaps.difficultyrating]", cursor.Difficulty); + if (cursor.Ranked != null) + req.AddParameter("cursor[approved_date]", cursor.Ranked); + if (cursor.Rating != null) + req.AddParameter("cursor[rating]", cursor.Rating); + if (cursor.Plays != null) + req.AddParameter("cursor[play_count]", cursor.Plays); + if (cursor.Favourites != null) + req.AddParameter("cursor[favourite_count]", cursor.Favourites); + if (cursor.Relevance != null) + req.AddParameter("cursor[_score]", cursor.Relevance); + req.AddParameter("cursor[_id]", cursor.Id); - req.AddParameter("cursor[approved_date]", cursor.ApprovedDate); } return req; From cabf3a89b1e79edcd452900477a684ad4f5b3bd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=80=8C=E7=A9=BA=E7=99=BD=E3=80=8D?= <「空白」> Date: Wed, 13 May 2020 03:44:57 +0900 Subject: [PATCH 13/45] More robust cursor parsing solution > Change cursor request to return last response's entire cursor structure --- .../API/Requests/SearchBeatmapSetsRequest.cs | 54 +++---------------- 1 file changed, 8 insertions(+), 46 deletions(-) diff --git a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs index 2987d554af..e1df7fe6d3 100644 --- a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs +++ b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs @@ -2,10 +2,13 @@ // 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.Overlays; using osu.Game.Overlays.BeatmapListing; using osu.Game.Rulesets; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System.Collections.Generic; namespace osu.Game.Online.API.Requests { @@ -13,32 +16,8 @@ namespace osu.Game.Online.API.Requests { public class Cursor { - [JsonProperty("title.raw")] - public string Title; - - [JsonProperty("artist.raw")] - public string Artist; - - [JsonProperty("beatmaps.difficultyrating")] - public string Difficulty; - - [JsonProperty("approved_date")] - public string Ranked; - - [JsonProperty("rating")] - public string Rating; - - [JsonProperty("play_count")] - public string Plays; - - [JsonProperty("favourite_count")] - public string Favourites; - - [JsonProperty("_score")] - public string Relevance; - - [JsonProperty("_id")] - public string Id; + [JsonExtensionData] + public IDictionary Properties; } public SearchCategory SearchCategory { get; set; } @@ -89,27 +68,10 @@ namespace osu.Game.Online.API.Requests req.AddParameter("sort", $"{SortCriteria.ToString().ToLowerInvariant()}_{directionString}"); - if (cursor != null) + cursor?.Properties.ForEach(x => { - if (cursor.Title != null) - req.AddParameter("cursor[title.raw]", cursor.Title); - if (cursor.Artist != null) - req.AddParameter("cursor[artist.raw]", cursor.Artist); - if (cursor.Difficulty != null) - req.AddParameter("cursor[beatmaps.difficultyrating]", cursor.Difficulty); - if (cursor.Ranked != null) - req.AddParameter("cursor[approved_date]", cursor.Ranked); - if (cursor.Rating != null) - req.AddParameter("cursor[rating]", cursor.Rating); - if (cursor.Plays != null) - req.AddParameter("cursor[play_count]", cursor.Plays); - if (cursor.Favourites != null) - req.AddParameter("cursor[favourite_count]", cursor.Favourites); - if (cursor.Relevance != null) - req.AddParameter("cursor[_score]", cursor.Relevance); - - req.AddParameter("cursor[_id]", cursor.Id); - } + req.AddParameter("cursor[" + x.Key + "]", x.Value?.ToString()); + }); return req; } From 5962dedd35947eab90642aef09a6dee8547e3ef5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=80=8C=E7=A9=BA=E7=99=BD=E3=80=8D?= <「空白」> Date: Wed, 13 May 2020 05:04:39 +0900 Subject: [PATCH 14/45] Reimplement cursor as part of WebRequest extensions > Added WebRequestExtensions > Moved Cursor request logic from SearchBeatmapSetsRequest --- osu.Game/Extensions/WebRequestExtensions.cs | 25 +++++++++++++++++++ .../API/Requests/SearchBeatmapSetsRequest.cs | 16 ++---------- .../API/Requests/SearchBeatmapSetsResponse.cs | 3 ++- 3 files changed, 29 insertions(+), 15 deletions(-) create mode 100644 osu.Game/Extensions/WebRequestExtensions.cs diff --git a/osu.Game/Extensions/WebRequestExtensions.cs b/osu.Game/Extensions/WebRequestExtensions.cs new file mode 100644 index 0000000000..c8e3c564a5 --- /dev/null +++ b/osu.Game/Extensions/WebRequestExtensions.cs @@ -0,0 +1,25 @@ +using osu.Framework.IO.Network; +using osu.Framework.Extensions.IEnumerableExtensions; +using System.Collections.Generic; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace osu.Game.Extensions +{ + public class Cursor + { + [JsonExtensionData] + public IDictionary Properties; + } + + public static class WebRequestExtensions + { + 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/Online/API/Requests/SearchBeatmapSetsRequest.cs b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs index e1df7fe6d3..a49cb70c37 100644 --- a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs +++ b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs @@ -2,24 +2,15 @@ // 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.Extensions; using osu.Game.Overlays; using osu.Game.Overlays.BeatmapListing; using osu.Game.Rulesets; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using System.Collections.Generic; namespace osu.Game.Online.API.Requests { public class SearchBeatmapSetsRequest : APIRequest { - public class Cursor - { - [JsonExtensionData] - public IDictionary Properties; - } - public SearchCategory SearchCategory { get; set; } public SortCriteria SortCriteria { get; set; } @@ -68,10 +59,7 @@ namespace osu.Game.Online.API.Requests req.AddParameter("sort", $"{SortCriteria.ToString().ToLowerInvariant()}_{directionString}"); - cursor?.Properties.ForEach(x => - { - req.AddParameter("cursor[" + x.Key + "]", x.Value?.ToString()); - }); + req.AddCursor(cursor); return req; } diff --git a/osu.Game/Online/API/Requests/SearchBeatmapSetsResponse.cs b/osu.Game/Online/API/Requests/SearchBeatmapSetsResponse.cs index 2adf7004e8..a4d2c0e871 100644 --- a/osu.Game/Online/API/Requests/SearchBeatmapSetsResponse.cs +++ b/osu.Game/Online/API/Requests/SearchBeatmapSetsResponse.cs @@ -3,11 +3,12 @@ using System.Collections.Generic; using Newtonsoft.Json; +using osu.Game.Extensions; using osu.Game.Online.API.Requests.Responses; namespace osu.Game.Online.API.Requests { - public class SearchBeatmapSetsResponse : ResponseWithCursor + public class SearchBeatmapSetsResponse : ResponseWithCursor { [JsonProperty("beatmapsets")] public IEnumerable BeatmapSets; From 00efeb7cc6db07eac414dab21366b041fa46f6ba Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 13 May 2020 19:19:58 +0900 Subject: [PATCH 15/45] Fix spawning too many sprites due to not yet populated sizing --- .../Skinning/LegacyTaikoScroller.cs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs index 3ec6be8a6c..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,21 +52,16 @@ namespace osu.Game.Rulesets.Taiko.Skinning }, true); } - public Bindable LastResult = new Bindable(); - protected override void Update() { base.Update(); - bool wideEnough() => - InternalChildren.Any() - && InternalChildren.First().ScreenSpaceDrawQuad.Width * InternalChildren.Count >= ScreenSpaceDrawQuad.Width * 2; - // 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 - while (!wideEnough()) + if (!InternalChildren.Any() + || InternalChildren.First().ScreenSpaceDrawQuad.Width * InternalChildren.Count < ScreenSpaceDrawQuad.Width * 2) AddInternal(new ScrollerSprite { Passing = passing }); var first = InternalChildren.First(); From 1ac9c7c15a88fe24132084b93c2d619cfb46b2da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=80=8C=E7=A9=BA=E7=99=BD=E3=80=8D?= <「空白」> Date: Thu, 14 May 2020 00:04:31 +0900 Subject: [PATCH 16/45] Add license header to WebRequestExtensions --- osu.Game/Extensions/WebRequestExtensions.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Extensions/WebRequestExtensions.cs b/osu.Game/Extensions/WebRequestExtensions.cs index c8e3c564a5..f92f707d30 100644 --- a/osu.Game/Extensions/WebRequestExtensions.cs +++ b/osu.Game/Extensions/WebRequestExtensions.cs @@ -1,4 +1,7 @@ -using osu.Framework.IO.Network; +// 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 System.Collections.Generic; using Newtonsoft.Json; From 43450b54853712698da7909e31568a5ecd6b4566 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=80=8C=E7=A9=BA=E7=99=BD=E3=80=8D?= <「空白」> Date: Thu, 14 May 2020 01:57:03 +0900 Subject: [PATCH 17/45] Resolve remaining InspectCode issues > CI should now pass build test --- osu.Game/Extensions/WebRequestExtensions.cs | 2 ++ .../Online/API/Requests/SearchBeatmapSetsRequest.cs | 13 ++++++------- .../Overlays/BeatmapListing/BeatmapListingPager.cs | 4 ++-- osu.Game/Overlays/BeatmapListingOverlay.cs | 3 +-- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/osu.Game/Extensions/WebRequestExtensions.cs b/osu.Game/Extensions/WebRequestExtensions.cs index f92f707d30..80c8b147bf 100644 --- a/osu.Game/Extensions/WebRequestExtensions.cs +++ b/osu.Game/Extensions/WebRequestExtensions.cs @@ -6,11 +6,13 @@ using osu.Framework.Extensions.IEnumerableExtensions; using System.Collections.Generic; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using JetBrains.Annotations; namespace osu.Game.Extensions { public class Cursor { + [UsedImplicitly] [JsonExtensionData] public IDictionary Properties; } diff --git a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs index a49cb70c37..0c3272c7de 100644 --- a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs +++ b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs @@ -11,15 +11,15 @@ 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; @@ -27,8 +27,7 @@ namespace osu.Game.Online.API.Requests private string directionString => SortDirection == SortDirection.Descending ? @"desc" : @"asc"; - public SearchBeatmapSetsRequest(string query, RulesetInfo ruleset, Cursor cursor = null, - SearchCategory searchCategory = SearchCategory.Any, SortCriteria sortCriteria = SortCriteria.Ranked, SortDirection sortDirection = SortDirection.Descending) + 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; diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingPager.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingPager.cs index 4c8902d314..f55e37ebc7 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingPager.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingPager.cs @@ -24,9 +24,9 @@ namespace osu.Game.Overlays.BeatmapListing private SearchBeatmapSetsRequest getSetsRequest; private SearchBeatmapSetsResponse lastResponse; - private bool isLastPageFetched = false; + private bool isLastPageFetched; private bool isFetching => getSetsRequest != null; - public bool IsPastFirstPage { get; private set; } = false; + public bool IsPastFirstPage { get; private set; } public bool CanFetchNextPage => !isLastPageFetched && !isFetching; public BeatmapListingPager(IAPIProvider api, RulesetStore rulesets, string query, RulesetInfo ruleset, SearchCategory searchCategory = SearchCategory.Any, SortCriteria sortCriteria = SortCriteria.Ranked, SortDirection sortDirection = SortDirection.Descending) diff --git a/osu.Game/Overlays/BeatmapListingOverlay.cs b/osu.Game/Overlays/BeatmapListingOverlay.cs index ba92181ac5..e26f084ea4 100644 --- a/osu.Game/Overlays/BeatmapListingOverlay.cs +++ b/osu.Game/Overlays/BeatmapListingOverlay.cs @@ -184,8 +184,7 @@ 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) - .Then().Schedule(() => panelTarget.Remove(lastContent)); + lastContent.Delay(25).Schedule(() => lastContent.BypassAutoSizeAxes = Axes.Y).Then().Schedule(() => panelTarget.Remove(lastContent)); } if (!content.IsAlive) From 5e09a1b33485663dbb69fa53b44b12861849831c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 14 May 2020 14:23:12 +0900 Subject: [PATCH 18/45] Use Action rather than custom handler --- osu.Game/Overlays/BeatmapListing/BeatmapListingPager.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingPager.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingPager.cs index f55e37ebc7..dc9f30cab3 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingPager.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingPager.cs @@ -1,6 +1,7 @@ // 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.Collections.Generic; using System.Linq; using osu.Game.Beatmaps; @@ -20,7 +21,8 @@ namespace osu.Game.Overlays.BeatmapListing private readonly SortCriteria sortCriteria; private readonly SortDirection sortDirection; - public event PageFetchHandler PageFetched; + public event Action> PageFetched; + private SearchBeatmapSetsRequest getSetsRequest; private SearchBeatmapSetsResponse lastResponse; @@ -82,7 +84,5 @@ namespace osu.Game.Overlays.BeatmapListing getSetsRequest?.Cancel(); getSetsRequest = null; } - - public delegate void PageFetchHandler(List sets); } } From fa3373e5f306e0f64afeccefa9977c02584b2f5c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 14 May 2020 14:24:43 +0900 Subject: [PATCH 19/45] Reorder file and change naming slightly --- .../BeatmapListingFilterControl.cs | 30 +++++++++---------- osu.Game/Overlays/BeatmapListingOverlay.cs | 2 +- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs index ac5ad96f7c..92822794b7 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs @@ -36,6 +36,9 @@ namespace osu.Game.Overlays.BeatmapListing private BeatmapListingPager beatmapListingPager; + private ScheduledDelegate queryChangedDebounce; + private ScheduledDelegate queryPagingDebounce; + public BeatmapListingFilterControl() { RelativeSizeAxes = Axes.X; @@ -113,8 +116,17 @@ namespace osu.Game.Overlays.BeatmapListing sortDirection.BindValueChanged(_ => queueUpdateSearch()); } - private ScheduledDelegate queryChangedDebounce; - private ScheduledDelegate queryPagingDebounce; + public void TakeFocus() => searchControl.TakeFocus(); + + public void ShowMore() + { + if (beatmapListingPager == null || !beatmapListingPager.CanFetchNextPage) + return; + if (queryPagingDebounce != null) + return; + + beatmapListingPager.FetchNextPage(); + } private void queueUpdateSearch(bool queryTextChanged = false) { @@ -142,7 +154,7 @@ namespace osu.Game.Overlays.BeatmapListing queryPagingDebounce = null; beatmapListingPager.PageFetched += onSearchFinished; - AddPageToResult(); + ShowMore(); } private void onSearchFinished(List beatmaps) @@ -165,17 +177,5 @@ namespace osu.Game.Overlays.BeatmapListing base.Dispose(isDisposing); } - - public void TakeFocus() => searchControl.TakeFocus(); - - public void AddPageToResult() - { - if (beatmapListingPager == null || !beatmapListingPager.CanFetchNextPage) - return; - if (queryPagingDebounce != null) - return; - - beatmapListingPager.FetchNextPage(); - } } } diff --git a/osu.Game/Overlays/BeatmapListingOverlay.cs b/osu.Game/Overlays/BeatmapListingOverlay.cs index e26f084ea4..4aa754491c 100644 --- a/osu.Game/Overlays/BeatmapListingOverlay.cs +++ b/osu.Game/Overlays/BeatmapListingOverlay.cs @@ -247,7 +247,7 @@ namespace osu.Game.Overlays base.Update(); if (shouldAddNextPage) - filterControl.AddPageToResult(); + filterControl.ShowMore(); } } } From 04c99735264be3cd769bc085d7958727c6b83bda Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 14 May 2020 14:33:31 +0900 Subject: [PATCH 20/45] Clean up cancellation logic --- .../BeatmapListingFilterControl.cs | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs index 92822794b7..3df4d5d588 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs @@ -122,6 +122,7 @@ namespace osu.Game.Overlays.BeatmapListing { if (beatmapListingPager == null || !beatmapListingPager.CanFetchNextPage) return; + if (queryPagingDebounce != null) return; @@ -132,14 +133,15 @@ namespace osu.Game.Overlays.BeatmapListing { SearchStarted?.Invoke(); - beatmapListingPager?.Reset(); + cancelSearch(); - queryChangedDebounce?.Cancel(); queryChangedDebounce = Scheduler.AddDelayed(updateSearch, queryTextChanged ? 500 : 100); } private void updateSearch() { + cancelSearch(); + beatmapListingPager = new BeatmapListingPager( api, rulesets, @@ -150,13 +152,20 @@ namespace osu.Game.Overlays.BeatmapListing sortControl.SortDirection.Value ); - queryPagingDebounce?.Cancel(); - queryPagingDebounce = null; beatmapListingPager.PageFetched += onSearchFinished; ShowMore(); } + private void cancelSearch() + { + beatmapListingPager?.Reset(); + queryChangedDebounce?.Cancel(); + + queryPagingDebounce?.Cancel(); + queryPagingDebounce = null; + } + private void onSearchFinished(List beatmaps) { queryPagingDebounce = Scheduler.AddDelayed(() => queryPagingDebounce = null, 1000); @@ -171,9 +180,7 @@ namespace osu.Game.Overlays.BeatmapListing protected override void Dispose(bool isDisposing) { - beatmapListingPager?.Reset(); - queryChangedDebounce?.Cancel(); - queryPagingDebounce?.Cancel(); + cancelSearch(); base.Dispose(isDisposing); } From c836c9319bb0b6d28fa378e4412b9d4fc7d47e71 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 14 May 2020 15:35:11 +0900 Subject: [PATCH 21/45] Combine pagination logic into BeatmapListingFilterControl --- .../BeatmapListingFilterControl.cs | 118 +++++++++++------- .../BeatmapListing/BeatmapListingPager.cs | 88 ------------- osu.Game/Overlays/BeatmapListingOverlay.cs | 69 +++++----- 3 files changed, 111 insertions(+), 164 deletions(-) delete mode 100644 osu.Game/Overlays/BeatmapListing/BeatmapListingPager.cs diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs index 3df4d5d588..41c99d5d03 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -12,6 +13,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Threading; using osu.Game.Beatmaps; using osu.Game.Online.API; +using osu.Game.Online.API.Requests; using osu.Game.Rulesets; using osuTK; using osuTK.Graphics; @@ -20,9 +22,34 @@ 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; - private List currentBeatmaps; + + /// + /// 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; } @@ -30,19 +57,11 @@ namespace osu.Game.Overlays.BeatmapListing [Resolved] private RulesetStore rulesets { get; set; } - private readonly BeatmapListingSearchControl searchControl; - private readonly BeatmapListingSortTabControl sortControl; - private readonly Box sortControlBackground; - - private BeatmapListingPager beatmapListingPager; - - private ScheduledDelegate queryChangedDebounce; - private ScheduledDelegate queryPagingDebounce; - public BeatmapListingFilterControl() { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; + InternalChild = new FillFlowContainer { RelativeSizeAxes = Axes.X, @@ -118,69 +137,80 @@ namespace osu.Game.Overlays.BeatmapListing public void TakeFocus() => searchControl.TakeFocus(); - public void ShowMore() + /// + /// 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() { - if (beatmapListingPager == null || !beatmapListingPager.CanFetchNextPage) + // there may be no results left. + if (noMoreResults) return; - if (queryPagingDebounce != null) + // there may already be an active request. + if (getSetsRequest != null) return; - beatmapListingPager.FetchNextPage(); + if (lastResponse != null) + CurrentPage++; + + performRequest(); } private void queueUpdateSearch(bool queryTextChanged = false) { SearchStarted?.Invoke(); - cancelSearch(); + resetSearch(); - queryChangedDebounce = Scheduler.AddDelayed(updateSearch, queryTextChanged ? 500 : 100); + queryChangedDebounce = Scheduler.AddDelayed(() => + { + resetSearch(); + FetchNextPage(); + }, queryTextChanged ? 500 : 100); } - private void updateSearch() + private void performRequest() { - cancelSearch(); - - beatmapListingPager = new BeatmapListingPager( - api, - rulesets, + getSetsRequest = new SearchBeatmapSetsRequest( searchControl.Query.Value, searchControl.Ruleset.Value, + lastResponse?.Cursor, searchControl.Category.Value, sortControl.Current.Value, - sortControl.SortDirection.Value - ); + sortControl.SortDirection.Value); - beatmapListingPager.PageFetched += onSearchFinished; + getSetsRequest.Success += response => + { + var sets = response.BeatmapSets.Select(responseJson => responseJson.ToBeatmapSet(rulesets)).ToList(); - ShowMore(); + if (sets.Count == 0) + noMoreResults = true; + + lastResponse = response; + getSetsRequest = null; + + SearchFinished?.Invoke(sets); + }; + + api.Queue(getSetsRequest); } - private void cancelSearch() + private void resetSearch() { - beatmapListingPager?.Reset(); + noMoreResults = false; + CurrentPage = 0; + + lastResponse = null; + + getSetsRequest?.Cancel(); + getSetsRequest = null; + queryChangedDebounce?.Cancel(); - - queryPagingDebounce?.Cancel(); - queryPagingDebounce = null; - } - - private void onSearchFinished(List beatmaps) - { - queryPagingDebounce = Scheduler.AddDelayed(() => queryPagingDebounce = null, 1000); - - if (currentBeatmaps == null || !beatmapListingPager.IsPastFirstPage) - currentBeatmaps = beatmaps; - else - currentBeatmaps.AddRange(beatmaps); - - SearchFinished?.Invoke(beatmaps); } protected override void Dispose(bool isDisposing) { - cancelSearch(); + resetSearch(); base.Dispose(isDisposing); } diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingPager.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingPager.cs deleted file mode 100644 index dc9f30cab3..0000000000 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingPager.cs +++ /dev/null @@ -1,88 +0,0 @@ -// 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.Collections.Generic; -using System.Linq; -using osu.Game.Beatmaps; -using osu.Game.Online.API; -using osu.Game.Online.API.Requests; -using osu.Game.Rulesets; - -namespace osu.Game.Overlays.BeatmapListing -{ - public class BeatmapListingPager - { - private readonly IAPIProvider api; - private readonly RulesetStore rulesets; - private readonly string query; - private readonly RulesetInfo ruleset; - private readonly SearchCategory searchCategory; - private readonly SortCriteria sortCriteria; - private readonly SortDirection sortDirection; - - public event Action> PageFetched; - - private SearchBeatmapSetsRequest getSetsRequest; - private SearchBeatmapSetsResponse lastResponse; - - private bool isLastPageFetched; - private bool isFetching => getSetsRequest != null; - public bool IsPastFirstPage { get; private set; } - public bool CanFetchNextPage => !isLastPageFetched && !isFetching; - - public BeatmapListingPager(IAPIProvider api, RulesetStore rulesets, string query, RulesetInfo ruleset, SearchCategory searchCategory = SearchCategory.Any, SortCriteria sortCriteria = SortCriteria.Ranked, SortDirection sortDirection = SortDirection.Descending) - { - this.api = api; - this.rulesets = rulesets; - this.query = query; - this.ruleset = ruleset; - this.searchCategory = searchCategory; - this.sortCriteria = sortCriteria; - this.sortDirection = sortDirection; - } - - public void FetchNextPage() - { - if (isFetching) - return; - - if (lastResponse != null) - IsPastFirstPage = true; - - getSetsRequest = new SearchBeatmapSetsRequest( - query, - ruleset, - lastResponse?.Cursor, - searchCategory, - sortCriteria, - sortDirection); - - getSetsRequest.Success += response => - { - var sets = response.BeatmapSets.Select(responseJson => responseJson.ToBeatmapSet(rulesets)).ToList(); - - if (sets.Count == 0) - isLastPageFetched = true; - - lastResponse = response; - getSetsRequest = null; - - PageFetched?.Invoke(sets); - }; - - api.Queue(getSetsRequest); - } - - public void Reset() - { - isLastPageFetched = false; - IsPastFirstPage = false; - - lastResponse = null; - - getSetsRequest?.Cancel(); - getSetsRequest = null; - } - } -} diff --git a/osu.Game/Overlays/BeatmapListingOverlay.cs b/osu.Game/Overlays/BeatmapListingOverlay.cs index 4aa754491c..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; @@ -34,8 +36,6 @@ namespace osu.Game.Overlays private NotFoundDrawable notFoundContent; private OverlayScrollContainer resultScrollContainer; - private const int pagination_scroll_distance = 500; - private bool shouldAddNextPage => resultScrollContainer.ScrollableExtent > 0 && resultScrollContainer.IsScrolledToEnd(pagination_scroll_distance); public BeatmapListingOverlay() : base(OverlayColourScheme.Blue) @@ -121,51 +121,45 @@ namespace osu.Game.Overlays loadingLayer.Show(); } + private Task panelLoadDelegate; + private void onSearchFinished(List beatmaps) { - //No matches case - if (!beatmaps.Any()) + var newPanels = beatmaps.Select(b => new GridBeatmapPanel(b) { - LoadComponentAsync(notFoundContent, addContentToPlaceholder, (cancellationToken = new CancellationTokenSource()).Token); - return; - } + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + }); - //New query case - if (!shouldAddNextPage) + if (filterControl.CurrentPage == 0) { - //Spawn new child - var newPanels = new FillFlowContainer + //No matches case + if (!newPanels.Any()) + { + LoadComponentAsync(notFoundContent, addContentToPlaceholder, (cancellationToken = new CancellationTokenSource()).Token); + return; + } + + // 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 = beatmaps.Select(b => new GridBeatmapPanel(b) - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - }) + ChildrenEnumerable = newPanels }; - foundContent = newPanels; - LoadComponentAsync(foundContent, addContentToPlaceholder, (cancellationToken = new CancellationTokenSource()).Token); + panelLoadDelegate = LoadComponentAsync(foundContent = content, addContentToPlaceholder, (cancellationToken = new CancellationTokenSource()).Token); } - - //Pagination case else { - beatmaps.ForEach(x => + panelLoadDelegate = LoadComponentsAsync(newPanels, loaded => { - LoadComponentAsync(new GridBeatmapPanel(x) - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - }, loaded => - { - foundContent.Add(loaded); - loaded.FadeIn(200, Easing.OutQuint); - }); + lastFetchDisplayedTime = Time.Current; + foundContent.AddRange(loaded); + loaded.ForEach(p => p.FadeIn(200, Easing.OutQuint)); }); } } @@ -173,6 +167,7 @@ namespace osu.Game.Overlays private void addContentToPlaceholder(Drawable content) { loadingLayer.Hide(); + lastFetchDisplayedTime = Time.Current; var lastContent = currentContent; @@ -242,12 +237,22 @@ namespace osu.Game.Overlays } } + private const double time_between_fetches = 500; + + private double lastFetchDisplayedTime; + protected override void Update() { base.Update(); - if (shouldAddNextPage) - filterControl.ShowMore(); + 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(); } } } From facde2c8e17edd2804dfee0921f29caafcd04648 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 14 May 2020 16:01:07 +0900 Subject: [PATCH 22/45] Remove unnecessary generic specification on cursor --- osu.Game/Extensions/WebRequestExtensions.cs | 15 ++++---------- osu.Game/Online/API/Requests/Cursor.cs | 20 +++++++++++++++++++ .../Online/API/Requests/ResponseWithCursor.cs | 11 +--------- .../API/Requests/SearchBeatmapSetsResponse.cs | 3 +-- 4 files changed, 26 insertions(+), 23 deletions(-) create mode 100644 osu.Game/Online/API/Requests/Cursor.cs diff --git a/osu.Game/Extensions/WebRequestExtensions.cs b/osu.Game/Extensions/WebRequestExtensions.cs index 80c8b147bf..b940c7498b 100644 --- a/osu.Game/Extensions/WebRequestExtensions.cs +++ b/osu.Game/Extensions/WebRequestExtensions.cs @@ -3,22 +3,15 @@ using osu.Framework.IO.Network; using osu.Framework.Extensions.IEnumerableExtensions; -using System.Collections.Generic; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using JetBrains.Annotations; +using osu.Game.Online.API.Requests; namespace osu.Game.Extensions { - public class Cursor - { - [UsedImplicitly] - [JsonExtensionData] - public IDictionary Properties; - } - 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 => 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 b0fe9eea28..d52e999722 100644 --- a/osu.Game/Online/API/Requests/ResponseWithCursor.cs +++ b/osu.Game/Online/API/Requests/ResponseWithCursor.cs @@ -7,16 +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 abstract class ResponseWithCursor : ResponseWithCursor where T : class - { - [JsonProperty("cursor")] - public T Cursor; + public Cursor Cursor; } } diff --git a/osu.Game/Online/API/Requests/SearchBeatmapSetsResponse.cs b/osu.Game/Online/API/Requests/SearchBeatmapSetsResponse.cs index a4d2c0e871..3c4fb11ed1 100644 --- a/osu.Game/Online/API/Requests/SearchBeatmapSetsResponse.cs +++ b/osu.Game/Online/API/Requests/SearchBeatmapSetsResponse.cs @@ -3,12 +3,11 @@ using System.Collections.Generic; using Newtonsoft.Json; -using osu.Game.Extensions; using osu.Game.Online.API.Requests.Responses; namespace osu.Game.Online.API.Requests { - public class SearchBeatmapSetsResponse : ResponseWithCursor + public class SearchBeatmapSetsResponse : ResponseWithCursor { [JsonProperty("beatmapsets")] public IEnumerable BeatmapSets; From 6bb06e9d611aeb9255c28a5936f59c0cdc2559f1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 13 May 2020 22:57:31 +0900 Subject: [PATCH 23/45] Expose CurrentDirectory bindable for consumption --- .../Graphics/UserInterfaceV2/DirectorySelector.cs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/osu.Game/Graphics/UserInterfaceV2/DirectorySelector.cs b/osu.Game/Graphics/UserInterfaceV2/DirectorySelector.cs index ee428c0047..6ea026ad3d 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] @@ -68,7 +68,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 }, }; - currentDirectory.BindValueChanged(updateDisplay, true); + CurrentDirectory.BindValueChanged(updateDisplay, true); } private void updateDisplay(ValueChangedEvent directory) @@ -86,9 +86,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 +97,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 } catch (Exception) { - currentDirectory.Value = directory.OldValue; - + CurrentDirectory.Value = directory.OldValue; this.FlashColour(Color4.Red, 300); } } From cb0b25ac55c7db5e6b1cc4a941971676690388ee Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 13 May 2020 22:57:41 +0900 Subject: [PATCH 24/45] Throw better exceptions from OsuStorage --- osu.Game/IO/OsuStorage.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/IO/OsuStorage.cs b/osu.Game/IO/OsuStorage.cs index 71b01ce479..8109631ef9 100644 --- a/osu.Game/IO/OsuStorage.cs +++ b/osu.Game/IO/OsuStorage.cs @@ -48,11 +48,14 @@ namespace osu.Game.IO var source = new DirectoryInfo(GetFullPath(".")); var destination = new DirectoryInfo(newLocation); + if (source.FullName == destination.FullName) + throw new ArgumentException("Destination provided is already the current location", 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); } From 0b73063a89ba56edc8fe0d364fe105375fd63c3f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 13 May 2020 22:58:05 +0900 Subject: [PATCH 25/45] Add basic (working) migration UI --- .../Sections/Maintenance/GeneralSettings.cs | 9 +- .../Maintenance/MigrationRunScreen.cs | 88 +++++++++++++++++++ .../Maintenance/MigrationSelectScreen.cs | 57 ++++++++++++ 3 files changed, 153 insertions(+), 1 deletion(-) create mode 100644 osu.Game/Overlays/Settings/Sections/Maintenance/MigrationRunScreen.cs create mode 100644 osu.Game/Overlays/Settings/Sections/Maintenance/MigrationSelectScreen.cs diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs index 832673703b..8bdeadae5c 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Framework.Screens; using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterface; using osu.Game.Scoring; @@ -26,8 +27,14 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance private TriangleButton undeleteButton; [BackgroundDependencyLoader] - private void load(BeatmapManager beatmaps, ScoreManager scores, SkinManager skins, DialogOverlay dialogOverlay) + private void load(BeatmapManager beatmaps, ScoreManager scores, SkinManager skins, DialogOverlay dialogOverlay, OsuGame game) { + Add(importBeatmapsButton = new SettingsButton + { + Text = "Migrate storage to new location", + Action = () => game.PerformFromScreen(menu => menu.Push(new MigrationSelectScreen())) + }); + if (beatmaps.SupportsImportFromStable) { Add(importBeatmapsButton = new SettingsButton 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..76f01dc4b9 --- /dev/null +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationRunScreen.cs @@ -0,0 +1,88 @@ +// 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; + +namespace osu.Game.Overlays.Settings.Sections.Maintenance +{ + public class MigrationRunScreen : OsuScreen + { + private readonly DirectoryInfo destination; + + [Resolved] + private OsuGame game { get; set; } + + public override bool AllowBackButton => false; + + public override bool AllowExternalScreenChange => false; + + public override bool DisallowExternalBeatmapRulesetChanges => 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, + Children = new Drawable[] + { + new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = "Migration in progress", + Font = OsuFont.Default.With(size: 48) + }, + new LoadingSpinner(true) + { + State = { Value = Visibility.Visible } + } + } + }, + }; + + Beatmap.Value = Beatmap.Default; + + migrationTask = Task.Run(() => game.Migrate(destination.FullName)) + .ContinueWith(t => + { + if (t.IsFaulted) + Logger.Log($"Error during migration: {t.Exception?.Message}", level: LogLevel.Error); + + Schedule(this.Exit); + }); + } + + 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..d1c2f6d6ee --- /dev/null +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationSelectScreen.cs @@ -0,0 +1,57 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Screens; +using osu.Game.Graphics.UserInterface; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Screens; + +namespace osu.Game.Overlays.Settings.Sections.Maintenance +{ + public class MigrationSelectScreen : OsuScreen + { + private DirectorySelector directorySelector; + + [BackgroundDependencyLoader] + private void load() + { + InternalChild = new GridContainer + { + RelativeSizeAxes = Axes.Both, + RowDimensions = new[] + { + new Dimension(GridSizeMode.Relative, 0.8f), + new Dimension(), + }, + Content = new[] + { + new Drawable[] { directorySelector = new DirectorySelector { RelativeSizeAxes = Axes.Both } }, + new Drawable[] + { + new OsuButton + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 300, + Text = "Start", + Action = start + }, + } + } + }; + } + + private void start() + { + var target = directorySelector.CurrentDirectory.Value; + if (target.GetDirectories().Length > 0 || target.GetFiles().Length > 0) + target = target.CreateSubdirectory("osu-lazer"); + + ValidForResume = false; + this.Push(new MigrationRunScreen(target)); + } + } +} From 06f507496a0d4bb8865d226ea9af6d220a5ab4fb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 13 May 2020 23:02:28 +0900 Subject: [PATCH 26/45] Delete migration source if no files exist after completion --- osu.Game/IO/OsuStorage.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/IO/OsuStorage.cs b/osu.Game/IO/OsuStorage.cs index 8109631ef9..443f4fdb69 100644 --- a/osu.Game/IO/OsuStorage.cs +++ b/osu.Game/IO/OsuStorage.cs @@ -87,6 +87,9 @@ namespace osu.Game.IO dir.Delete(true); } + + if (target.GetFiles().Length == 0 && target.GetDirectories().Length == 0) + target.Delete(); } private static void copyRecursive(DirectoryInfo source, DirectoryInfo destination, bool topLevelExcludes = true) From d04079f6ab7911b5d7f8fd633019993137b0fc65 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 14 May 2020 17:40:30 +0900 Subject: [PATCH 27/45] Fix directory selector not masking properly --- .../UserInterfaceV2/DirectorySelector.cs | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/osu.Game/Graphics/UserInterfaceV2/DirectorySelector.cs b/osu.Game/Graphics/UserInterfaceV2/DirectorySelector.cs index 6ea026ad3d..ae34281bfb 100644 --- a/osu.Game/Graphics/UserInterfaceV2/DirectorySelector.cs +++ b/osu.Game/Graphics/UserInterfaceV2/DirectorySelector.cs @@ -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,7 +71,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 } } } - }, + } }; CurrentDirectory.BindValueChanged(updateDisplay, true); From 4e4a779d6827d76323702fad328c451c3e3fb000 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 14 May 2020 17:40:43 +0900 Subject: [PATCH 28/45] Improve overall UI --- .../Sections/General/UpdateSettings.cs | 10 +- .../Sections/Maintenance/GeneralSettings.cs | 7 -- .../Maintenance/MigrationRunScreen.cs | 29 ++++- .../Maintenance/MigrationSelectScreen.cs | 105 +++++++++++++++--- 4 files changed, 123 insertions(+), 28 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs index 188c9c05ef..b5d07ee7b1 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 { @@ -13,7 +15,7 @@ namespace osu.Game.Overlays.Settings.Sections.General protected override string Header => "Updates"; [BackgroundDependencyLoader] - private void load(Storage storage, OsuConfigManager config) + 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/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs index 8bdeadae5c..1dd079a8ab 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs @@ -5,7 +5,6 @@ using System.Linq; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Framework.Screens; using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterface; using osu.Game.Scoring; @@ -29,12 +28,6 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance [BackgroundDependencyLoader] private void load(BeatmapManager beatmaps, ScoreManager scores, SkinManager skins, DialogOverlay dialogOverlay, OsuGame game) { - Add(importBeatmapsButton = new SettingsButton - { - Text = "Migrate storage to new location", - Action = () => game.PerformFromScreen(menu => menu.Push(new MigrationSelectScreen())) - }); - if (beatmaps.SupportsImportFromStable) { Add(importBeatmapsButton = new SettingsButton diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationRunScreen.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationRunScreen.cs index 76f01dc4b9..b29cd0d630 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationRunScreen.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationRunScreen.cs @@ -12,6 +12,7 @@ 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 { @@ -28,6 +29,8 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance public override bool DisallowExternalBeatmapRulesetChanges => true; + public override bool HideOverlaysOnEnter => true; + private Task migrationTask; public MigrationRunScreen(DirectoryInfo destination) @@ -47,6 +50,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance Direction = FillDirection.Vertical, Anchor = Anchor.Centre, Origin = Anchor.Centre, + Spacing = new Vector2(10), Children = new Drawable[] { new OsuSpriteText @@ -54,12 +58,26 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance Anchor = Anchor.Centre, Origin = Anchor.Centre, Text = "Migration in progress", - Font = OsuFont.Default.With(size: 48) + 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) + }, } }, }; @@ -76,6 +94,13 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance }); } + 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 diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationSelectScreen.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationSelectScreen.cs index d1c2f6d6ee..c1aa7f095c 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationSelectScreen.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationSelectScreen.cs @@ -1,13 +1,21 @@ // 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 { @@ -15,40 +23,101 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance { private DirectorySelector directorySelector; + public override bool AllowExternalScreenChange => false; + + public override bool DisallowExternalBeatmapRulesetChanges => true; + + public override bool HideOverlaysOnEnter => true; + [BackgroundDependencyLoader] - private void load() + private void load(OsuGame game, Storage storage, OsuColour colours) { - InternalChild = new GridContainer + 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, - RowDimensions = new[] + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(0.5f, 0.8f), + Children = new Drawable[] { - new Dimension(GridSizeMode.Relative, 0.8f), - new Dimension(), - }, - Content = new[] - { - new Drawable[] { directorySelector = new DirectorySelector { RelativeSizeAxes = Axes.Both } }, - new Drawable[] + new Box { - new OsuButton + Colour = colours.GreySeafoamDark, + RelativeSizeAxes = Axes.Both, + }, + new GridContainer + { + RelativeSizeAxes = Axes.Both, + RowDimensions = new[] { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Width = 300, - Text = "Start", - Action = start + 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; - if (target.GetDirectories().Length > 0 || target.GetFiles().Length > 0) - target = target.CreateSubdirectory("osu-lazer"); + + 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; this.Push(new MigrationRunScreen(target)); From a5826116479b547084fa2d3d0fab0211ab83089a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 14 May 2020 19:05:35 +0900 Subject: [PATCH 29/45] Add test coverage --- .../Settings/TestSceneMigrationScreens.cs | 36 +++++++++++++++++++ .../Maintenance/MigrationRunScreen.cs | 6 ++-- .../Maintenance/MigrationSelectScreen.cs | 10 +++--- 3 files changed, 46 insertions(+), 6 deletions(-) create mode 100644 osu.Game.Tests/Visual/Settings/TestSceneMigrationScreens.cs 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/Overlays/Settings/Sections/Maintenance/MigrationRunScreen.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationRunScreen.cs index b29cd0d630..b0b61554eb 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationRunScreen.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationRunScreen.cs @@ -20,7 +20,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance { private readonly DirectoryInfo destination; - [Resolved] + [Resolved(canBeNull: true)] private OsuGame game { get; set; } public override bool AllowBackButton => false; @@ -84,7 +84,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance Beatmap.Value = Beatmap.Default; - migrationTask = Task.Run(() => game.Migrate(destination.FullName)) + migrationTask = Task.Run(PerformMigration) .ContinueWith(t => { if (t.IsFaulted) @@ -94,6 +94,8 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance }); } + protected virtual void PerformMigration() => game?.Migrate(destination.FullName); + public override void OnEntering(IScreen last) { base.OnEntering(last); diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationSelectScreen.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationSelectScreen.cs index c1aa7f095c..79d842a617 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationSelectScreen.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationSelectScreen.cs @@ -29,10 +29,10 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance public override bool HideOverlaysOnEnter => true; - [BackgroundDependencyLoader] + [BackgroundDependencyLoader(true)] private void load(OsuGame game, Storage storage, OsuColour colours) { - game.Toolbar.Hide(); + game?.Toolbar.Hide(); // begin selection in the parent directory of the current storage location var initialPath = new DirectoryInfo(storage.GetFullPath(string.Empty)).Parent?.FullName; @@ -115,12 +115,14 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance } catch (Exception e) { - Logger.Log($"Error during migration: {e?.Message}", level: LogLevel.Error); + Logger.Log($"Error during migration: {e.Message}", level: LogLevel.Error); return; } ValidForResume = false; - this.Push(new MigrationRunScreen(target)); + BeginMigration(target); } + + protected virtual void BeginMigration(DirectoryInfo target) => this.Push(new MigrationRunScreen(target)); } } From f3b1c32a85050ace4ad7f410913ea3665312e3de Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 14 May 2020 20:18:57 +0900 Subject: [PATCH 30/45] Update test logic for new exception type --- osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs index ef2b20de64..7a20bd364b 100644 --- a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs +++ b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs @@ -211,7 +211,7 @@ 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 { From 42f446faa9a8d19a0a49d970b8139e7bf28cf4c1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 14 May 2020 21:10:04 +0900 Subject: [PATCH 31/45] Fix remaining test failure --- osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs index b5d07ee7b1..95a1868392 100644 --- a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs @@ -14,7 +14,7 @@ namespace osu.Game.Overlays.Settings.Sections.General { protected override string Header => "Updates"; - [BackgroundDependencyLoader] + [BackgroundDependencyLoader(true)] private void load(Storage storage, OsuConfigManager config, OsuGame game) { Add(new SettingsEnumDropdown @@ -34,7 +34,7 @@ namespace osu.Game.Overlays.Settings.Sections.General Add(new SettingsButton { Text = "Change folder location...", - Action = () => game.PerformFromScreen(menu => menu.Push(new MigrationSelectScreen())) + Action = () => game?.PerformFromScreen(menu => menu.Push(new MigrationSelectScreen())) }); } } From 155e918ca3063b4c03146144cc3b8f9a035d5566 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 14 May 2020 21:40:26 +0900 Subject: [PATCH 32/45] Remove unused parameter --- .../Overlays/Settings/Sections/Maintenance/GeneralSettings.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs index 1dd079a8ab..832673703b 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs @@ -26,7 +26,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance private TriangleButton undeleteButton; [BackgroundDependencyLoader] - private void load(BeatmapManager beatmaps, ScoreManager scores, SkinManager skins, DialogOverlay dialogOverlay, OsuGame game) + private void load(BeatmapManager beatmaps, ScoreManager scores, SkinManager skins, DialogOverlay dialogOverlay) { if (beatmaps.SupportsImportFromStable) { From ef8375b442d78dea8737ef91b6bb5b3ecbf6e7ee Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 14 May 2020 22:42:42 +0900 Subject: [PATCH 33/45] Add protection against migrating to a nested folder --- .../NonVisual/CustomDataDirectoryTest.cs | 24 +++++++++++++++++++ osu.Game/IO/OsuStorage.cs | 6 +++++ 2 files changed, 30 insertions(+) diff --git a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs index ef2b20de64..433067ffdd 100644 --- a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs +++ b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs @@ -220,6 +220,30 @@ namespace osu.Game.Tests.NonVisual } } + [Test] + public void TestMigrationToNestedTargetFails() + { + using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestMigrationToSameTargetFails))) + { + try + { + var osu = loadOsu(host); + + Assert.DoesNotThrow(() => osu.Migrate(customPath)); + + string subFolder = Path.Combine(customPath, "sub"); + + Directory.CreateDirectory(subFolder); + + Assert.Throws(() => osu.Migrate(subFolder)); + } + finally + { + host.Exit(); + } + } + } + private OsuGameBase loadOsu(GameHost host) { var osu = new OsuGameBase(); diff --git a/osu.Game/IO/OsuStorage.cs b/osu.Game/IO/OsuStorage.cs index 71b01ce479..7c0af16a63 100644 --- a/osu.Game/IO/OsuStorage.cs +++ b/osu.Game/IO/OsuStorage.cs @@ -48,6 +48,12 @@ namespace osu.Game.IO var source = new DirectoryInfo(GetFullPath(".")); var destination = new DirectoryInfo(newLocation); + if (source.FullName == destination.FullName) + throw new ArgumentException("Destination provided is already the current location", nameof(newLocation)); + + if (destination.FullName.Contains(source.FullName)) + 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) { From 827d75b152f17af984b9823dc4322a185fdaaa55 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 14 May 2020 22:44:27 +0900 Subject: [PATCH 34/45] Revert "Add protection against migrating to a nested folder" This reverts commit ef8375b442d78dea8737ef91b6bb5b3ecbf6e7ee. --- .../NonVisual/CustomDataDirectoryTest.cs | 24 ------------------- osu.Game/IO/OsuStorage.cs | 6 ----- 2 files changed, 30 deletions(-) diff --git a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs index 433067ffdd..ef2b20de64 100644 --- a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs +++ b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs @@ -220,30 +220,6 @@ namespace osu.Game.Tests.NonVisual } } - [Test] - public void TestMigrationToNestedTargetFails() - { - using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestMigrationToSameTargetFails))) - { - try - { - var osu = loadOsu(host); - - Assert.DoesNotThrow(() => osu.Migrate(customPath)); - - string subFolder = Path.Combine(customPath, "sub"); - - Directory.CreateDirectory(subFolder); - - Assert.Throws(() => osu.Migrate(subFolder)); - } - finally - { - host.Exit(); - } - } - } - private OsuGameBase loadOsu(GameHost host) { var osu = new OsuGameBase(); diff --git a/osu.Game/IO/OsuStorage.cs b/osu.Game/IO/OsuStorage.cs index 7c0af16a63..71b01ce479 100644 --- a/osu.Game/IO/OsuStorage.cs +++ b/osu.Game/IO/OsuStorage.cs @@ -48,12 +48,6 @@ namespace osu.Game.IO var source = new DirectoryInfo(GetFullPath(".")); var destination = new DirectoryInfo(newLocation); - if (source.FullName == destination.FullName) - throw new ArgumentException("Destination provided is already the current location", nameof(newLocation)); - - if (destination.FullName.Contains(source.FullName)) - 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) { From 364aa5aa129af4dbf996d4942b2f4884b6d53dbd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 14 May 2020 22:42:42 +0900 Subject: [PATCH 35/45] Add protection against migrating to a nested folder --- .../NonVisual/CustomDataDirectoryTest.cs | 24 +++++++++++++++++++ osu.Game/IO/OsuStorage.cs | 3 +++ 2 files changed, 27 insertions(+) diff --git a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs index 7a20bd364b..e8f052cdeb 100644 --- a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs +++ b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs @@ -220,6 +220,30 @@ namespace osu.Game.Tests.NonVisual } } + [Test] + public void TestMigrationToNestedTargetFails() + { + using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestMigrationToSameTargetFails))) + { + try + { + var osu = loadOsu(host); + + Assert.DoesNotThrow(() => osu.Migrate(customPath)); + + string subFolder = Path.Combine(customPath, "sub"); + + Directory.CreateDirectory(subFolder); + + Assert.Throws(() => osu.Migrate(subFolder)); + } + finally + { + host.Exit(); + } + } + } + private OsuGameBase loadOsu(GameHost host) { var osu = new OsuGameBase(); diff --git a/osu.Game/IO/OsuStorage.cs b/osu.Game/IO/OsuStorage.cs index 443f4fdb69..e3888c0c28 100644 --- a/osu.Game/IO/OsuStorage.cs +++ b/osu.Game/IO/OsuStorage.cs @@ -51,6 +51,9 @@ namespace osu.Game.IO if (source.FullName == destination.FullName) throw new ArgumentException("Destination provided is already the current location", nameof(newLocation)); + if (destination.FullName.Contains(source.FullName)) + 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) { From 6ec55eb400a480dbf475565fdf48589b7e0ea50d Mon Sep 17 00:00:00 2001 From: Shivam Date: Thu, 14 May 2020 21:51:39 +0200 Subject: [PATCH 36/45] Give mappool scene its own video --- osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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, From 1768beb690a9733f3ebf966c6576b8006c011984 Mon Sep 17 00:00:00 2001 From: Shivam Date: Thu, 14 May 2020 21:52:10 +0200 Subject: [PATCH 37/45] Rename class SeeingEditorScreen to SeedingEditorScreen --- .../Screens/Editors/SeedingEditorScreen.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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); } } From 25bbb02999481de9adecaa38f51e3e6293018bcd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 13 May 2020 22:57:41 +0900 Subject: [PATCH 38/45] Throw better exceptions from OsuStorage --- osu.Game/IO/OsuStorage.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/IO/OsuStorage.cs b/osu.Game/IO/OsuStorage.cs index 71b01ce479..8109631ef9 100644 --- a/osu.Game/IO/OsuStorage.cs +++ b/osu.Game/IO/OsuStorage.cs @@ -48,11 +48,14 @@ namespace osu.Game.IO var source = new DirectoryInfo(GetFullPath(".")); var destination = new DirectoryInfo(newLocation); + if (source.FullName == destination.FullName) + throw new ArgumentException("Destination provided is already the current location", 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); } From 19f117ae53fe3058ed251bd687a7cb8266e75cf8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 14 May 2020 20:18:57 +0900 Subject: [PATCH 39/45] Update test logic for new exception type --- osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs index ef2b20de64..7a20bd364b 100644 --- a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs +++ b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs @@ -211,7 +211,7 @@ 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 { From 0690d81bbb1bc7ee969b7e07050a565138b5b5c0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 14 May 2020 22:42:42 +0900 Subject: [PATCH 40/45] Add protection against migrating to a nested folder --- .../NonVisual/CustomDataDirectoryTest.cs | 48 +++++++++++++++++++ osu.Game/IO/OsuStorage.cs | 9 +++- 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs index 7a20bd364b..2a98a6dbc6 100644 --- a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs +++ b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs @@ -220,6 +220,54 @@ namespace osu.Game.Tests.NonVisual } } + [Test] + public void TestMigrationToNestedTargetFails() + { + using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestMigrationToSameTargetFails))) + { + try + { + var osu = loadOsu(host); + + Assert.DoesNotThrow(() => osu.Migrate(customPath)); + + string subFolder = Path.Combine(customPath, "sub"); + + 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 subFolder = customPath + "sub"; + + Directory.CreateDirectory(subFolder); + + osu.Migrate(subFolder); + } + finally + { + host.Exit(); + } + } + } + private OsuGameBase loadOsu(GameHost host) { var osu = new OsuGameBase(); diff --git a/osu.Game/IO/OsuStorage.cs b/osu.Game/IO/OsuStorage.cs index 8109631ef9..ac28a05375 100644 --- a/osu.Game/IO/OsuStorage.cs +++ b/osu.Game/IO/OsuStorage.cs @@ -48,9 +48,16 @@ namespace osu.Game.IO var source = new DirectoryInfo(GetFullPath(".")); var destination = new DirectoryInfo(newLocation); - if (source.FullName == destination.FullName) + // 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) { From 7641507c90f770bab9e096dad63132001cc532d5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 15 May 2020 10:45:57 +0900 Subject: [PATCH 41/45] Ensure test directories are deleted before subsequent run --- osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs index 2a98a6dbc6..d69bf94ee2 100644 --- a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs +++ b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs @@ -233,6 +233,9 @@ namespace osu.Game.Tests.NonVisual string subFolder = Path.Combine(customPath, "sub"); + if (Directory.Exists(subFolder)) + Directory.Delete(subFolder, true); + Directory.CreateDirectory(subFolder); Assert.Throws(() => osu.Migrate(subFolder)); @@ -255,11 +258,14 @@ namespace osu.Game.Tests.NonVisual Assert.DoesNotThrow(() => osu.Migrate(customPath)); - string subFolder = customPath + "sub"; + string seeminglySubFolder = customPath + "sub"; - Directory.CreateDirectory(subFolder); + if (Directory.Exists(seeminglySubFolder)) + Directory.Delete(seeminglySubFolder, true); - osu.Migrate(subFolder); + Directory.CreateDirectory(seeminglySubFolder); + + osu.Migrate(seeminglySubFolder); } finally { From 94cf99bf978305b7f45eb0bfb1323c34e3525bb5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 15 May 2020 12:21:02 +0900 Subject: [PATCH 42/45] Fix mute button falling off the screen when UI scaling is used --- osu.Game/Overlays/VolumeOverlay.cs | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) 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); From aea192080a51f014d79da5caad8ce70507d65a74 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 15 May 2020 13:02:46 +0900 Subject: [PATCH 43/45] Fix incorrect storage name --- osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs index d69bf94ee2..743c924bbd 100644 --- a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs +++ b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs @@ -223,7 +223,7 @@ namespace osu.Game.Tests.NonVisual [Test] public void TestMigrationToNestedTargetFails() { - using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestMigrationToSameTargetFails))) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestMigrationToNestedTargetFails))) { try { From 6ca102bc3fb44d751986c3daa141825d8ed094ec Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 15 May 2020 13:19:03 +0900 Subject: [PATCH 44/45] Attempt delete operations more than once --- osu.Game/IO/OsuStorage.cs | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/osu.Game/IO/OsuStorage.cs b/osu.Game/IO/OsuStorage.cs index 5393cbf7ae..499bcb4063 100644 --- a/osu.Game/IO/OsuStorage.cs +++ b/osu.Game/IO/OsuStorage.cs @@ -84,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()) @@ -92,11 +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) - target.Delete(); + attemptOperation(target.Delete); } private static void copyRecursive(DirectoryInfo source, DirectoryInfo destination, bool topLevelExcludes = true) @@ -109,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()) @@ -121,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); } } } From 9b6525bb03a09323b38ec1e2296d63ce1a73a5d8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 15 May 2020 18:44:47 +0900 Subject: [PATCH 45/45] Fix applied platform/user offsets being incorrect when rate adjust mods are active --- osu.Game/Screens/Play/GameplayClockContainer.cs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) 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) + { + } + } } }