Merge branch 'master' into score-refactor/isolated-serialisation

This commit is contained in:
Dean Herbert 2021-11-01 15:49:25 +09:00
commit b98faf6159
28 changed files with 628 additions and 360 deletions

View File

@ -128,7 +128,7 @@ namespace osu.Game.Tests.Online
private void addAvailabilityCheckStep(string description, Func<BeatmapAvailability> expected) private void addAvailabilityCheckStep(string description, Func<BeatmapAvailability> expected)
{ {
AddAssert(description, () => availabilityTracker.Availability.Value.Equals(expected.Invoke())); AddUntilStep(description, () => availabilityTracker.Availability.Value.Equals(expected.Invoke()));
} }
private static BeatmapInfo getTestBeatmapInfo(string archiveFile) private static BeatmapInfo getTestBeatmapInfo(string archiveFile)

View File

@ -29,6 +29,15 @@ namespace osu.Game.Tests.Skins.IO
assertCorrectMetadata(import1, "test skin [skin]", "skinner", osu); assertCorrectMetadata(import1, "test skin [skin]", "skinner", osu);
}); });
[Test]
public Task TestSingleImportWeirdIniFileCase() => runSkinTest(async osu =>
{
var import1 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOskWithIni("test skin", "skinner", iniFilename: "Skin.InI"), "skin.osk"));
// When the import filename doesn't match, it should be appended (and update the skin.ini).
assertCorrectMetadata(import1, "test skin [skin]", "skinner", osu);
});
[Test] [Test]
public Task TestSingleImportMatchingFilename() => runSkinTest(async osu => public Task TestSingleImportMatchingFilename() => runSkinTest(async osu =>
{ {
@ -190,11 +199,11 @@ namespace osu.Game.Tests.Skins.IO
return zipStream; return zipStream;
} }
private MemoryStream createOskWithIni(string name, string author, bool makeUnique = false) private MemoryStream createOskWithIni(string name, string author, bool makeUnique = false, string iniFilename = @"skin.ini")
{ {
var zipStream = new MemoryStream(); var zipStream = new MemoryStream();
using var zip = ZipArchive.Create(); using var zip = ZipArchive.Create();
zip.AddEntry("skin.ini", generateSkinIni(name, author, makeUnique)); zip.AddEntry(iniFilename, generateSkinIni(name, author, makeUnique));
zip.SaveTo(zipStream); zip.SaveTo(zipStream);
return zipStream; return zipStream;
} }

View File

@ -76,7 +76,7 @@ namespace osu.Game.Tournament.Components
{ {
new TournamentSpriteText new TournamentSpriteText
{ {
Text = Beatmap.GetDisplayTitleRomanisable(false), Text = Beatmap.GetDisplayTitleRomanisable(false, false),
Font = OsuFont.Torus.With(weight: FontWeight.Bold), Font = OsuFont.Torus.With(weight: FontWeight.Bold),
}, },
new FillFlowContainer new FillFlowContainer

View File

@ -16,9 +16,9 @@ namespace osu.Game.Beatmaps
/// <summary> /// <summary>
/// A user-presentable display title representing this beatmap, with localisation handling for potentially romanisable fields. /// A user-presentable display title representing this beatmap, with localisation handling for potentially romanisable fields.
/// </summary> /// </summary>
public static RomanisableString GetDisplayTitleRomanisable(this IBeatmapInfo beatmapInfo, bool includeDifficultyName = true) public static RomanisableString GetDisplayTitleRomanisable(this IBeatmapInfo beatmapInfo, bool includeDifficultyName = true, bool includeCreator = true)
{ {
var metadata = getClosestMetadata(beatmapInfo).GetDisplayTitleRomanisable(); var metadata = getClosestMetadata(beatmapInfo).GetDisplayTitleRomanisable(includeCreator);
if (includeDifficultyName) if (includeDifficultyName)
{ {

View File

@ -34,9 +34,9 @@ namespace osu.Game.Beatmaps
/// <summary> /// <summary>
/// A user-presentable display title representing this beatmap, with localisation handling for potentially romanisable fields. /// A user-presentable display title representing this beatmap, with localisation handling for potentially romanisable fields.
/// </summary> /// </summary>
public static RomanisableString GetDisplayTitleRomanisable(this IBeatmapMetadataInfo metadataInfo) public static RomanisableString GetDisplayTitleRomanisable(this IBeatmapMetadataInfo metadataInfo, bool includeCreator = true)
{ {
string author = string.IsNullOrEmpty(metadataInfo.Author) ? string.Empty : $"({metadataInfo.Author})"; string author = !includeCreator || string.IsNullOrEmpty(metadataInfo.Author) ? string.Empty : $"({metadataInfo.Author})";
string artistUnicode = string.IsNullOrEmpty(metadataInfo.ArtistUnicode) ? metadataInfo.Artist : metadataInfo.ArtistUnicode; string artistUnicode = string.IsNullOrEmpty(metadataInfo.ArtistUnicode) ? metadataInfo.Artist : metadataInfo.ArtistUnicode;
string titleUnicode = string.IsNullOrEmpty(metadataInfo.TitleUnicode) ? metadataInfo.Title : metadataInfo.TitleUnicode; string titleUnicode = string.IsNullOrEmpty(metadataInfo.TitleUnicode) ? metadataInfo.Title : metadataInfo.TitleUnicode;

View File

@ -13,6 +13,9 @@ namespace osu.Game.Beatmaps
protected override ArchiveDownloadRequest<BeatmapSetInfo> CreateDownloadRequest(BeatmapSetInfo set, bool minimiseDownloadSize) => protected override ArchiveDownloadRequest<BeatmapSetInfo> CreateDownloadRequest(BeatmapSetInfo set, bool minimiseDownloadSize) =>
new DownloadBeatmapSetRequest(set, minimiseDownloadSize); new DownloadBeatmapSetRequest(set, minimiseDownloadSize);
public override ArchiveDownloadRequest<BeatmapSetInfo> GetExistingDownload(BeatmapSetInfo model)
=> CurrentDownloads.Find(r => r.Model.OnlineID == model.OnlineID);
public BeatmapModelDownloader(IBeatmapModelManager beatmapModelManager, IAPIProvider api, GameHost host = null) public BeatmapModelDownloader(IBeatmapModelManager beatmapModelManager, IAPIProvider api, GameHost host = null)
: base(beatmapModelManager, api, host) : base(beatmapModelManager, api, host)
{ {

View File

@ -30,7 +30,7 @@ namespace osu.Game.Database
private readonly IModelManager<TModel> modelManager; private readonly IModelManager<TModel> modelManager;
private readonly IAPIProvider api; private readonly IAPIProvider api;
private readonly List<ArchiveDownloadRequest<TModel>> currentDownloads = new List<ArchiveDownloadRequest<TModel>>(); protected readonly List<ArchiveDownloadRequest<TModel>> CurrentDownloads = new List<ArchiveDownloadRequest<TModel>>();
protected ModelDownloader(IModelManager<TModel> modelManager, IAPIProvider api, IIpcHost importHost = null) protected ModelDownloader(IModelManager<TModel> modelManager, IAPIProvider api, IIpcHost importHost = null)
{ {
@ -74,7 +74,7 @@ namespace osu.Game.Database
if (!imported.Any()) if (!imported.Any())
downloadFailed.Value = new WeakReference<ArchiveDownloadRequest<TModel>>(request); downloadFailed.Value = new WeakReference<ArchiveDownloadRequest<TModel>>(request);
currentDownloads.Remove(request); CurrentDownloads.Remove(request);
}, TaskCreationOptions.LongRunning); }, TaskCreationOptions.LongRunning);
}; };
@ -86,7 +86,7 @@ namespace osu.Game.Database
return true; return true;
}; };
currentDownloads.Add(request); CurrentDownloads.Add(request);
PostNotification?.Invoke(notification); PostNotification?.Invoke(notification);
api.PerformAsync(request); api.PerformAsync(request);
@ -96,7 +96,7 @@ namespace osu.Game.Database
void triggerFailure(Exception error) void triggerFailure(Exception error)
{ {
currentDownloads.Remove(request); CurrentDownloads.Remove(request);
downloadFailed.Value = new WeakReference<ArchiveDownloadRequest<TModel>>(request); downloadFailed.Value = new WeakReference<ArchiveDownloadRequest<TModel>>(request);
@ -107,7 +107,7 @@ namespace osu.Game.Database
} }
} }
public ArchiveDownloadRequest<TModel> GetExistingDownload(TModel model) => currentDownloads.Find(r => r.Model.Equals(model)); public abstract ArchiveDownloadRequest<TModel> GetExistingDownload(TModel model);
private bool canDownload(TModel model) => GetExistingDownload(model) == null && api != null; private bool canDownload(TModel model) => GetExistingDownload(model) == null && api != null;

View File

@ -119,6 +119,16 @@ namespace osu.Game.Localisation
/// </summary> /// </summary>
public static LocalisableString ShowCursorInScreenshots => new TranslatableString(getKey(@"show_cursor_in_screenshots"), @"Show menu cursor in screenshots"); public static LocalisableString ShowCursorInScreenshots => new TranslatableString(getKey(@"show_cursor_in_screenshots"), @"Show menu cursor in screenshots");
/// <summary>
/// "Video"
/// </summary>
public static LocalisableString VideoHeader => new TranslatableString(getKey(@"video_header"), @"Video");
/// <summary>
/// "Use hardware acceleration"
/// </summary>
public static LocalisableString UseHardwareAcceleration => new TranslatableString(getKey(@"use_hardware_acceleration"), @"Use hardware acceleration");
private static string getKey(string key) => $"{prefix}:{key}"; private static string getKey(string key) => $"{prefix}:{key}";
} }
} }

