1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-14 07:42:57 +08:00

Merge branch 'master' into remove-requiredtypes

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

View File

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

View File

@ -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;

View File

@ -3,6 +3,7 @@
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Testing;
using osu.Framework.Timing;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Skinning;
@ -12,11 +13,30 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
{
public class TestSceneTaikoScroller : TaikoSkinnableTestScene
{
private readonly ManualClock clock = new ManualClock();
private bool reversed;
public TestSceneTaikoScroller()
{
AddStep("Load scroller", () => SetContents(() => new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.Scroller), _ => Empty())));
AddStep("Load scroller", () => SetContents(() =>
new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.Scroller), _ => Empty())
{
Clock = new FramedClock(clock),
Height = 0.4f,
}));
AddToggleStep("Toggle passing", passing => this.ChildrenOfType<LegacyTaikoScroller>().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;
}
}
}

View File

@ -17,6 +17,8 @@ namespace osu.Game.Rulesets.Taiko.Skinning
{
public class LegacyTaikoScroller : CompositeDrawable
{
public Bindable<JudgementResult> LastResult = new Bindable<JudgementResult>();
public LegacyTaikoScroller()
{
RelativeSizeAxes = Axes.Both;
@ -50,37 +52,38 @@ namespace osu.Game.Rulesets.Taiko.Skinning
}, true);
}
public Bindable<JudgementResult> LastResult = new Bindable<JudgementResult>();
protected override void Update()
{
base.Update();
while (true)
// store X before checking wide enough so if we perform layout there is no positional discrepancy.
float currentX = (InternalChildren?.FirstOrDefault()?.X ?? 0) - (float)Clock.ElapsedFrameTime * 0.1f;
// ensure we have enough sprites
if (!InternalChildren.Any()
|| InternalChildren.First().ScreenSpaceDrawQuad.Width * InternalChildren.Count < ScreenSpaceDrawQuad.Width * 2)
AddInternal(new ScrollerSprite { Passing = passing });
var first = InternalChildren.First();
var last = InternalChildren.Last();
foreach (var sprite in InternalChildren)
{
float? additiveX = null;
// add the x coordinates and perform re-layout on all sprites as spacing may change with gameplay scale.
sprite.X = currentX;
currentX += sprite.DrawWidth;
}
foreach (var sprite in InternalChildren)
{
// add the x coordinates and perform re-layout on all sprites as spacing may change with gameplay scale.
sprite.X = additiveX ??= sprite.X - (float)Time.Elapsed * 0.1f;
if (first.ScreenSpaceDrawQuad.TopLeft.X >= ScreenSpaceDrawQuad.TopLeft.X)
{
foreach (var internalChild in InternalChildren)
internalChild.X -= first.DrawWidth;
}
additiveX += sprite.DrawWidth - 1;
if (sprite.X + sprite.DrawWidth < 0)
sprite.Expire();
}
var last = InternalChildren.LastOrDefault();
// only break from this loop once we have saturated horizontal space completely.
if (last != null && last.ScreenSpaceDrawQuad.TopRight.X >= ScreenSpaceDrawQuad.TopRight.X)
break;
AddInternal(new ScrollerSprite
{
Passing = passing
});
if (last.ScreenSpaceDrawQuad.TopRight.X <= ScreenSpaceDrawQuad.TopRight.X)
{
foreach (var internalChild in InternalChildren)
internalChild.X += first.DrawWidth;
}
}

View File

@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Taiko.UI
{
new BarLineGenerator<BarLine>(Beatmap).BarLines.ForEach(bar => Playfield.Add(bar.Major ? new DrawableBarLineMajor(bar) : new DrawableBarLine(bar)));
AddInternal(scroller = new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.Scroller), _ => Empty())
FrameStableComponents.Add(scroller = new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.Scroller), _ => Empty())
{
RelativeSizeAxes = Axes.X,
Depth = float.MaxValue

View File

@ -13,18 +13,16 @@ namespace osu.Game.Rulesets.Taiko.UI
private const float default_relative_height = TaikoPlayfield.DEFAULT_HEIGHT / 768;
private const float default_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;
}
}
}

View File

@ -211,7 +211,61 @@ namespace osu.Game.Tests.NonVisual
var osu = loadOsu(host);
Assert.DoesNotThrow(() => osu.Migrate(customPath));
Assert.Throws<InvalidOperationException>(() => osu.Migrate(customPath));
Assert.Throws<ArgumentException>(() => osu.Migrate(customPath));
}
finally
{
host.Exit();
}
}
}
[Test]
public void TestMigrationToNestedTargetFails()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestMigrationToNestedTargetFails)))
{
try
{
var osu = loadOsu(host);
Assert.DoesNotThrow(() => osu.Migrate(customPath));
string subFolder = Path.Combine(customPath, "sub");
if (Directory.Exists(subFolder))
Directory.Delete(subFolder, true);
Directory.CreateDirectory(subFolder);
Assert.Throws<ArgumentException>(() => osu.Migrate(subFolder));
}
finally
{
host.Exit();
}
}
}
[Test]
public void TestMigrationToSeeminglyNestedTarget()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestMigrationToSeeminglyNestedTarget)))
{
try
{
var osu = loadOsu(host);
Assert.DoesNotThrow(() => osu.Migrate(customPath));
string seeminglySubFolder = customPath + "sub";
if (Directory.Exists(seeminglySubFolder))
Directory.Delete(seeminglySubFolder, true);
Directory.CreateDirectory(seeminglySubFolder);
osu.Migrate(seeminglySubFolder);
}
finally
{

View File

@ -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;

View File

@ -0,0 +1,36 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.IO;
using System.Threading;
using osu.Framework.Screens;
using osu.Game.Overlays.Settings.Sections.Maintenance;
namespace osu.Game.Tests.Visual.Settings
{
public class TestSceneMigrationScreens : ScreenTestScene
{
public TestSceneMigrationScreens()
{
AddStep("Push screen", () => Stack.Push(new TestMigrationSelectScreen()));
}
private class TestMigrationSelectScreen : MigrationSelectScreen
{
protected override void BeginMigration(DirectoryInfo target) => this.Push(new TestMigrationRunScreen());
private class TestMigrationRunScreen : MigrationRunScreen
{
protected override void PerformMigration()
{
Thread.Sleep(3000);
}
public TestMigrationRunScreen()
: base(null)
{
}
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -48,11 +48,21 @@ namespace osu.Game.IO
var source = new DirectoryInfo(GetFullPath("."));
var destination = new DirectoryInfo(newLocation);
// using Uri is the easiest way to check equality and contains (https://stackoverflow.com/a/7710620)
var sourceUri = new Uri(source.FullName + Path.DirectorySeparatorChar);
var destinationUri = new Uri(destination.FullName + Path.DirectorySeparatorChar);
if (sourceUri == destinationUri)
throw new ArgumentException("Destination provided is already the current location", nameof(newLocation));
if (sourceUri.IsBaseOf(destinationUri))
throw new ArgumentException("Destination provided is inside the source", nameof(newLocation));
// ensure the new location has no files present, else hard abort
if (destination.Exists)
{
if (destination.GetFiles().Length > 0 || destination.GetDirectories().Length > 0)
throw new InvalidOperationException("Migration destination already has files present");
throw new ArgumentException("Destination provided already has files or directories present", nameof(newLocation));
deleteRecursive(destination);
}
@ -74,7 +84,7 @@ namespace osu.Game.IO
if (topLevelExcludes && IGNORE_FILES.Contains(fi.Name))
continue;
fi.Delete();
attemptOperation(() => fi.Delete());
}
foreach (DirectoryInfo dir in target.GetDirectories())
@ -82,8 +92,11 @@ namespace osu.Game.IO
if (topLevelExcludes && IGNORE_DIRECTORIES.Contains(dir.Name))
continue;
dir.Delete(true);
attemptOperation(() => dir.Delete(true));
}
if (target.GetFiles().Length == 0 && target.GetDirectories().Length == 0)
attemptOperation(target.Delete);
}
private static void copyRecursive(DirectoryInfo source, DirectoryInfo destination, bool topLevelExcludes = true)
@ -96,7 +109,7 @@ namespace osu.Game.IO
if (topLevelExcludes && IGNORE_FILES.Contains(fi.Name))
continue;
attemptCopy(fi, Path.Combine(destination.FullName, fi.Name));
attemptOperation(() => fi.CopyTo(Path.Combine(destination.FullName, fi.Name), true));
}
foreach (DirectoryInfo dir in source.GetDirectories())
@ -108,24 +121,27 @@ namespace osu.Game.IO
}
}
private static void attemptCopy(System.IO.FileInfo fileInfo, string destination)
/// <summary>
/// Attempt an IO operation multiple times and only throw if none of the attempts succeed.
/// </summary>
/// <param name="action">The action to perform.</param>
/// <param name="attempts">The number of attempts (250ms wait between each).</param>
private static void attemptOperation(Action action, int attempts = 10)
{
int tries = 5;
while (true)
{
try
{
fileInfo.CopyTo(destination, true);
action();
return;
}
catch (Exception)
{
if (tries-- == 0)
if (attempts-- == 0)
throw;
}
Thread.Sleep(50);
Thread.Sleep(250);
}
}
}