View File

@ -0,0 +1,155 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Game.Beatmaps;
using osu.Game.Online.API;
#nullable enable
namespace osu.Game.Online
{
public class BeatmapDownloadTracker : DownloadTracker<IBeatmapSetInfo>
{
[Resolved(CanBeNull = true)]
protected BeatmapManager? Manager { get; private set; }
private ArchiveDownloadRequest<BeatmapSetInfo>? attachedRequest;
public BeatmapDownloadTracker(IBeatmapSetInfo trackedItem)
: base(trackedItem)
{
}
private IBindable<WeakReference<BeatmapSetInfo>>? managerUpdated;
private IBindable<WeakReference<BeatmapSetInfo>>? managerRemoved;
private IBindable<WeakReference<ArchiveDownloadRequest<BeatmapSetInfo>>>? managerDownloadBegan;
private IBindable<WeakReference<ArchiveDownloadRequest<BeatmapSetInfo>>>? managerDownloadFailed;
[BackgroundDependencyLoader(true)]
private void load()
{
if (Manager == null)
return;
// Used to interact with manager classes that don't support interface types. Will eventually be replaced.
var beatmapSetInfo = new BeatmapSetInfo { OnlineBeatmapSetID = TrackedItem.OnlineID };
if (Manager.IsAvailableLocally(beatmapSetInfo))
UpdateState(DownloadState.LocallyAvailable);
else
attachDownload(Manager.GetExistingDownload(beatmapSetInfo));
managerDownloadBegan = Manager.DownloadBegan.GetBoundCopy();
managerDownloadBegan.BindValueChanged(downloadBegan);
managerDownloadFailed = Manager.DownloadFailed.GetBoundCopy();
managerDownloadFailed.BindValueChanged(downloadFailed);
managerUpdated = Manager.ItemUpdated.GetBoundCopy();
managerUpdated.BindValueChanged(itemUpdated);
managerRemoved = Manager.ItemRemoved.GetBoundCopy();
managerRemoved.BindValueChanged(itemRemoved);
}
private void downloadBegan(ValueChangedEvent<WeakReference<ArchiveDownloadRequest<BeatmapSetInfo>>> weakRequest)
{
if (weakRequest.NewValue.TryGetTarget(out var request))
{
Schedule(() =>
{
if (checkEquality(request.Model, TrackedItem))
attachDownload(request);
});
}
}
private void downloadFailed(ValueChangedEvent<WeakReference<ArchiveDownloadRequest<BeatmapSetInfo>>> weakRequest)
{
if (weakRequest.NewValue.TryGetTarget(out var request))
{
Schedule(() =>
{
if (checkEquality(request.Model, TrackedItem))
attachDownload(null);
});
}
}
private void attachDownload(ArchiveDownloadRequest<BeatmapSetInfo>? request)
{
if (attachedRequest != null)
{
attachedRequest.Failure -= onRequestFailure;
attachedRequest.DownloadProgressed -= onRequestProgress;
attachedRequest.Success -= onRequestSuccess;
}
attachedRequest = request;
if (attachedRequest != null)
{
if (attachedRequest.Progress == 1)
{
UpdateProgress(1);
UpdateState(DownloadState.Importing);
}
else
{
UpdateProgress(attachedRequest.Progress);
UpdateState(DownloadState.Downloading);
attachedRequest.Failure += onRequestFailure;
attachedRequest.DownloadProgressed += onRequestProgress;
attachedRequest.Success += onRequestSuccess;
}
}
else
{
UpdateState(DownloadState.NotDownloaded);
}
}
private void onRequestSuccess(string _) => Schedule(() => UpdateState(DownloadState.Importing));
private void onRequestProgress(float progress) => Schedule(() => UpdateProgress(progress));
private void onRequestFailure(Exception e) => Schedule(() => attachDownload(null));
private void itemUpdated(ValueChangedEvent<WeakReference<BeatmapSetInfo>> weakItem)
{
if (weakItem.NewValue.TryGetTarget(out var item))
{
Schedule(() =>
{
if (checkEquality(item, TrackedItem))
UpdateState(DownloadState.LocallyAvailable);
});
}
}
private void itemRemoved(ValueChangedEvent<WeakReference<BeatmapSetInfo>> weakItem)
{
if (weakItem.NewValue.TryGetTarget(out var item))
{
Schedule(() =>
{
if (checkEquality(item, TrackedItem))
UpdateState(DownloadState.NotDownloaded);
});
}
}
private bool checkEquality(IBeatmapSetInfo x, IBeatmapSetInfo y) => x.OnlineID == y.OnlineID;
#region Disposal
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
attachDownload(null);
}
#endregion
}
}

View File

@ -0,0 +1,39 @@
// 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.Bindables;
using osu.Framework.Graphics;
#nullable enable
namespace osu.Game.Online
{
public abstract class DownloadTracker<T> : Component
where T : class
{
public readonly T TrackedItem;
/// <summary>
/// Holds the current download state of the download - whether is has already been downloaded, is in progress, or is not downloaded.
/// </summary>
public IBindable<DownloadState> State => state;
private readonly Bindable<DownloadState> state = new Bindable<DownloadState>();
/// <summary>
/// The progress of an active download.
/// </summary>
public IBindableNumber<double> Progress => progress;
private readonly BindableNumber<double> progress = new BindableNumber<double> { MinValue = 0, MaxValue = 1 };
protected DownloadTracker(T trackedItem)
{
TrackedItem = trackedItem;
}
protected void UpdateState(DownloadState newState) => state.Value = newState;
protected void UpdateProgress(double newProgress) => progress.Value = newProgress;
}
}

View File