View File

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

View File

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

View File

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

View File

@ -22,25 +22,46 @@ namespace osu.Game.Overlays.BeatmapListing
{
public class BeatmapListingFilterControl : CompositeDrawable
{
/// <summary>
/// Fired when a search finishes. Contains only new items in the case of pagination.
/// </summary>
public Action<List<BeatmapSetInfo>> SearchFinished;
/// <summary>
/// Fired when search criteria change.
/// </summary>
public Action SearchStarted;
/// <summary>
/// True when pagination has reached the end of available results.
/// </summary>
private bool noMoreResults;
/// <summary>
/// The current page fetched of results (zero index).
/// </summary>
public int CurrentPage { get; private set; }
private readonly BeatmapListingSearchControl searchControl;
private readonly BeatmapListingSortTabControl sortControl;
private readonly Box sortControlBackground;
private ScheduledDelegate queryChangedDebounce;
private SearchBeatmapSetsRequest getSetsRequest;
private SearchBeatmapSetsResponse lastResponse;
[Resolved]
private IAPIProvider api { get; set; }
[Resolved]
private RulesetStore rulesets { get; set; }
private readonly BeatmapListingSearchControl searchControl;
private readonly BeatmapListingSortTabControl sortControl;
private readonly Box sortControlBackground;
private SearchBeatmapSetsRequest getSetsRequest;
public BeatmapListingFilterControl()
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
InternalChild = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
@ -114,51 +135,84 @@ namespace osu.Game.Overlays.BeatmapListing
sortDirection.BindValueChanged(_ => queueUpdateSearch());
}
private ScheduledDelegate queryChangedDebounce;
public void TakeFocus() => searchControl.TakeFocus();
/// <summary>
/// Fetch the next page of results. May result in a no-op if a fetch is already in progress, or if there are no results left.
/// </summary>
public void FetchNextPage()
{
// there may be no results left.
if (noMoreResults)
return;
// there may already be an active request.
if (getSetsRequest != null)
return;
if (lastResponse != null)
CurrentPage++;
performRequest();
}
private void queueUpdateSearch(bool queryTextChanged = false)
{
SearchStarted?.Invoke();
getSetsRequest?.Cancel();
resetSearch();
queryChangedDebounce?.Cancel();
queryChangedDebounce = Scheduler.AddDelayed(updateSearch, queryTextChanged ? 500 : 100);
queryChangedDebounce = Scheduler.AddDelayed(() =>
{
resetSearch();
FetchNextPage();
}, queryTextChanged ? 500 : 100);
}
private void updateSearch()
private void performRequest()
{
getSetsRequest = new SearchBeatmapSetsRequest(searchControl.Query.Value, searchControl.Ruleset.Value)
{
SearchCategory = searchControl.Category.Value,
SortCriteria = sortControl.Current.Value,
SortDirection = sortControl.SortDirection.Value,
Genre = searchControl.Genre.Value,
Language = searchControl.Language.Value
};
getSetsRequest = new SearchBeatmapSetsRequest(
searchControl.Query.Value,
searchControl.Ruleset.Value,
lastResponse?.Cursor,
searchControl.Category.Value,
sortControl.Current.Value,
sortControl.SortDirection.Value);
getSetsRequest.Success += response => Schedule(() => onSearchFinished(response));
getSetsRequest.Success += response =>
{
var sets = response.BeatmapSets.Select(responseJson => responseJson.ToBeatmapSet(rulesets)).ToList();
if (sets.Count == 0)
noMoreResults = true;
lastResponse = response;
getSetsRequest = null;
SearchFinished?.Invoke(sets);
};
api.Queue(getSetsRequest);
}
private void onSearchFinished(SearchBeatmapSetsResponse response)
private void resetSearch()
{
var beatmaps = response.BeatmapSets.Select(r => r.ToBeatmapSet(rulesets)).ToList();
noMoreResults = false;
CurrentPage = 0;
searchControl.BeatmapSet = response.Total == 0 ? null : beatmaps.First();
lastResponse = null;
SearchFinished?.Invoke(beatmaps);
getSetsRequest?.Cancel();
getSetsRequest = null;
queryChangedDebounce?.Cancel();
}
protected override void Dispose(bool isDisposing)
{
getSetsRequest?.Cancel();
queryChangedDebounce?.Cancel();
resetSearch();
base.Dispose(isDisposing);
}
public void TakeFocus() => searchControl.TakeFocus();
}
}

View File

@ -4,7 +4,9 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
@ -30,6 +32,10 @@ namespace osu.Game.Overlays
private Drawable currentContent;
private LoadingLayer loadingLayer;
private Container panelTarget;
private FillFlowContainer<BeatmapPanel> foundContent;
private NotFoundDrawable notFoundContent;
private OverlayScrollContainer resultScrollContainer;
public BeatmapListingOverlay()
: base(OverlayColourScheme.Blue)
@ -48,7 +54,7 @@ namespace osu.Game.Overlays
RelativeSizeAxes = Axes.Both,
Colour = ColourProvider.Background6
},
new OverlayScrollContainer
resultScrollContainer = new OverlayScrollContainer
{
RelativeSizeAxes = Axes.Both,
ScrollbarVisible = false,
@ -80,9 +86,14 @@ namespace osu.Game.Overlays
{
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
Padding = new MarginPadding { Horizontal = 20 }
},
loadingLayer = new LoadingLayer(panelTarget)
Padding = new MarginPadding { Horizontal = 20 },
Children = new Drawable[]
{
foundContent = new FillFlowContainer<BeatmapPanel>(),
notFoundContent = new NotFoundDrawable(),
loadingLayer = new LoadingLayer(panelTarget)
}
}
}
},
}
@ -110,34 +121,53 @@ namespace osu.Game.Overlays
loadingLayer.Show();
}
private Task panelLoadDelegate;
private void onSearchFinished(List<BeatmapSetInfo> beatmaps)
{
if (!beatmaps.Any())
var newPanels = beatmaps.Select<BeatmapSetInfo, BeatmapPanel>(b => new GridBeatmapPanel(b)
{
LoadComponentAsync(new NotFoundDrawable(), addContentToPlaceholder, (cancellationToken = new CancellationTokenSource()).Token);
return;
}
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
});
var newPanels = new FillFlowContainer<BeatmapPanel>
if (filterControl.CurrentPage == 0)
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(10),
Alpha = 0,
Margin = new MarginPadding { Vertical = 15 },
ChildrenEnumerable = beatmaps.Select<BeatmapSetInfo, BeatmapPanel>(b => new GridBeatmapPanel(b)
//No matches case
if (!newPanels.Any())
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
})
};
LoadComponentAsync(notFoundContent, addContentToPlaceholder, (cancellationToken = new CancellationTokenSource()).Token);
return;
}
LoadComponentAsync(newPanels, addContentToPlaceholder, (cancellationToken = new CancellationTokenSource()).Token);
// spawn new children with the contained so we only clear old content at the last moment.
var content = new FillFlowContainer<BeatmapPanel>
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(10),
Alpha = 0,
Margin = new MarginPadding { Vertical = 15 },
ChildrenEnumerable = newPanels
};
panelLoadDelegate = LoadComponentAsync(foundContent = content, addContentToPlaceholder, (cancellationToken = new CancellationTokenSource()).Token);
}
else
{
panelLoadDelegate = LoadComponentsAsync(newPanels, loaded =>
{
lastFetchDisplayedTime = Time.Current;
foundContent.AddRange(loaded);
loaded.ForEach(p => p.FadeIn(200, Easing.OutQuint));
});
}
}
private void addContentToPlaceholder(Drawable content)
{
loadingLayer.Hide();
lastFetchDisplayedTime = Time.Current;
var lastContent = currentContent;
@ -149,11 +179,14 @@ namespace osu.Game.Overlays
// If the auto-size computation is delayed until fade out completes, the background remain high for too long making the resulting transition to the smaller height look weird.
// At the same time, if the last content's height is bypassed immediately, there is a period where the new content is at Alpha = 0 when the auto-sized height will be 0.
// To resolve both of these issues, the bypass is delayed until a point when the content transitions (fade-in and fade-out) overlap and it looks good to do so.
lastContent.Delay(25).Schedule(() => lastContent.BypassAutoSizeAxes = Axes.Y);
lastContent.Delay(25).Schedule(() => lastContent.BypassAutoSizeAxes = Axes.Y).Then().Schedule(() => panelTarget.Remove(lastContent));
}
panelTarget.Add(currentContent = content);
currentContent.FadeIn(200, Easing.OutQuint);
if (!content.IsAlive)
panelTarget.Add(content);
content.FadeIn(200, Easing.OutQuint);
currentContent = content;
}
protected override void Dispose(bool isDisposing)
@ -203,5 +236,23 @@ namespace osu.Game.Overlays
});
}
}
private const double time_between_fetches = 500;
private double lastFetchDisplayedTime;
protected override void Update()
{
base.Update();
const int pagination_scroll_distance = 500;
bool shouldShowMore = panelLoadDelegate?.IsCompleted != false
&& Time.Current - lastFetchDisplayedTime > time_between_fetches
&& (resultScrollContainer.ScrollableExtent > 0 && resultScrollContainer.IsScrolledToEnd(pagination_scroll_distance));
if (shouldShowMore)
filterControl.FetchNextPage();
}
}
}