@ -1,196 +0,0 @@
// 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 JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Containers;
using osu.Game.Database;
using osu.Game.Online.API;
namespace osu.Game.Online
{
/// <summary>
/// A component which tracks a <typeparamref name="TModel"/> through potential download/import/deletion.
/// </summary>
public abstract class DownloadTrackingComposite<TModel, TModelManager> : CompositeDrawable
where TModel : class, IEquatable<TModel>
where TModelManager : class, IModelDownloader<TModel>, IModelManager<TModel>
{
protected readonly Bindable<TModel> Model = new Bindable<TModel>();
[Resolved(CanBeNull = true)]
protected TModelManager Manager { get; private set; }
/// <summary>
/// Holds the current download state of the <typeparamref name="TModel"/>, whether is has already been downloaded, is in progress, or is not downloaded.
/// </summary>
protected readonly Bindable<DownloadState> State = new Bindable<DownloadState>();
protected readonly BindableNumber<double> Progress = new BindableNumber<double> { MinValue = 0, MaxValue = 1 };
protected DownloadTrackingComposite(TModel model = null)
{
Model.Value = model;
}
private IBindable<WeakReference<TModel>> managerUpdated;
private IBindable<WeakReference<TModel>> managerRemoved;
private IBindable<WeakReference<ArchiveDownloadRequest<TModel>>> managerDownloadBegan;
private IBindable<WeakReference<ArchiveDownloadRequest<TModel>>> managerDownloadFailed;
[BackgroundDependencyLoader(true)]
private void load()
{
Model.BindValueChanged(modelInfo =>
{
if (modelInfo.NewValue == null)
attachDownload(null);
else if (IsModelAvailableLocally())
State.Value = DownloadState.LocallyAvailable;
else
attachDownload(Manager?.GetExistingDownload(modelInfo.NewValue));
}, true);
if (Manager == null)
return;
managerDownloadBegan = Manager.DownloadBegan.GetBoundCopy();
managerDownloadBegan.BindValueChanged(downloadBegan);
managerDownloadFailed = Manager.DownloadFailed.GetBoundCopy();
managerDownloadFailed.BindValueChanged(downloadFailed);
managerUpdated = Manager.ItemUpdated.GetBoundCopy();
managerUpdated.BindValueChanged(itemUpdated);
managerRemoved = Manager.ItemRemoved.GetBoundCopy();
managerRemoved.BindValueChanged(itemRemoved);
}
/// <summary>
/// Checks that a database model matches the one expected to be downloaded.
/// </summary>
/// <example>
/// For online play, this could be used to check that the databased model matches the online beatmap.
/// </example>
/// <param name="databasedModel">The model in database.</param>
protected virtual bool VerifyDatabasedModel([NotNull] TModel databasedModel) => true;
/// <summary>
/// Whether the given model is available in the database.
/// By default, this calls <see cref="IModelManager{TModel}.IsAvailableLocally"/>,
/// but can be overriden to add additional checks for verifying the model in database.
/// </summary>
protected virtual bool IsModelAvailableLocally() => Manager?.IsAvailableLocally(Model.Value) == true;
private void downloadBegan(ValueChangedEvent<WeakReference<ArchiveDownloadRequest<TModel>>> weakRequest)
{
if (weakRequest.NewValue.TryGetTarget(out var request))
{
Schedule(() =>
{
if (request.Model.Equals(Model.Value))
attachDownload(request);
});
}
}
private void downloadFailed(ValueChangedEvent<WeakReference<ArchiveDownloadRequest<TModel>>> weakRequest)
{
if (weakRequest.NewValue.TryGetTarget(out var request))
{
Schedule(() =>
{
if (request.Model.Equals(Model.Value))
attachDownload(null);
});
}
}
private ArchiveDownloadRequest<TModel> attachedRequest;
private void attachDownload(ArchiveDownloadRequest<TModel> request)
{
if (attachedRequest != null)
{
attachedRequest.Failure -= onRequestFailure;
attachedRequest.DownloadProgressed -= onRequestProgress;
attachedRequest.Success -= onRequestSuccess;
}
attachedRequest = request;
if (attachedRequest != null)
{
if (attachedRequest.Progress == 1)
{
Progress.Value = 1;
State.Value = DownloadState.Importing;
}
else
{
Progress.Value = attachedRequest.Progress;
State.Value = DownloadState.Downloading;
attachedRequest.Failure += onRequestFailure;
attachedRequest.DownloadProgressed += onRequestProgress;
attachedRequest.Success += onRequestSuccess;
}
}
else
{
State.Value = DownloadState.NotDownloaded;
}
}
private void onRequestSuccess(string _) => Schedule(() => State.Value = DownloadState.Importing);
private void onRequestProgress(float progress) => Schedule(() => Progress.Value = progress);
private void onRequestFailure(Exception e) => Schedule(() => attachDownload(null));
private void itemUpdated(ValueChangedEvent<WeakReference<TModel>> weakItem)
{
if (weakItem.NewValue.TryGetTarget(out var item))
{
Schedule(() =>
{
if (!item.Equals(Model.Value))
return;
if (!VerifyDatabasedModel(item))
{
State.Value = DownloadState.NotDownloaded;
return;
}
State.Value = DownloadState.LocallyAvailable;
});
}
}
private void itemRemoved(ValueChangedEvent<WeakReference<TModel>> weakItem)
{
if (weakItem.NewValue.TryGetTarget(out var item))
{
Schedule(() =>
{
if (item.Equals(Model.Value))
State.Value = DownloadState.NotDownloaded;
});
}
}
#region Disposal
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
State.UnbindAll();
attachDownload(null);
}
#endregion
}
}

View File

@ -2,8 +2,10 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Linq; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Framework.Threading; using osu.Framework.Threading;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
@ -16,19 +18,27 @@ namespace osu.Game.Online.Rooms
/// This differs from a regular download tracking composite as this accounts for the /// This differs from a regular download tracking composite as this accounts for the
/// databased beatmap set's checksum, to disallow from playing with an altered version of the beatmap. /// databased beatmap set's checksum, to disallow from playing with an altered version of the beatmap.
/// </summary> /// </summary>
public class OnlinePlayBeatmapAvailabilityTracker : DownloadTrackingComposite<BeatmapSetInfo, BeatmapManager> public sealed class OnlinePlayBeatmapAvailabilityTracker : CompositeDrawable
{ {
public readonly IBindable<PlaylistItem> SelectedItem = new Bindable<PlaylistItem>(); public readonly IBindable<PlaylistItem> SelectedItem = new Bindable<PlaylistItem>();
// Required to allow child components to update. Can potentially be replaced with a `CompositeComponent` class if or when we make one.
protected override bool RequiresChildrenUpdate => true;
[Resolved]
private BeatmapManager beatmapManager { get; set; }
/// <summary> /// <summary>
/// The availability state of the currently selected playlist item. /// The availability state of the currently selected playlist item.
/// </summary> /// </summary>
public IBindable<BeatmapAvailability> Availability => availability; public IBindable<BeatmapAvailability> Availability => availability;
private readonly Bindable<BeatmapAvailability> availability = new Bindable<BeatmapAvailability>(BeatmapAvailability.LocallyAvailable()); private readonly Bindable<BeatmapAvailability> availability = new Bindable<BeatmapAvailability>(BeatmapAvailability.NotDownloaded());
private ScheduledDelegate progressUpdate; private ScheduledDelegate progressUpdate;
private BeatmapDownloadTracker downloadTracker;
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.LoadComplete(); base.LoadComplete();
@ -40,58 +50,38 @@ namespace osu.Game.Online.Rooms
if (item.NewValue == null) if (item.NewValue == null)
return; return;
Model.Value = item.NewValue.Beatmap.Value.BeatmapSet; downloadTracker?.RemoveAndDisposeImmediately();
downloadTracker = new BeatmapDownloadTracker(item.NewValue.Beatmap.Value.BeatmapSet);
downloadTracker.State.BindValueChanged(_ => updateAvailability());
downloadTracker.Progress.BindValueChanged(_ =>
{
if (downloadTracker.State.Value != DownloadState.Downloading)
return;
// incoming progress changes are going to be at a very high rate.
// we don't want to flood the network with this, so rate limit how often we send progress updates.
if (progressUpdate?.Completed != false)
progressUpdate = Scheduler.AddDelayed(updateAvailability, progressUpdate == null ? 0 : 500);
});
AddInternal(downloadTracker);
}, true); }, true);
Progress.BindValueChanged(_ =>
{
if (State.Value != DownloadState.Downloading)
return;
// incoming progress changes are going to be at a very high rate.
// we don't want to flood the network with this, so rate limit how often we send progress updates.
if (progressUpdate?.Completed != false)
progressUpdate = Scheduler.AddDelayed(updateAvailability, progressUpdate == null ? 0 : 500);
});
State.BindValueChanged(_ => updateAvailability(), true);
}
protected override bool VerifyDatabasedModel(BeatmapSetInfo databasedSet)
{
int beatmapId = SelectedItem.Value?.Beatmap.Value.OnlineID ?? -1;
string checksum = SelectedItem.Value?.Beatmap.Value.MD5Hash;
var matchingBeatmap = databasedSet.Beatmaps.FirstOrDefault(b => b.OnlineBeatmapID == beatmapId && b.MD5Hash == checksum);
if (matchingBeatmap == null)
{
Logger.Log("The imported beatmap set does not match the online version.", LoggingTarget.Runtime, LogLevel.Important);
return false;
}
return true;
}
protected override bool IsModelAvailableLocally()
{
int onlineId = SelectedItem.Value.Beatmap.Value.OnlineID;
string checksum = SelectedItem.Value.Beatmap.Value.MD5Hash;
var beatmap = Manager.QueryBeatmap(b => b.OnlineBeatmapID == onlineId && b.MD5Hash == checksum);
return beatmap?.BeatmapSet.DeletePending == false;
} }
private void updateAvailability() private void updateAvailability()
{ {
switch (State.Value) if (downloadTracker == null)
return;
switch (downloadTracker.State.Value)
{ {
case DownloadState.NotDownloaded: case DownloadState.NotDownloaded:
availability.Value = BeatmapAvailability.NotDownloaded(); availability.Value = BeatmapAvailability.NotDownloaded();
break; break;
case DownloadState.Downloading: case DownloadState.Downloading:
availability.Value = BeatmapAvailability.Downloading((float)Progress.Value); availability.Value = BeatmapAvailability.Downloading((float)downloadTracker.Progress.Value);
break; break;
case DownloadState.Importing: case DownloadState.Importing:
@ -99,12 +89,27 @@ namespace osu.Game.Online.Rooms
break; break;
case DownloadState.LocallyAvailable: case DownloadState.LocallyAvailable:
availability.Value = BeatmapAvailability.LocallyAvailable(); bool hashMatches = checkHashValidity();
availability.Value = hashMatches ? BeatmapAvailability.LocallyAvailable() : BeatmapAvailability.NotDownloaded();
// only display a message to the user if a download seems to have just completed.
if (!hashMatches && downloadTracker.Progress.Value == 1)
Logger.Log("The imported beatmap set does not match the online version.", LoggingTarget.Runtime, LogLevel.Important);
break; break;
default: default:
throw new ArgumentOutOfRangeException(nameof(State)); throw new ArgumentOutOfRangeException();
} }
} }
private bool checkHashValidity()
{
int onlineId = SelectedItem.Value.Beatmap.Value.OnlineID;
string checksum = SelectedItem.Value.Beatmap.Value.MD5Hash;
return beatmapManager.QueryBeatmap(b => b.OnlineBeatmapID == onlineId && b.MD5Hash == checksum && !b.BeatmapSet.DeletePending) != null;
}
} }
} }