View File

@ -4,7 +4,9 @@
using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Platform;
using osu.Framework.Screens;
using osu.Game.Configuration;
using osu.Game.Overlays.Settings.Sections.Maintenance;
namespace osu.Game.Overlays.Settings.Sections.General
{
@ -12,8 +14,8 @@ namespace osu.Game.Overlays.Settings.Sections.General
{
protected override string Header => "Updates";
[BackgroundDependencyLoader]
private void load(Storage storage, OsuConfigManager config)
[BackgroundDependencyLoader(true)]
private void load(Storage storage, OsuConfigManager config, OsuGame game)
{
Add(new SettingsEnumDropdown<ReleaseStream>
{
@ -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()))
});
}
}
}

View File

@ -0,0 +1,115 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.IO;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Logging;
using osu.Framework.Screens;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Screens;
using osuTK;
namespace osu.Game.Overlays.Settings.Sections.Maintenance
{
public class MigrationRunScreen : OsuScreen
{
private readonly DirectoryInfo destination;
[Resolved(canBeNull: true)]
private OsuGame game { get; set; }
public override bool AllowBackButton => false;
public override bool AllowExternalScreenChange => false;
public override bool DisallowExternalBeatmapRulesetChanges => true;
public override bool HideOverlaysOnEnter => true;
private Task migrationTask;
public MigrationRunScreen(DirectoryInfo destination)
{
this.destination = destination;
}
protected override void LoadComplete()
{
base.LoadComplete();
InternalChildren = new Drawable[]
{
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Spacing = new Vector2(10),
Children = new Drawable[]
{
new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Text = "Migration in progress",
Font = OsuFont.Default.With(size: 40)
},
new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Text = "This could take a few minutes depending on the speed of your disk(s).",
Font = OsuFont.Default.With(size: 30)
},
new LoadingSpinner(true)
{
State = { Value = Visibility.Visible }
},
new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Text = "Please avoid interacting with the game!",
Font = OsuFont.Default.With(size: 30)
},
}
},
};
Beatmap.Value = Beatmap.Default;
migrationTask = Task.Run(PerformMigration)
.ContinueWith(t =>
{
if (t.IsFaulted)
Logger.Log($"Error during migration: {t.Exception?.Message}", level: LogLevel.Error);
Schedule(this.Exit);
});
}
protected virtual void PerformMigration() => game?.Migrate(destination.FullName);
public override void OnEntering(IScreen last)
{
base.OnEntering(last);
this.FadeOut().Delay(250).Then().FadeIn(250);
}
public override bool OnExiting(IScreen next)
{
// block until migration is finished
if (migrationTask?.IsCompleted == false)
return true;
return base.OnExiting(next);
}
}
}