View File

@ -0,0 +1,157 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Game.Online.API;
using osu.Game.Scoring;
#nullable enable
namespace osu.Game.Online
{
public class ScoreDownloadTracker : DownloadTracker<ScoreInfo>
{
[Resolved(CanBeNull = true)]
protected ScoreManager? Manager { get; private set; }
private ArchiveDownloadRequest<ScoreInfo>? attachedRequest;
public ScoreDownloadTracker(ScoreInfo trackedItem)
: base(trackedItem)
{
}
private IBindable<WeakReference<ScoreInfo>>? managerUpdated;
private IBindable<WeakReference<ScoreInfo>>? managerRemoved;
private IBindable<WeakReference<ArchiveDownloadRequest<ScoreInfo>>>? managerDownloadBegan;
private IBindable<WeakReference<ArchiveDownloadRequest<ScoreInfo>>>? managerDownloadFailed;
[BackgroundDependencyLoader(true)]
private void load()
{
if (Manager == null)
return;
// Used to interact with manager classes that don't support interface types. Will eventually be replaced.
var scoreInfo = new ScoreInfo { OnlineScoreID = TrackedItem.OnlineScoreID };
if (Manager.IsAvailableLocally(scoreInfo))
UpdateState(DownloadState.LocallyAvailable);
else
attachDownload(Manager.GetExistingDownload(scoreInfo));
managerDownloadBegan = Manager.DownloadBegan.GetBoundCopy();
managerDownloadBegan.BindValueChanged(downloadBegan);
managerDownloadFailed = Manager.DownloadFailed.GetBoundCopy();
managerDownloadFailed.BindValueChanged(downloadFailed);
managerUpdated = Manager.ItemUpdated.GetBoundCopy();
managerUpdated.BindValueChanged(itemUpdated);
managerRemoved = Manager.ItemRemoved.GetBoundCopy();
managerRemoved.BindValueChanged(itemRemoved);
}
private void downloadBegan(ValueChangedEvent<WeakReference<ArchiveDownloadRequest<ScoreInfo>>> weakRequest)
{
if (weakRequest.NewValue.TryGetTarget(out var request))
{
Schedule(() =>
{
if (checkEquality(request.Model, TrackedItem))
attachDownload(request);
});
}
}
private void downloadFailed(ValueChangedEvent<WeakReference<ArchiveDownloadRequest<ScoreInfo>>> weakRequest)
{
if (weakRequest.NewValue.TryGetTarget(out var request))
{
Schedule(() =>
{
if (checkEquality(request.Model, TrackedItem))
attachDownload(null);
});
}
}
private void attachDownload(ArchiveDownloadRequest<ScoreInfo>? request)
{
if (attachedRequest != null)
{
attachedRequest.Failure -= onRequestFailure;
attachedRequest.DownloadProgressed -= onRequestProgress;
attachedRequest.Success -= onRequestSuccess;
}
attachedRequest = request;
if (attachedRequest != null)
{
if (attachedRequest.Progress == 1)
{
UpdateProgress(1);
UpdateState(DownloadState.Importing);
}
else
{
UpdateProgress(attachedRequest.Progress);
UpdateState(DownloadState.Downloading);
attachedRequest.Failure += onRequestFailure;
attachedRequest.DownloadProgressed += onRequestProgress;
attachedRequest.Success += onRequestSuccess;
}
}
else
{
UpdateState(DownloadState.NotDownloaded);
}
}
private void onRequestSuccess(string _) => Schedule(() => UpdateState(DownloadState.Importing));
private void onRequestProgress(float progress) => Schedule(() => UpdateProgress(progress));
private void onRequestFailure(Exception e) => Schedule(() => attachDownload(null));
private void itemUpdated(ValueChangedEvent<WeakReference<ScoreInfo>> weakItem)
{
if (weakItem.NewValue.TryGetTarget(out var item))
{
Schedule(() =>
{
if (!checkEquality(item, TrackedItem))
return;
UpdateState(DownloadState.NotDownloaded);
});
}
}
private void itemRemoved(ValueChangedEvent<WeakReference<ScoreInfo>> weakItem)
{
if (weakItem.NewValue.TryGetTarget(out var item))
{
Schedule(() =>
{
if (checkEquality(item, TrackedItem))
UpdateState(DownloadState.NotDownloaded);
});
}
}
private bool checkEquality(ScoreInfo x, ScoreInfo y) => x.OnlineScoreID == y.OnlineScoreID;
#region Disposal
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
attachDownload(null);
}
#endregion
}
}

View File

@ -1,19 +0,0 @@
// 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.Bindables;
using osu.Game.Beatmaps;
using osu.Game.Online;
namespace osu.Game.Overlays
{
public abstract class BeatmapDownloadTrackingComposite : DownloadTrackingComposite<BeatmapSetInfo, BeatmapManager>
{
public Bindable<BeatmapSetInfo> BeatmapSet => Model;
protected BeatmapDownloadTrackingComposite(BeatmapSetInfo set = null)
: base(set)
{
}
}
}

View File

@ -5,6 +5,7 @@ using System;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
@ -13,7 +14,7 @@ using osu.Game.Online;
namespace osu.Game.Overlays.BeatmapListing.Panels namespace osu.Game.Overlays.BeatmapListing.Panels
{ {
public class BeatmapPanelDownloadButton : BeatmapDownloadTrackingComposite public class BeatmapPanelDownloadButton : CompositeDrawable
{ {
protected bool DownloadEnabled => button.Enabled.Value; protected bool DownloadEnabled => button.Enabled.Value;
@ -26,16 +27,31 @@ namespace osu.Game.Overlays.BeatmapListing.Panels
private readonly DownloadButton button; private readonly DownloadButton button;
private Bindable<bool> noVideoSetting; private Bindable<bool> noVideoSetting;
protected readonly BeatmapDownloadTracker DownloadTracker;
protected readonly Bindable<DownloadState> State = new Bindable<DownloadState>();
private readonly BeatmapSetInfo beatmapSet;
public BeatmapPanelDownloadButton(BeatmapSetInfo beatmapSet) public BeatmapPanelDownloadButton(BeatmapSetInfo beatmapSet)
: base(beatmapSet)
{ {
InternalChild = shakeContainer = new ShakeContainer this.beatmapSet = beatmapSet;
InternalChildren = new Drawable[]
{ {
RelativeSizeAxes = Axes.Both, shakeContainer = new ShakeContainer
Child = button = new DownloadButton
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Child = button = new DownloadButton
{
RelativeSizeAxes = Axes.Both,
State = { BindTarget = State }
},
}, },
DownloadTracker = new BeatmapDownloadTracker(beatmapSet)
{
State = { BindTarget = State }
}
}; };
button.Add(new DownloadProgressBar(beatmapSet) button.Add(new DownloadProgressBar(beatmapSet)
@ -46,14 +62,6 @@ namespace osu.Game.Overlays.BeatmapListing.Panels
}); });
} }
protected override void LoadComplete()
{
base.LoadComplete();
button.State.BindTo(State);
FinishTransforms(true);
}
[BackgroundDependencyLoader(true)] [BackgroundDependencyLoader(true)]
private void load(OsuGame game, BeatmapManager beatmaps, OsuConfigManager osuConfig) private void load(OsuGame game, BeatmapManager beatmaps, OsuConfigManager osuConfig)
{ {
@ -61,7 +69,7 @@ namespace osu.Game.Overlays.BeatmapListing.Panels
button.Action = () => button.Action = () =>
{ {
switch (State.Value) switch (DownloadTracker.State.Value)
{ {
case DownloadState.Downloading: case DownloadState.Downloading:
case DownloadState.Importing: case DownloadState.Importing:
@ -73,11 +81,11 @@ namespace osu.Game.Overlays.BeatmapListing.Panels
if (SelectedBeatmap.Value != null) if (SelectedBeatmap.Value != null)
findPredicate = b => b.OnlineBeatmapID == SelectedBeatmap.Value.OnlineBeatmapID; findPredicate = b => b.OnlineBeatmapID == SelectedBeatmap.Value.OnlineBeatmapID;
game?.PresentBeatmap(BeatmapSet.Value, findPredicate); game?.PresentBeatmap(beatmapSet, findPredicate);
break; break;
default: default:
beatmaps.Download(BeatmapSet.Value, noVideoSetting.Value); beatmaps.Download(beatmapSet, noVideoSetting.Value);
break; break;
} }
}; };
@ -92,7 +100,7 @@ namespace osu.Game.Overlays.BeatmapListing.Panels
break; break;
default: default:
if (BeatmapSet.Value?.OnlineInfo?.Availability.DownloadDisabled ?? false) if (beatmapSet.OnlineInfo?.Availability.DownloadDisabled ?? false)
{ {
button.Enabled.Value = false; button.Enabled.Value = false;
button.TooltipText = "this beatmap is currently not available for download."; button.TooltipText = "this beatmap is currently not available for download.";
@ -102,5 +110,11 @@ namespace osu.Game.Overlays.BeatmapListing.Panels
} }
}, true); }, true);
} }
protected override void LoadComplete()
{
base.LoadComplete();
FinishTransforms(true);
}
} }
} }

View File