View File

@ -0,0 +1,128 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.IO;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Framework.Screens;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Screens;
using osuTK;
namespace osu.Game.Overlays.Settings.Sections.Maintenance
{
public class MigrationSelectScreen : OsuScreen
{
private DirectorySelector directorySelector;
public override bool AllowExternalScreenChange => false;
public override bool DisallowExternalBeatmapRulesetChanges => true;
public override bool HideOverlaysOnEnter => true;
[BackgroundDependencyLoader(true)]
private void load(OsuGame game, Storage storage, OsuColour colours)
{
game?.Toolbar.Hide();
// begin selection in the parent directory of the current storage location
var initialPath = new DirectoryInfo(storage.GetFullPath(string.Empty)).Parent?.FullName;
InternalChild = new Container
{
Masking = true,
CornerRadius = 10,
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(0.5f, 0.8f),
Children = new Drawable[]
{
new Box
{
Colour = colours.GreySeafoamDark,
RelativeSizeAxes = Axes.Both,
},
new GridContainer
{
RelativeSizeAxes = Axes.Both,
RowDimensions = new[]
{
new Dimension(),
new Dimension(GridSizeMode.Relative, 0.8f),
new Dimension(),
},
Content = new[]
{
new Drawable[]
{
new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Text = "Please select a new location",
Font = OsuFont.Default.With(size: 40)
},
},
new Drawable[]
{
directorySelector = new DirectorySelector(initialPath)
{
RelativeSizeAxes = Axes.Both,
}
},
new Drawable[]
{
new TriangleButton
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 300,
Text = "Begin folder migration",
Action = start
},
}
}
}
}
};
}
public override void OnSuspending(IScreen next)
{
base.OnSuspending(next);
this.FadeOut(250);
}
private void start()
{
var target = directorySelector.CurrentDirectory.Value;
try
{
if (target.GetDirectories().Length > 0 || target.GetFiles().Length > 0)
target = target.CreateSubdirectory("osu-lazer");
}
catch (Exception e)
{
Logger.Log($"Error during migration: {e.Message}", level: LogLevel.Error);
return;
}
ValidForResume = false;
BeginMigration(target);
}
protected virtual void BeginMigration(DirectoryInfo target) => this.Push(new MigrationRunScreen(target));
}
}

View File

@ -46,6 +46,13 @@ namespace osu.Game.Overlays
Width = 300,
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);

View File

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

View File

@ -27,7 +27,8 @@ namespace osu.Game.Rulesets.Mods
public override string Description => "Watch a perfect automated play through the song.";
public override 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) };

View File

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

View File

@ -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;

View File

@ -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)

View File

@ -78,10 +78,10 @@ namespace osu.Game.Screens.Play
// Lazer's audio timings in general doesn't match stable. This is the result of user testing, albeit limited.
// 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)
{
}
}
}
}

View File

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

View File

@ -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)

View File

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

View File

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

View File

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

View File

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