@ -4,6 +4,7 @@
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
@ -12,13 +13,22 @@ using osuTK.Graphics;
namespace osu.Game.Overlays.BeatmapListing.Panels namespace osu.Game.Overlays.BeatmapListing.Panels
{ {
public class DownloadProgressBar : BeatmapDownloadTrackingComposite public class DownloadProgressBar : CompositeDrawable
{ {
private readonly ProgressBar progressBar; private readonly ProgressBar progressBar;
private readonly BeatmapDownloadTracker downloadTracker;
public DownloadProgressBar(BeatmapSetInfo beatmapSet) public DownloadProgressBar(BeatmapSetInfo beatmapSet)
: base(beatmapSet)
{ {
InternalChildren = new Drawable[]
{
progressBar = new ProgressBar(false)
{
Height = 0,
Alpha = 0,
},
downloadTracker = new BeatmapDownloadTracker(beatmapSet),
};
AddInternal(progressBar = new ProgressBar(false) AddInternal(progressBar = new ProgressBar(false)
{ {
Height = 0, Height = 0,
@ -34,9 +44,9 @@ namespace osu.Game.Overlays.BeatmapListing.Panels
{ {
progressBar.FillColour = colours.Blue; progressBar.FillColour = colours.Blue;
progressBar.BackgroundColour = Color4.Black.Opacity(0.7f); progressBar.BackgroundColour = Color4.Black.Opacity(0.7f);
progressBar.Current = Progress; progressBar.Current.BindTarget = downloadTracker.Progress;
State.BindValueChanged(state => downloadTracker.State.BindValueChanged(state =>
{ {
switch (state.NewValue) switch (state.NewValue)
{ {

View File

@ -3,12 +3,14 @@
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables; using osu.Game.Beatmaps.Drawables;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
@ -21,8 +23,10 @@ using osuTK;
namespace osu.Game.Overlays.BeatmapSet namespace osu.Game.Overlays.BeatmapSet
{ {
public class BeatmapSetHeaderContent : BeatmapDownloadTrackingComposite public class BeatmapSetHeaderContent : CompositeDrawable
{ {
public readonly Bindable<BeatmapSetInfo> BeatmapSet = new Bindable<BeatmapSetInfo>();
private const float transition_duration = 200; private const float transition_duration = 200;
private const float buttons_height = 45; private const float buttons_height = 45;
private const float buttons_spacing = 5; private const float buttons_spacing = 5;
@ -45,6 +49,8 @@ namespace osu.Game.Overlays.BeatmapSet
private readonly FillFlowContainer fadeContent; private readonly FillFlowContainer fadeContent;
private readonly LoadingSpinner loading; private readonly LoadingSpinner loading;
private BeatmapDownloadTracker downloadTracker;
[Resolved] [Resolved]
private IAPIProvider api { get; set; } private IAPIProvider api { get; set; }
@ -222,13 +228,13 @@ namespace osu.Game.Overlays.BeatmapSet
{ {
coverGradient.Colour = ColourInfo.GradientVertical(colourProvider.Background6.Opacity(0.3f), colourProvider.Background6.Opacity(0.8f)); coverGradient.Colour = ColourInfo.GradientVertical(colourProvider.Background6.Opacity(0.3f), colourProvider.Background6.Opacity(0.8f));
State.BindValueChanged(_ => updateDownloadButtons());
BeatmapSet.BindValueChanged(setInfo => BeatmapSet.BindValueChanged(setInfo =>
{ {
Picker.BeatmapSet = rulesetSelector.BeatmapSet = author.BeatmapSet = beatmapAvailability.BeatmapSet = Details.BeatmapSet = setInfo.NewValue; Picker.BeatmapSet = rulesetSelector.BeatmapSet = author.BeatmapSet = beatmapAvailability.BeatmapSet = Details.BeatmapSet = setInfo.NewValue;
cover.OnlineInfo = setInfo.NewValue?.OnlineInfo; cover.OnlineInfo = setInfo.NewValue?.OnlineInfo;
downloadTracker?.RemoveAndDisposeImmediately();
if (setInfo.NewValue == null) if (setInfo.NewValue == null)
{ {
onlineStatusPill.FadeTo(0.5f, 500, Easing.OutQuint); onlineStatusPill.FadeTo(0.5f, 500, Easing.OutQuint);
@ -241,6 +247,10 @@ namespace osu.Game.Overlays.BeatmapSet
} }
else else
{ {
downloadTracker = new BeatmapDownloadTracker(setInfo.NewValue);
downloadTracker.State.BindValueChanged(_ => updateDownloadButtons());
AddInternal(downloadTracker);
fadeContent.FadeIn(500, Easing.OutQuint); fadeContent.FadeIn(500, Easing.OutQuint);
loading.Hide(); loading.Hide();
@ -266,13 +276,13 @@ namespace osu.Game.Overlays.BeatmapSet
{ {
if (BeatmapSet.Value == null) return; if (BeatmapSet.Value == null) return;
if (BeatmapSet.Value.OnlineInfo.Availability.DownloadDisabled && State.Value != DownloadState.LocallyAvailable) if (BeatmapSet.Value.OnlineInfo.Availability.DownloadDisabled && downloadTracker.State.Value != DownloadState.LocallyAvailable)
{ {
downloadButtonsContainer.Clear(); downloadButtonsContainer.Clear();
return; return;
} }
switch (State.Value) switch (downloadTracker.State.Value)
{ {
case DownloadState.LocallyAvailable: case DownloadState.LocallyAvailable:
// temporary for UX until new design is implemented. // temporary for UX until new design is implemented.

View File

@ -22,7 +22,7 @@ using osuTK.Graphics;
namespace osu.Game.Overlays.BeatmapSet.Buttons namespace osu.Game.Overlays.BeatmapSet.Buttons
{ {
public class HeaderDownloadButton : BeatmapDownloadTrackingComposite, IHasTooltip public class HeaderDownloadButton : CompositeDrawable, IHasTooltip
{ {
private const int text_size = 12; private const int text_size = 12;
@ -35,9 +35,12 @@ namespace osu.Game.Overlays.BeatmapSet.Buttons
private ShakeContainer shakeContainer; private ShakeContainer shakeContainer;
private HeaderButton button; private HeaderButton button;
private BeatmapDownloadTracker downloadTracker;
private readonly BeatmapSetInfo beatmapSet;
public HeaderDownloadButton(BeatmapSetInfo beatmapSet, bool noVideo = false) public HeaderDownloadButton(BeatmapSetInfo beatmapSet, bool noVideo = false)
: base(beatmapSet)
{ {
this.beatmapSet = beatmapSet;
this.noVideo = noVideo; this.noVideo = noVideo;
Width = 120; Width = 120;
@ -49,13 +52,17 @@ namespace osu.Game.Overlays.BeatmapSet.Buttons
{ {
FillFlowContainer textSprites; FillFlowContainer textSprites;
AddInternal(shakeContainer = new ShakeContainer InternalChildren = new Drawable[]
{ {
RelativeSizeAxes = Axes.Both, shakeContainer = new ShakeContainer
Masking = true, {
CornerRadius = 5, RelativeSizeAxes = Axes.Both,
Child = button = new HeaderButton { RelativeSizeAxes = Axes.Both }, Masking = true,
}); CornerRadius = 5,
Child = button = new HeaderButton { RelativeSizeAxes = Axes.Both },
},
downloadTracker = new BeatmapDownloadTracker(beatmapSet),
};
button.AddRange(new Drawable[] button.AddRange(new Drawable[]
{ {
@ -83,7 +90,7 @@ namespace osu.Game.Overlays.BeatmapSet.Buttons
}, },
} }
}, },
new DownloadProgressBar(BeatmapSet.Value) new DownloadProgressBar(beatmapSet)
{ {
Anchor = Anchor.BottomLeft, Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft, Origin = Anchor.BottomLeft,
@ -92,20 +99,20 @@ namespace osu.Game.Overlays.BeatmapSet.Buttons
button.Action = () => button.Action = () =>
{ {
if (State.Value != DownloadState.NotDownloaded) if (downloadTracker.State.Value != DownloadState.NotDownloaded)
{ {
shakeContainer.Shake(); shakeContainer.Shake();
return; return;
} }
beatmaps.Download(BeatmapSet.Value, noVideo); beatmaps.Download(beatmapSet, noVideo);
}; };
localUser.BindTo(api.LocalUser); localUser.BindTo(api.LocalUser);
localUser.BindValueChanged(userChanged, true); localUser.BindValueChanged(userChanged, true);
button.Enabled.BindValueChanged(enabledChanged, true); button.Enabled.BindValueChanged(enabledChanged, true);
State.BindValueChanged(state => downloadTracker.State.BindValueChanged(state =>
{ {
switch (state.NewValue) switch (state.NewValue)
{ {
@ -161,7 +168,7 @@ namespace osu.Game.Overlays.BeatmapSet.Buttons
private LocalisableString getVideoSuffixText() private LocalisableString getVideoSuffixText()
{ {
if (!BeatmapSet.Value.OnlineInfo.HasVideo) if (!beatmapSet.OnlineInfo.HasVideo)
return string.Empty; return string.Empty;
return noVideo ? BeatmapsetsStrings.ShowDetailsDownloadNoVideo : BeatmapsetsStrings.ShowDetailsDownloadVideo; return noVideo ? BeatmapsetsStrings.ShowDetailsDownloadNoVideo : BeatmapsetsStrings.ShowDetailsDownloadVideo;

View File

@ -0,0 +1,43 @@
// 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.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Configuration;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Video;
using osu.Framework.Localisation;
using osu.Game.Localisation;
namespace osu.Game.Overlays.Settings.Sections.Graphics
{
public class VideoSettings : SettingsSubsection
{
protected override LocalisableString Header => GraphicsSettingsStrings.VideoHeader;
private Bindable<HardwareVideoDecoder> hardwareVideoDecoder;
private SettingsCheckbox hwAccelCheckbox;
[BackgroundDependencyLoader]
private void load(FrameworkConfigManager config)
{
hardwareVideoDecoder = config.GetBindable<HardwareVideoDecoder>(FrameworkSetting.HardwareVideoDecoder);
Children = new Drawable[]
{
hwAccelCheckbox = new SettingsCheckbox
{
LabelText = GraphicsSettingsStrings.UseHardwareAcceleration,
},
};
hwAccelCheckbox.Current.Default = hardwareVideoDecoder.Default != HardwareVideoDecoder.None;
hwAccelCheckbox.Current.Value = hardwareVideoDecoder.Value != HardwareVideoDecoder.None;
hwAccelCheckbox.Current.BindValueChanged(val =>
{
hardwareVideoDecoder.Value = val.NewValue ? HardwareVideoDecoder.Any : HardwareVideoDecoder.None;
});
}
}
}

View File

@ -24,6 +24,7 @@ namespace osu.Game.Overlays.Settings.Sections
{ {
new LayoutSettings(), new LayoutSettings(),
new RendererSettings(), new RendererSettings(),
new VideoSettings(),
new ScreenshotSettings(), new ScreenshotSettings(),
}; };
} }

View File

@ -16,5 +16,8 @@ namespace osu.Game.Scoring
} }
protected override ArchiveDownloadRequest<ScoreInfo> CreateDownloadRequest(ScoreInfo score, bool minimiseDownload) => new DownloadReplayRequest(score); protected override ArchiveDownloadRequest<ScoreInfo> CreateDownloadRequest(ScoreInfo score, bool minimiseDownload) => new DownloadReplayRequest(score);
public override ArchiveDownloadRequest<ScoreInfo> GetExistingDownload(ScoreInfo model)
=> CurrentDownloads.Find(r => r.Model.OnlineScoreID == model.OnlineScoreID);
} }
} }

View File

@ -248,10 +248,7 @@ namespace osu.Game.Screens.OnlinePlay
protected virtual IEnumerable<Drawable> CreateButtons() => protected virtual IEnumerable<Drawable> CreateButtons() =>
new Drawable[] new Drawable[]
{ {
new PlaylistDownloadButton(Item) new PlaylistDownloadButton(Item),
{
Size = new Vector2(50, 30)
},
new PlaylistRemoveButton new PlaylistRemoveButton
{ {
Size = new Vector2(30, 30), Size = new Vector2(30, 30),
@ -282,28 +279,33 @@ namespace osu.Game.Screens.OnlinePlay
return true; return true;
} }
private class PlaylistDownloadButton : BeatmapPanelDownloadButton private sealed class PlaylistDownloadButton : BeatmapPanelDownloadButton
{ {
private readonly PlaylistItem playlistItem; private readonly PlaylistItem playlistItem;
[Resolved] [Resolved]
private BeatmapManager beatmapManager { get; set; } private BeatmapManager beatmapManager { get; set; }
public override bool IsPresent => base.IsPresent || Scheduler.HasPendingTasks; // required for download tracking, as this button hides itself. can probably be removed with a bit of consideration.
public override bool IsPresent => true;
private const float width = 50;
public PlaylistDownloadButton(PlaylistItem playlistItem) public PlaylistDownloadButton(PlaylistItem playlistItem)
: base(playlistItem.Beatmap.Value.BeatmapSet) : base(playlistItem.Beatmap.Value.BeatmapSet)
{ {
this.playlistItem = playlistItem; this.playlistItem = playlistItem;
Size = new Vector2(width, 30);
Alpha = 0; Alpha = 0;
} }
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.LoadComplete();
State.BindValueChanged(stateChanged, true); State.BindValueChanged(stateChanged, true);
FinishTransforms(true);
// base implementation calls FinishTransforms, so should be run after the above state update.
base.LoadComplete();
} }
private void stateChanged(ValueChangedEvent<DownloadState> state) private void stateChanged(ValueChangedEvent<DownloadState> state)
@ -315,12 +317,16 @@ namespace osu.Game.Screens.OnlinePlay
if (beatmapManager.QueryBeatmap(b => b.MD5Hash == playlistItem.Beatmap.Value.MD5Hash) == null) if (beatmapManager.QueryBeatmap(b => b.MD5Hash == playlistItem.Beatmap.Value.MD5Hash) == null)
State.Value = DownloadState.NotDownloaded; State.Value = DownloadState.NotDownloaded;
else else
this.FadeTo(0, 500); {
this.FadeTo(0, 500)
.ResizeWidthTo(0, 500, Easing.OutQuint);
}
break; break;
default: default:
this.FadeTo(1, 500); this.ResizeWidthTo(width, 500, Easing.OutQuint)
.FadeTo(1, 500);
break; break;
} }
} }

View File

@ -65,9 +65,9 @@ namespace osu.Game.Screens.OnlinePlay.Match
private IBindable<WeakReference<BeatmapSetInfo>> managerUpdated; private IBindable<WeakReference<BeatmapSetInfo>> managerUpdated;
[Cached] [Cached]
protected OnlinePlayBeatmapAvailabilityTracker BeatmapAvailabilityTracker { get; private set; } private OnlinePlayBeatmapAvailabilityTracker beatmapAvailabilityTracker { get; set; }
protected IBindable<BeatmapAvailability> BeatmapAvailability => BeatmapAvailabilityTracker.Availability; protected IBindable<BeatmapAvailability> BeatmapAvailability => beatmapAvailabilityTracker.Availability;
public readonly Room Room; public readonly Room Room;
private readonly bool allowEdit; private readonly bool allowEdit;
@ -88,7 +88,7 @@ namespace osu.Game.Screens.OnlinePlay.Match
Padding = new MarginPadding { Top = Header.HEIGHT }; Padding = new MarginPadding { Top = Header.HEIGHT };
BeatmapAvailabilityTracker = new OnlinePlayBeatmapAvailabilityTracker beatmapAvailabilityTracker = new OnlinePlayBeatmapAvailabilityTracker
{ {
SelectedItem = { BindTarget = SelectedItem } SelectedItem = { BindTarget = SelectedItem }
}; };
@ -103,7 +103,7 @@ namespace osu.Game.Screens.OnlinePlay.Match
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
BeatmapAvailabilityTracker, beatmapAvailabilityTracker,
new GridContainer new GridContainer
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,

View File

@ -10,6 +10,7 @@ using ManagedBass.Fx;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio.Sample; using osu.Framework.Audio.Sample;
using osu.Framework.Audio.Track; using osu.Framework.Audio.Track;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
@ -83,7 +84,7 @@ namespace osu.Game.Screens.Play
Content, Content,
redFlashLayer = new Box redFlashLayer = new Box
{ {
Colour = Color4.Red, Colour = Color4.Red.Opacity(0.6f),
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Blending = BlendingParameters.Additive, Blending = BlendingParameters.Additive,
Depth = float.MinValue, Depth = float.MinValue,

View File

@ -4,6 +4,7 @@
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Online; using osu.Game.Online;
@ -12,13 +13,17 @@ using osuTK;
namespace osu.Game.Screens.Ranking namespace osu.Game.Screens.Ranking
{ {
public class ReplayDownloadButton : DownloadTrackingComposite<ScoreInfo, ScoreManager> public class ReplayDownloadButton : CompositeDrawable
{ {
public Bindable<ScoreInfo> Score => Model; public readonly Bindable<ScoreInfo> Score = new Bindable<ScoreInfo>();
protected readonly Bindable<DownloadState> State = new Bindable<DownloadState>();
private DownloadButton button; private DownloadButton button;
private ShakeContainer shakeContainer; private ShakeContainer shakeContainer;
private ScoreDownloadTracker downloadTracker;
private ReplayAvailability replayAvailability private ReplayAvailability replayAvailability
{ {
get get
@ -26,7 +31,7 @@ namespace osu.Game.Screens.Ranking
if (State.Value == DownloadState.LocallyAvailable) if (State.Value == DownloadState.LocallyAvailable)
return ReplayAvailability.Local; return ReplayAvailability.Local;
if (!string.IsNullOrEmpty(Model.Value?.Hash)) if (!string.IsNullOrEmpty(Score.Value?.Hash))
return ReplayAvailability.Online; return ReplayAvailability.Online;
return ReplayAvailability.NotAvailable; return ReplayAvailability.NotAvailable;
@ -34,8 +39,8 @@ namespace osu.Game.Screens.Ranking
} }
public ReplayDownloadButton(ScoreInfo score) public ReplayDownloadButton(ScoreInfo score)
: base(score)
{ {
Score.Value = score;
Size = new Vector2(50, 30); Size = new Vector2(50, 30);
} }
@ -56,11 +61,11 @@ namespace osu.Game.Screens.Ranking
switch (State.Value) switch (State.Value)
{ {
case DownloadState.LocallyAvailable: case DownloadState.LocallyAvailable:
game?.PresentScore(Model.Value, ScorePresentType.Gameplay); game?.PresentScore(Score.Value, ScorePresentType.Gameplay);
break; break;
case DownloadState.NotDownloaded: case DownloadState.NotDownloaded:
scores.Download(Model.Value, false); scores.Download(Score.Value, false);
break; break;
case DownloadState.Importing: case DownloadState.Importing:
@ -70,17 +75,25 @@ namespace osu.Game.Screens.Ranking
} }
}; };
State.BindValueChanged(state => Score.BindValueChanged(score =>
{ {
button.State.Value = state.NewValue; downloadTracker?.RemoveAndDisposeImmediately();
if (score.NewValue != null)
{
AddInternal(downloadTracker = new ScoreDownloadTracker(score.NewValue)
{
State = { BindTarget = State }
});
}
button.Enabled.Value = replayAvailability != ReplayAvailability.NotAvailable;
updateTooltip(); updateTooltip();
}, true); }, true);
Model.BindValueChanged(_ => State.BindValueChanged(state =>
{ {
button.Enabled.Value = replayAvailability != ReplayAvailability.NotAvailable; button.State.Value = state.NewValue;
updateTooltip(); updateTooltip();
}, true); }, true);
} }

View File

@ -94,9 +94,6 @@ namespace osu.Game.Screens.Select.Details
modSettingChangeTracker = new ModSettingChangeTracker(mods.NewValue); modSettingChangeTracker = new ModSettingChangeTracker(mods.NewValue);
modSettingChangeTracker.SettingChanged += m => modSettingChangeTracker.SettingChanged += m =>
{ {
if (!(m is IApplicableToDifficulty))
return;
debouncedStatisticsUpdate?.Cancel(); debouncedStatisticsUpdate?.Cancel();
debouncedStatisticsUpdate = Scheduler.AddDelayed(updateStatistics, 100); debouncedStatisticsUpdate = Scheduler.AddDelayed(updateStatistics, 100);
}; };

View File

@ -93,7 +93,7 @@ namespace osu.Game.Skinning
private Stream getConfigurationStream() private Stream getConfigurationStream()
{ {
string path = SkinInfo.Files.SingleOrDefault(f => f.Filename == "skin.ini")?.FileInfo.StoragePath; string path = SkinInfo.Files.SingleOrDefault(f => f.Filename.Equals(@"skin.ini", StringComparison.OrdinalIgnoreCase))?.FileInfo.StoragePath;
if (string.IsNullOrEmpty(path)) if (string.IsNullOrEmpty(path))
return null; return null;

View File

@ -108,7 +108,7 @@ namespace osu.Game.Skinning
} }
} }
protected override bool ShouldDeleteArchive(string path) => Path.GetExtension(path)?.ToLowerInvariant() == ".osk"; protected override bool ShouldDeleteArchive(string path) => Path.GetExtension(path)?.ToLowerInvariant() == @".osk";
/// <summary> /// <summary>
/// Returns a list of all usable <see cref="SkinInfo"/>s. Includes the special default skin plus all skins from <see cref="GetAllUserSkins"/>. /// Returns a list of all usable <see cref="SkinInfo"/>s. Includes the special default skin plus all skins from <see cref="GetAllUserSkins"/>.
@ -149,9 +149,9 @@ namespace osu.Game.Skinning
CurrentSkinInfo.Value = ModelStore.ConsumableItems.Single(i => i.ID == chosen.ID); CurrentSkinInfo.Value = ModelStore.ConsumableItems.Single(i => i.ID == chosen.ID);
} }
protected override SkinInfo CreateModel(ArchiveReader archive) => new SkinInfo { Name = archive.Name ?? "No name" }; protected override SkinInfo CreateModel(ArchiveReader archive) => new SkinInfo { Name = archive.Name ?? @"No name" };
private const string unknown_creator_string = "Unknown"; private const string unknown_creator_string = @"Unknown";
protected override bool HasCustomHashFunction => true; protected override bool HasCustomHashFunction => true;
@ -164,7 +164,7 @@ namespace osu.Game.Skinning
// `Skin` will parse the skin.ini and populate `Skin.Configuration` during construction above. // `Skin` will parse the skin.ini and populate `Skin.Configuration` during construction above.
string skinIniSourcedName = instance.Configuration.SkinInfo.Name; string skinIniSourcedName = instance.Configuration.SkinInfo.Name;
string skinIniSourcedCreator = instance.Configuration.SkinInfo.Creator; string skinIniSourcedCreator = instance.Configuration.SkinInfo.Creator;
string archiveName = item.Name.Replace(".osk", "", StringComparison.OrdinalIgnoreCase); string archiveName = item.Name.Replace(@".osk", string.Empty, StringComparison.OrdinalIgnoreCase);
bool isImport = item.ID == 0; bool isImport = item.ID == 0;
@ -177,7 +177,7 @@ namespace osu.Game.Skinning
// In an ideal world, skin.ini would be the only source of metadata, but a lot of skin creators and users don't update it when making modifications. // In an ideal world, skin.ini would be the only source of metadata, but a lot of skin creators and users don't update it when making modifications.
// In both of these cases, the expectation from the user is that the filename or folder name is displayed somewhere to identify the skin. // In both of these cases, the expectation from the user is that the filename or folder name is displayed somewhere to identify the skin.
if (archiveName != item.Name) if (archiveName != item.Name)
item.Name = $"{item.Name} [{archiveName}]"; item.Name = @$"{item.Name} [{archiveName}]";
} }
// By this point, the metadata in SkinInfo will be correct. // By this point, the metadata in SkinInfo will be correct.
@ -191,10 +191,10 @@ namespace osu.Game.Skinning
private void updateSkinIniMetadata(SkinInfo item) private void updateSkinIniMetadata(SkinInfo item)
{ {
string nameLine = $"Name: {item.Name}"; string nameLine = @$"Name: {item.Name}";
string authorLine = $"Author: {item.Creator}"; string authorLine = @$"Author: {item.Creator}";
var existingFile = item.Files.SingleOrDefault(f => f.Filename == "skin.ini"); var existingFile = item.Files.SingleOrDefault(f => f.Filename.Equals(@"skin.ini", StringComparison.OrdinalIgnoreCase));
if (existingFile != null) if (existingFile != null)
{ {
@ -210,12 +210,12 @@ namespace osu.Game.Skinning
while ((line = sr.ReadLine()) != null) while ((line = sr.ReadLine()) != null)
{ {
if (line.StartsWith("Name:", StringComparison.Ordinal)) if (line.StartsWith(@"Name:", StringComparison.Ordinal))
{ {
outputLines.Add(nameLine); outputLines.Add(nameLine);
addedName = true; addedName = true;
} }
else if (line.StartsWith("Author:", StringComparison.Ordinal)) else if (line.StartsWith(@"Author:", StringComparison.Ordinal))
{ {
outputLines.Add(authorLine); outputLines.Add(authorLine);
addedAuthor = true; addedAuthor = true;
@ -229,7 +229,7 @@ namespace osu.Game.Skinning
{ {
outputLines.AddRange(new[] outputLines.AddRange(new[]
{ {
"[General]", @"[General]",
nameLine, nameLine,
authorLine, authorLine,
}); });
@ -252,13 +252,13 @@ namespace osu.Game.Skinning
{ {
using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true)) using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true))
{ {
sw.WriteLine("[General]"); sw.WriteLine(@"[General]");
sw.WriteLine(nameLine); sw.WriteLine(nameLine);
sw.WriteLine(authorLine); sw.WriteLine(authorLine);
sw.WriteLine("Version: latest"); sw.WriteLine(@"Version: latest");
} }
AddFile(item, stream, "skin.ini"); AddFile(item, stream, @"skin.ini");
} }
} }
} }
@ -295,7 +295,7 @@ namespace osu.Game.Skinning
// if the user is attempting to save one of the default skin implementations, create a copy first. // if the user is attempting to save one of the default skin implementations, create a copy first.
CurrentSkinInfo.Value = Import(new SkinInfo CurrentSkinInfo.Value = Import(new SkinInfo
{ {
Name = skin.SkinInfo.Name + " (modified)", Name = skin.SkinInfo.Name + @" (modified)",
Creator = skin.SkinInfo.Creator, Creator = skin.SkinInfo.Creator,
InstantiationInfo = skin.SkinInfo.InstantiationInfo, InstantiationInfo = skin.SkinInfo.InstantiationInfo,
}).Result.Value; }).Result.Value;
@ -312,7 +312,7 @@ namespace osu.Game.Skinning
using (var streamContent = new MemoryStream(Encoding.UTF8.GetBytes(json))) using (var streamContent = new MemoryStream(Encoding.UTF8.GetBytes(json)))
{ {
string filename = $"{drawableInfo.Key}.json"; string filename = @$"{drawableInfo.Key}.json";
var oldFile = skin.SkinInfo.Files.FirstOrDefault(f => f.Filename == filename); var oldFile = skin.SkinInfo.Files.FirstOrDefault(f => f.Filename == filename);