1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-12 18:23:04 +08:00

Merge branch 'score-refactor/isolated-serialisation' into beatmap-refactor/get-and-present

This commit is contained in:
Dean Herbert 2021-11-01 18:06:53 +09:00
commit 602fe372c2
61 changed files with 888 additions and 559 deletions

View File

@ -52,7 +52,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.1026.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2021.1026.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.1026.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2021.1029.0" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Transitive Dependencies"> <ItemGroup Label="Transitive Dependencies">
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. --> <!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->

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

@ -13,6 +13,7 @@ using osu.Framework.Bindables;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Ranking; using osu.Game.Screens.Ranking;
using osuTK.Input; using osuTK.Input;
@ -136,7 +137,8 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
OnlineID = 2553163309, OnlineID = 2553163309,
OnlineRulesetID = 0, OnlineRulesetID = 0,
Replay = replayAvailable, Beatmap = CreateAPIBeatmapSet(new OsuRuleset().RulesetInfo).Beatmaps.First(),
HasReplay = replayAvailable,
User = new User User = new User
{ {
Id = 39828, Id = 39828,

View File

@ -15,6 +15,7 @@ using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Select; using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Carousel; using osu.Game.Screens.Select.Carousel;
using osu.Game.Screens.Select.Filter; using osu.Game.Screens.Select.Filter;
@ -684,6 +685,7 @@ namespace osu.Game.Tests.Visual.SongSelect
set.Beatmaps.Add(new BeatmapInfo set.Beatmaps.Add(new BeatmapInfo
{ {
Version = $"Stars: {i}", Version = $"Stars: {i}",
Ruleset = new OsuRuleset().RulesetInfo,
StarDifficulty = i, StarDifficulty = i,
}); });
} }
@ -868,6 +870,7 @@ namespace osu.Game.Tests.Visual.SongSelect
OnlineBeatmapID = id++ * 10, OnlineBeatmapID = id++ * 10,
Version = version, Version = version,
StarDifficulty = diff, StarDifficulty = diff,
Ruleset = new OsuRuleset().RulesetInfo,
BaseDifficulty = new BeatmapDifficulty BaseDifficulty = new BeatmapDifficulty
{ {
OverallDifficulty = diff, OverallDifficulty = diff,

View File

@ -140,7 +140,7 @@ namespace osu.Game.Tests.Visual.SongSelect
} }
} }
public override async Task<StarDifficulty> GetDifficultyAsync(BeatmapInfo beatmapInfo, RulesetInfo rulesetInfo = null, IEnumerable<Mod> mods = null, CancellationToken cancellationToken = default) public override async Task<StarDifficulty> GetDifficultyAsync(IBeatmapInfo beatmapInfo, IRulesetInfo rulesetInfo = null, IEnumerable<Mod> mods = null, CancellationToken cancellationToken = default)
{ {
if (blockCalculation) if (blockCalculation)
await calculationBlocker.Task.ConfigureAwait(false); await calculationBlocker.Task.ConfigureAwait(false);

View File

@ -56,95 +56,71 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("Set width to 300", () => content.ResizeWidthTo(300, 500)); AddStep("Set width to 300", () => content.ResizeWidthTo(300, 500));
} }
private static readonly List<BeatmapSetInfo> new_beatmaps = new List<BeatmapSetInfo> private static readonly List<APIBeatmapSet> new_beatmaps = new List<APIBeatmapSet>
{ {
new BeatmapSetInfo new APIBeatmapSet
{ {
Metadata = new BeatmapMetadata Title = "Very Long Title (TV size) [TATOE]",
Artist = "This artist has a really long name how is this possible",
Author = new User
{ {
Title = "Very Long Title (TV size) [TATOE]", Username = "author",
Artist = "This artist has a really long name how is this possible", Id = 100
Author = new User
{
Username = "author",
Id = 100
}
}, },
OnlineInfo = new APIBeatmapSet Covers = new BeatmapSetOnlineCovers
{ {
Covers = new BeatmapSetOnlineCovers Cover = "https://assets.ppy.sh/beatmaps/1189904/covers/cover.jpg?1595456608",
{ },
Cover = "https://assets.ppy.sh/beatmaps/1189904/covers/cover.jpg?1595456608", Ranked = DateTimeOffset.Now
},
Ranked = DateTimeOffset.Now
}
}, },
new BeatmapSetInfo new APIBeatmapSet
{ {
Metadata = new BeatmapMetadata Title = "Very Long Title (TV size) [TATOE]",
Artist = "This artist has a really long name how is this possible",
Author = new User
{ {
Title = "Very Long Title (TV size) [TATOE]", Username = "author",
Artist = "This artist has a really long name how is this possible", Id = 100
Author = new User
{
Username = "author",
Id = 100
}
}, },
OnlineInfo = new APIBeatmapSet Covers = new BeatmapSetOnlineCovers
{ {
Covers = new BeatmapSetOnlineCovers Cover = "https://assets.ppy.sh/beatmaps/1189904/covers/cover.jpg?1595456608",
{ },
Cover = "https://assets.ppy.sh/beatmaps/1189904/covers/cover.jpg?1595456608", Ranked = DateTimeOffset.Now
},
Ranked = DateTimeOffset.MinValue
}
} }
}; };
private static readonly List<BeatmapSetInfo> popular_beatmaps = new List<BeatmapSetInfo> private static readonly List<APIBeatmapSet> popular_beatmaps = new List<APIBeatmapSet>
{ {
new BeatmapSetInfo new APIBeatmapSet
{ {
Metadata = new BeatmapMetadata Title = "Very Long Title (TV size) [TATOE]",
Artist = "This artist has a really long name how is this possible",
Author = new User
{ {
Title = "Title", Username = "author",
Artist = "Artist", Id = 100
Author = new User
{
Username = "author",
Id = 100
}
}, },
OnlineInfo = new APIBeatmapSet Covers = new BeatmapSetOnlineCovers
{ {
Covers = new BeatmapSetOnlineCovers Cover = "https://assets.ppy.sh/beatmaps/1189904/covers/cover.jpg?1595456608",
{ },
Cover = "https://assets.ppy.sh/beatmaps/1079428/covers/cover.jpg?1595295586", Ranked = DateTimeOffset.Now
},
FavouriteCount = 100
}
}, },
new BeatmapSetInfo new APIBeatmapSet
{ {
Metadata = new BeatmapMetadata Title = "Very Long Title (TV size) [TATOE]",
Artist = "This artist has a really long name how is this possible",
Author = new User
{ {
Title = "Title 2", Username = "author",
Artist = "Artist 2", Id = 100
Author = new User
{
Username = "someone",
Id = 100
}
}, },
OnlineInfo = new APIBeatmapSet Covers = new BeatmapSetOnlineCovers
{ {
Covers = new BeatmapSetOnlineCovers Cover = "https://assets.ppy.sh/beatmaps/1189904/covers/cover.jpg?1595456608",
{ },
Cover = "https://assets.ppy.sh/beatmaps/1079428/covers/cover.jpg?1595295586", Ranked = DateTimeOffset.Now
},
FavouriteCount = 10
}
} }
}; };
} }

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

@ -88,7 +88,7 @@ namespace osu.Game.Beatmaps
/// <param name="beatmapInfo">The <see cref="BeatmapInfo"/> to get the difficulty of.</param> /// <param name="beatmapInfo">The <see cref="BeatmapInfo"/> to get the difficulty of.</param>
/// <param name="cancellationToken">An optional <see cref="CancellationToken"/> which stops updating the star difficulty for the given <see cref="BeatmapInfo"/>.</param> /// <param name="cancellationToken">An optional <see cref="CancellationToken"/> which stops updating the star difficulty for the given <see cref="BeatmapInfo"/>.</param>
/// <returns>A bindable that is updated to contain the star difficulty when it becomes available. Will be null while in an initial calculating state (but not during updates to ruleset and mods if a stale value is already propagated).</returns> /// <returns>A bindable that is updated to contain the star difficulty when it becomes available. Will be null while in an initial calculating state (but not during updates to ruleset and mods if a stale value is already propagated).</returns>
public IBindable<StarDifficulty?> GetBindableDifficulty([NotNull] BeatmapInfo beatmapInfo, CancellationToken cancellationToken = default) public IBindable<StarDifficulty?> GetBindableDifficulty([NotNull] IBeatmapInfo beatmapInfo, CancellationToken cancellationToken = default)
{ {
var bindable = createBindable(beatmapInfo, currentRuleset.Value, currentMods.Value, cancellationToken); var bindable = createBindable(beatmapInfo, currentRuleset.Value, currentMods.Value, cancellationToken);
@ -99,42 +99,45 @@ namespace osu.Game.Beatmaps
} }
/// <summary> /// <summary>
/// Retrieves a bindable containing the star difficulty of a <see cref="BeatmapInfo"/> with a given <see cref="RulesetInfo"/> and <see cref="Mod"/> combination. /// Retrieves a bindable containing the star difficulty of a <see cref="IBeatmapInfo"/> with a given <see cref="RulesetInfo"/> and <see cref="Mod"/> combination.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// The bindable will not update to follow the currently-selected ruleset and mods or its settings. /// The bindable will not update to follow the currently-selected ruleset and mods or its settings.
/// </remarks> /// </remarks>
/// <param name="beatmapInfo">The <see cref="BeatmapInfo"/> to get the difficulty of.</param> /// <param name="beatmapInfo">The <see cref="IBeatmapInfo"/> to get the difficulty of.</param>
/// <param name="rulesetInfo">The <see cref="RulesetInfo"/> to get the difficulty with. If <c>null</c>, the <paramref name="beatmapInfo"/>'s ruleset is used.</param> /// <param name="rulesetInfo">The <see cref="IRulesetInfo"/> to get the difficulty with. If <c>null</c>, the <paramref name="beatmapInfo"/>'s ruleset is used.</param>
/// <param name="mods">The <see cref="Mod"/>s to get the difficulty with. If <c>null</c>, no mods will be assumed.</param> /// <param name="mods">The <see cref="Mod"/>s to get the difficulty with. If <c>null</c>, no mods will be assumed.</param>
/// <param name="cancellationToken">An optional <see cref="CancellationToken"/> which stops updating the star difficulty for the given <see cref="BeatmapInfo"/>.</param> /// <param name="cancellationToken">An optional <see cref="CancellationToken"/> which stops updating the star difficulty for the given <see cref="IBeatmapInfo"/>.</param>
/// <returns>A bindable that is updated to contain the star difficulty when it becomes available. Will be null while in an initial calculating state.</returns> /// <returns>A bindable that is updated to contain the star difficulty when it becomes available. Will be null while in an initial calculating state.</returns>
public IBindable<StarDifficulty?> GetBindableDifficulty([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo rulesetInfo, [CanBeNull] IEnumerable<Mod> mods, public IBindable<StarDifficulty?> GetBindableDifficulty([NotNull] IBeatmapInfo beatmapInfo, [CanBeNull] IRulesetInfo rulesetInfo, [CanBeNull] IEnumerable<Mod> mods,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
=> createBindable(beatmapInfo, rulesetInfo, mods, cancellationToken); => createBindable(beatmapInfo, rulesetInfo, mods, cancellationToken);
/// <summary> /// <summary>
/// Retrieves the difficulty of a <see cref="BeatmapInfo"/>. /// Retrieves the difficulty of a <see cref="IBeatmapInfo"/>.
/// </summary> /// </summary>
/// <param name="beatmapInfo">The <see cref="BeatmapInfo"/> to get the difficulty of.</param> /// <param name="beatmapInfo">The <see cref="IBeatmapInfo"/> to get the difficulty of.</param>
/// <param name="rulesetInfo">The <see cref="RulesetInfo"/> to get the difficulty with.</param> /// <param name="rulesetInfo">The <see cref="IRulesetInfo"/> to get the difficulty with.</param>
/// <param name="mods">The <see cref="Mod"/>s to get the difficulty with.</param> /// <param name="mods">The <see cref="Mod"/>s to get the difficulty with.</param>
/// <param name="cancellationToken">An optional <see cref="CancellationToken"/> which stops computing the star difficulty.</param> /// <param name="cancellationToken">An optional <see cref="CancellationToken"/> which stops computing the star difficulty.</param>
/// <returns>The <see cref="StarDifficulty"/>.</returns> /// <returns>The <see cref="StarDifficulty"/>.</returns>
public virtual Task<StarDifficulty> GetDifficultyAsync([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo rulesetInfo = null, public virtual Task<StarDifficulty> GetDifficultyAsync([NotNull] IBeatmapInfo beatmapInfo, [CanBeNull] IRulesetInfo rulesetInfo = null,
[CanBeNull] IEnumerable<Mod> mods = null, CancellationToken cancellationToken = default) [CanBeNull] IEnumerable<Mod> mods = null, CancellationToken cancellationToken = default)
{ {
// In the case that the user hasn't given us a ruleset, use the beatmap's default ruleset. // In the case that the user hasn't given us a ruleset, use the beatmap's default ruleset.
rulesetInfo ??= beatmapInfo.Ruleset; rulesetInfo ??= beatmapInfo.Ruleset;
var localBeatmapInfo = beatmapInfo as BeatmapInfo;
var localRulesetInfo = rulesetInfo as RulesetInfo;
// Difficulty can only be computed if the beatmap and ruleset are locally available. // Difficulty can only be computed if the beatmap and ruleset are locally available.
if (beatmapInfo.ID == 0 || rulesetInfo.ID == null) if (localBeatmapInfo == null || localRulesetInfo == null)
{ {
// If not, fall back to the existing star difficulty (e.g. from an online source). // If not, fall back to the existing star difficulty (e.g. from an online source).
return Task.FromResult(new StarDifficulty(beatmapInfo.StarDifficulty, beatmapInfo.MaxCombo ?? 0)); return Task.FromResult(new StarDifficulty(beatmapInfo.StarRating, (beatmapInfo as IBeatmapOnlineInfo)?.MaxCombo ?? 0));
} }
return GetAsync(new DifficultyCacheLookup(beatmapInfo, rulesetInfo, mods), cancellationToken); return GetAsync(new DifficultyCacheLookup(localBeatmapInfo, localRulesetInfo, mods), cancellationToken);
} }
protected override Task<StarDifficulty> ComputeValueAsync(DifficultyCacheLookup lookup, CancellationToken token = default) protected override Task<StarDifficulty> ComputeValueAsync(DifficultyCacheLookup lookup, CancellationToken token = default)
@ -227,12 +230,12 @@ namespace osu.Game.Beatmaps
/// <summary> /// <summary>
/// Creates a new <see cref="BindableStarDifficulty"/> and triggers an initial value update. /// Creates a new <see cref="BindableStarDifficulty"/> and triggers an initial value update.
/// </summary> /// </summary>
/// <param name="beatmapInfo">The <see cref="BeatmapInfo"/> that star difficulty should correspond to.</param> /// <param name="beatmapInfo">The <see cref="IBeatmapInfo"/> that star difficulty should correspond to.</param>
/// <param name="initialRulesetInfo">The initial <see cref="RulesetInfo"/> to get the difficulty with.</param> /// <param name="initialRulesetInfo">The initial <see cref="IRulesetInfo"/> to get the difficulty with.</param>
/// <param name="initialMods">The initial <see cref="Mod"/>s to get the difficulty with.</param> /// <param name="initialMods">The initial <see cref="Mod"/>s to get the difficulty with.</param>
/// <param name="cancellationToken">An optional <see cref="CancellationToken"/> which stops updating the star difficulty for the given <see cref="BeatmapInfo"/>.</param> /// <param name="cancellationToken">An optional <see cref="CancellationToken"/> which stops updating the star difficulty for the given <see cref="IBeatmapInfo"/>.</param>
/// <returns>The <see cref="BindableStarDifficulty"/>.</returns> /// <returns>The <see cref="BindableStarDifficulty"/>.</returns>
private BindableStarDifficulty createBindable([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo initialRulesetInfo, [CanBeNull] IEnumerable<Mod> initialMods, private BindableStarDifficulty createBindable([NotNull] IBeatmapInfo beatmapInfo, [CanBeNull] IRulesetInfo initialRulesetInfo, [CanBeNull] IEnumerable<Mod> initialMods,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
var bindable = new BindableStarDifficulty(beatmapInfo, cancellationToken); var bindable = new BindableStarDifficulty(beatmapInfo, cancellationToken);
@ -244,12 +247,12 @@ namespace osu.Game.Beatmaps
/// Updates the value of a <see cref="BindableStarDifficulty"/> with a given ruleset + mods. /// Updates the value of a <see cref="BindableStarDifficulty"/> with a given ruleset + mods.
/// </summary> /// </summary>
/// <param name="bindable">The <see cref="BindableStarDifficulty"/> to update.</param> /// <param name="bindable">The <see cref="BindableStarDifficulty"/> to update.</param>
/// <param name="rulesetInfo">The <see cref="RulesetInfo"/> to update with.</param> /// <param name="rulesetInfo">The <see cref="IRulesetInfo"/> to update with.</param>
/// <param name="mods">The <see cref="Mod"/>s to update with.</param> /// <param name="mods">The <see cref="Mod"/>s to update with.</param>
/// <param name="cancellationToken">A token that may be used to cancel this update.</param> /// <param name="cancellationToken">A token that may be used to cancel this update.</param>
private void updateBindable([NotNull] BindableStarDifficulty bindable, [CanBeNull] RulesetInfo rulesetInfo, [CanBeNull] IEnumerable<Mod> mods, CancellationToken cancellationToken = default) private void updateBindable([NotNull] BindableStarDifficulty bindable, [CanBeNull] IRulesetInfo rulesetInfo, [CanBeNull] IEnumerable<Mod> mods, CancellationToken cancellationToken = default)
{ {
// GetDifficultyAsync will fall back to existing data from BeatmapInfo if not locally available // GetDifficultyAsync will fall back to existing data from IBeatmapInfo if not locally available
// (contrary to GetAsync) // (contrary to GetAsync)
GetDifficultyAsync(bindable.BeatmapInfo, rulesetInfo, mods, cancellationToken) GetDifficultyAsync(bindable.BeatmapInfo, rulesetInfo, mods, cancellationToken)
.ContinueWith(t => .ContinueWith(t =>
@ -343,10 +346,10 @@ namespace osu.Game.Beatmaps
private class BindableStarDifficulty : Bindable<StarDifficulty?> private class BindableStarDifficulty : Bindable<StarDifficulty?>
{ {
public readonly BeatmapInfo BeatmapInfo; public readonly IBeatmapInfo BeatmapInfo;
public readonly CancellationToken CancellationToken; public readonly CancellationToken CancellationToken;
public BindableStarDifficulty(BeatmapInfo beatmapInfo, CancellationToken cancellationToken) public BindableStarDifficulty(IBeatmapInfo beatmapInfo, CancellationToken cancellationToken)
{ {
BeatmapInfo = beatmapInfo; BeatmapInfo = beatmapInfo;
CancellationToken = cancellationToken; CancellationToken = cancellationToken;

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

@ -37,10 +37,10 @@ namespace osu.Game.Beatmaps.Drawables
} }
[NotNull] [NotNull]
private readonly BeatmapInfo beatmapInfo; private readonly IBeatmapInfo beatmapInfo;
[CanBeNull] [CanBeNull]
private readonly RulesetInfo ruleset; private readonly IRulesetInfo ruleset;
[CanBeNull] [CanBeNull]
private readonly IReadOnlyList<Mod> mods; private readonly IReadOnlyList<Mod> mods;
@ -60,7 +60,7 @@ namespace osu.Game.Beatmaps.Drawables
/// <param name="ruleset">The ruleset to show the difficulty with.</param> /// <param name="ruleset">The ruleset to show the difficulty with.</param>
/// <param name="mods">The mods to show the difficulty with.</param> /// <param name="mods">The mods to show the difficulty with.</param>
/// <param name="shouldShowTooltip">Whether to display a tooltip when hovered.</param> /// <param name="shouldShowTooltip">Whether to display a tooltip when hovered.</param>
public DifficultyIcon([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo ruleset, [CanBeNull] IReadOnlyList<Mod> mods, bool shouldShowTooltip = true) public DifficultyIcon([NotNull] IBeatmapInfo beatmapInfo, [CanBeNull] IRulesetInfo ruleset, [CanBeNull] IReadOnlyList<Mod> mods, bool shouldShowTooltip = true)
: this(beatmapInfo, shouldShowTooltip) : this(beatmapInfo, shouldShowTooltip)
{ {
this.ruleset = ruleset ?? beatmapInfo.Ruleset; this.ruleset = ruleset ?? beatmapInfo.Ruleset;
@ -73,7 +73,7 @@ namespace osu.Game.Beatmaps.Drawables
/// <param name="beatmapInfo">The beatmap to show the difficulty of.</param> /// <param name="beatmapInfo">The beatmap to show the difficulty of.</param>
/// <param name="shouldShowTooltip">Whether to display a tooltip when hovered.</param> /// <param name="shouldShowTooltip">Whether to display a tooltip when hovered.</param>
/// <param name="performBackgroundDifficultyLookup">Whether to perform difficulty lookup (including calculation if necessary).</param> /// <param name="performBackgroundDifficultyLookup">Whether to perform difficulty lookup (including calculation if necessary).</param>
public DifficultyIcon([NotNull] BeatmapInfo beatmapInfo, bool shouldShowTooltip = true, bool performBackgroundDifficultyLookup = true) public DifficultyIcon([NotNull] IBeatmapInfo beatmapInfo, bool shouldShowTooltip = true, bool performBackgroundDifficultyLookup = true)
{ {
this.beatmapInfo = beatmapInfo ?? throw new ArgumentNullException(nameof(beatmapInfo)); this.beatmapInfo = beatmapInfo ?? throw new ArgumentNullException(nameof(beatmapInfo));
this.shouldShowTooltip = shouldShowTooltip; this.shouldShowTooltip = shouldShowTooltip;
@ -84,6 +84,9 @@ namespace osu.Game.Beatmaps.Drawables
InternalChild = iconContainer = new Container { Size = new Vector2(20f) }; InternalChild = iconContainer = new Container { Size = new Vector2(20f) };
} }
[Resolved]
private RulesetStore rulesets { get; set; }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colours) private void load(OsuColour colours)
{ {
@ -105,7 +108,7 @@ namespace osu.Game.Beatmaps.Drawables
Child = background = new Box Child = background = new Box
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Colour = colours.ForStarDifficulty(beatmapInfo.StarDifficulty) // Default value that will be re-populated once difficulty calculation completes Colour = colours.ForStarDifficulty(beatmapInfo.StarRating) // Default value that will be re-populated once difficulty calculation completes
}, },
}, },
new ConstrainedIconContainer new ConstrainedIconContainer
@ -114,18 +117,28 @@ namespace osu.Game.Beatmaps.Drawables
Origin = Anchor.Centre, Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
// the null coalesce here is only present to make unit tests work (ruleset dlls aren't copied correctly for testing at the moment) // the null coalesce here is only present to make unit tests work (ruleset dlls aren't copied correctly for testing at the moment)
Icon = (ruleset ?? beatmapInfo.Ruleset)?.CreateInstance()?.CreateIcon() ?? new SpriteIcon { Icon = FontAwesome.Regular.QuestionCircle } Icon = getRulesetIcon()
}, },
}; };
if (performBackgroundDifficultyLookup) if (performBackgroundDifficultyLookup)
iconContainer.Add(new DelayedLoadUnloadWrapper(() => new DifficultyRetriever(beatmapInfo, ruleset, mods) { StarDifficulty = { BindTarget = difficultyBindable } }, 0)); iconContainer.Add(new DelayedLoadUnloadWrapper(() => new DifficultyRetriever(beatmapInfo, ruleset, mods) { StarDifficulty = { BindTarget = difficultyBindable } }, 0));
else else
difficultyBindable.Value = new StarDifficulty(beatmapInfo.StarDifficulty, 0); difficultyBindable.Value = new StarDifficulty(beatmapInfo.StarRating, 0);
difficultyBindable.BindValueChanged(difficulty => background.Colour = colours.ForStarDifficulty(difficulty.NewValue.Stars)); difficultyBindable.BindValueChanged(difficulty => background.Colour = colours.ForStarDifficulty(difficulty.NewValue.Stars));
} }
private Drawable getRulesetIcon()
{
int? onlineID = (ruleset ?? beatmapInfo.Ruleset).OnlineID;
if (onlineID >= 0 && rulesets.GetRuleset(onlineID.Value)?.CreateInstance() is Ruleset rulesetInstance)
return rulesetInstance.CreateIcon();
return new SpriteIcon { Icon = FontAwesome.Regular.QuestionCircle };
}
ITooltip<DifficultyIconTooltipContent> IHasCustomTooltip<DifficultyIconTooltipContent>.GetCustomTooltip() => new DifficultyIconTooltip(); ITooltip<DifficultyIconTooltipContent> IHasCustomTooltip<DifficultyIconTooltipContent>.GetCustomTooltip() => new DifficultyIconTooltip();
DifficultyIconTooltipContent IHasCustomTooltip<DifficultyIconTooltipContent>.TooltipContent => shouldShowTooltip ? new DifficultyIconTooltipContent(beatmapInfo, difficultyBindable) : null; DifficultyIconTooltipContent IHasCustomTooltip<DifficultyIconTooltipContent>.TooltipContent => shouldShowTooltip ? new DifficultyIconTooltipContent(beatmapInfo, difficultyBindable) : null;
@ -134,8 +147,8 @@ namespace osu.Game.Beatmaps.Drawables
{ {
public readonly Bindable<StarDifficulty> StarDifficulty = new Bindable<StarDifficulty>(); public readonly Bindable<StarDifficulty> StarDifficulty = new Bindable<StarDifficulty>();
private readonly BeatmapInfo beatmapInfo; private readonly IBeatmapInfo beatmapInfo;
private readonly RulesetInfo ruleset; private readonly IRulesetInfo ruleset;
private readonly IReadOnlyList<Mod> mods; private readonly IReadOnlyList<Mod> mods;
private CancellationTokenSource difficultyCancellation; private CancellationTokenSource difficultyCancellation;
@ -143,7 +156,7 @@ namespace osu.Game.Beatmaps.Drawables
[Resolved] [Resolved]
private BeatmapDifficultyCache difficultyCache { get; set; } private BeatmapDifficultyCache difficultyCache { get; set; }
public DifficultyRetriever(BeatmapInfo beatmapInfo, RulesetInfo ruleset, IReadOnlyList<Mod> mods) public DifficultyRetriever(IBeatmapInfo beatmapInfo, IRulesetInfo ruleset, IReadOnlyList<Mod> mods)
{ {
this.beatmapInfo = beatmapInfo; this.beatmapInfo = beatmapInfo;
this.ruleset = ruleset; this.ruleset = ruleset;

View File

@ -89,7 +89,7 @@ namespace osu.Game.Beatmaps.Drawables
public void SetContent(DifficultyIconTooltipContent content) public void SetContent(DifficultyIconTooltipContent content)
{ {
difficultyName.Text = content.BeatmapInfo.Version; difficultyName.Text = content.BeatmapInfo.DifficultyName;
starDifficulty.UnbindAll(); starDifficulty.UnbindAll();
starDifficulty.BindTo(content.Difficulty); starDifficulty.BindTo(content.Difficulty);
@ -109,10 +109,10 @@ namespace osu.Game.Beatmaps.Drawables
internal class DifficultyIconTooltipContent internal class DifficultyIconTooltipContent
{ {
public readonly BeatmapInfo BeatmapInfo; public readonly IBeatmapInfo BeatmapInfo;
public readonly IBindable<StarDifficulty> Difficulty; public readonly IBindable<StarDifficulty> Difficulty;
public DifficultyIconTooltipContent(BeatmapInfo beatmapInfo, IBindable<StarDifficulty> difficulty) public DifficultyIconTooltipContent(IBeatmapInfo beatmapInfo, IBindable<StarDifficulty> difficulty)
{ {
BeatmapInfo = beatmapInfo; BeatmapInfo = beatmapInfo;
Difficulty = difficulty; Difficulty = difficulty;

View File

@ -19,8 +19,8 @@ namespace osu.Game.Beatmaps.Drawables
/// </remarks> /// </remarks>
public class GroupedDifficultyIcon : DifficultyIcon public class GroupedDifficultyIcon : DifficultyIcon
{ {
public GroupedDifficultyIcon(List<BeatmapInfo> beatmaps, RulesetInfo ruleset, Color4 counterColour) public GroupedDifficultyIcon(IEnumerable<IBeatmapInfo> beatmaps, IRulesetInfo ruleset, Color4 counterColour)
: base(beatmaps.OrderBy(b => b.StarDifficulty).Last(), ruleset, null, false) : base(beatmaps.OrderBy(b => b.StarRating).Last(), ruleset, null, false)
{ {
AddInternal(new OsuSpriteText AddInternal(new OsuSpriteText
{ {
@ -29,7 +29,7 @@ namespace osu.Game.Beatmaps.Drawables
Padding = new MarginPadding { Left = Size.X }, Padding = new MarginPadding { Left = Size.X },
Margin = new MarginPadding { Left = 2, Right = 5 }, Margin = new MarginPadding { Left = 2, Right = 5 },
Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold), Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold),
Text = beatmaps.Count.ToString(), Text = beatmaps.Count().ToString(),
Colour = counterColour, Colour = counterColour,
}); });
} }

View File

@ -8,7 +8,7 @@ namespace osu.Game.Database
public interface IHasOnlineID<out T> public interface IHasOnlineID<out T>
{ {
/// <summary> /// <summary>
/// The server-side ID representing this instance, if one exists. Any value 0 or less denotes a missing ID. /// The server-side ID representing this instance, if one exists. Any value 0 or less denotes a missing ID (except in special cases where autoincrement is not used, like rulesets).
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// Generally we use -1 when specifying "missing" in code, but values of 0 are also considered missing as the online source /// Generally we use -1 when specifying "missing" in code, but values of 0 are also considered missing as the online source

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

@ -6,9 +6,11 @@ using System.Linq;
using System.Threading; using System.Threading;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Development; using osu.Framework.Development;
using osu.Framework.Input.Bindings;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Framework.Statistics; using osu.Framework.Statistics;
using osu.Game.Input.Bindings;
using osu.Game.Models; using osu.Game.Models;
using Realms; using Realms;
@ -32,8 +34,9 @@ namespace osu.Game.Database
/// Version history: /// Version history:
/// 6 First tracked version (~20211018) /// 6 First tracked version (~20211018)
/// 7 Changed OnlineID fields to non-nullable to add indexing support (20211018) /// 7 Changed OnlineID fields to non-nullable to add indexing support (20211018)
/// 8 Rebind scroll adjust keys to not have control modifier (20211029)
/// </summary> /// </summary>
private const int schema_version = 7; private const int schema_version = 8;
/// <summary> /// <summary>
/// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking context creation during blocking periods. /// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking context creation during blocking periods.
@ -148,6 +151,21 @@ namespace osu.Game.Database
private void onMigration(Migration migration, ulong lastSchemaVersion) private void onMigration(Migration migration, ulong lastSchemaVersion)
{ {
if (lastSchemaVersion < 8)
{
// Ctrl -/+ now adjusts UI scale so let's clear any bindings which overlap these combinations.
// New defaults will be populated by the key store afterwards.
var keyBindings = migration.NewRealm.All<RealmKeyBinding>();
var increaseSpeedBinding = keyBindings.FirstOrDefault(k => k.ActionInt == (int)GlobalAction.IncreaseScrollSpeed);
if (increaseSpeedBinding != null && increaseSpeedBinding.KeyCombination.Keys.SequenceEqual(new[] { InputKey.Control, InputKey.Plus }))
migration.NewRealm.Remove(increaseSpeedBinding);
var decreaseSpeedBinding = keyBindings.FirstOrDefault(k => k.ActionInt == (int)GlobalAction.DecreaseScrollSpeed);
if (decreaseSpeedBinding != null && decreaseSpeedBinding.KeyCombination.Keys.SequenceEqual(new[] { InputKey.Control, InputKey.Minus }))
migration.NewRealm.Remove(decreaseSpeedBinding);
}
if (lastSchemaVersion < 7) if (lastSchemaVersion < 7)
{ {
convertOnlineIDs<RealmBeatmap>(); convertOnlineIDs<RealmBeatmap>();

View File

@ -9,6 +9,7 @@ using osu.Framework.Graphics.Sprites;
using System.Collections.Generic; using System.Collections.Generic;
using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
@ -58,39 +59,34 @@ namespace osu.Game.Graphics.Containers
} }
public void AddLink(string text, string url, Action<SpriteText> creationParameters = null) => public void AddLink(string text, string url, Action<SpriteText> creationParameters = null) =>
createLink(AddText(text, creationParameters), new LinkDetails(LinkAction.External, url), url); createLink(CreateChunkFor(text, true, CreateSpriteText, creationParameters), new LinkDetails(LinkAction.External, url), url);
public void AddLink(string text, Action action, string tooltipText = null, Action<SpriteText> creationParameters = null) public void AddLink(string text, Action action, string tooltipText = null, Action<SpriteText> creationParameters = null)
=> createLink(AddText(text, creationParameters), new LinkDetails(LinkAction.Custom, string.Empty), tooltipText, action); => createLink(CreateChunkFor(text, true, CreateSpriteText, creationParameters), new LinkDetails(LinkAction.Custom, string.Empty), tooltipText, action);
public void AddLink(string text, LinkAction action, string argument, string tooltipText = null, Action<SpriteText> creationParameters = null) public void AddLink(string text, LinkAction action, string argument, string tooltipText = null, Action<SpriteText> creationParameters = null)
=> createLink(AddText(text, creationParameters), new LinkDetails(action, argument), tooltipText); => createLink(CreateChunkFor(text, true, CreateSpriteText, creationParameters), new LinkDetails(action, argument), tooltipText);
public void AddLink(LocalisableString text, LinkAction action, string argument, string tooltipText = null, Action<SpriteText> creationParameters = null) public void AddLink(LocalisableString text, LinkAction action, string argument, string tooltipText = null, Action<SpriteText> creationParameters = null)
{ {
var spriteText = new OsuSpriteText { Text = text }; var spriteText = new OsuSpriteText { Text = text };
AddText(spriteText, creationParameters); AddText(spriteText, creationParameters);
createLink(spriteText.Yield(), new LinkDetails(action, argument), tooltipText); RemoveInternal(spriteText); // TODO: temporary, will go away when TextParts support localisation properly.
createLink(new TextPartManual(spriteText.Yield()), new LinkDetails(action, argument), tooltipText);
} }
public void AddLink(IEnumerable<SpriteText> text, LinkAction action, string linkArgument, string tooltipText = null) public void AddLink(IEnumerable<SpriteText> text, LinkAction action, string linkArgument, string tooltipText = null)
{ {
foreach (var t in text) createLink(new TextPartManual(text), new LinkDetails(action, linkArgument), tooltipText);
AddArbitraryDrawable(t);
createLink(text, new LinkDetails(action, linkArgument), tooltipText);
} }
public void AddUserLink(User user, Action<SpriteText> creationParameters = null) public void AddUserLink(User user, Action<SpriteText> creationParameters = null)
=> createLink(AddText(user.Username, creationParameters), new LinkDetails(LinkAction.OpenUserProfile, user.Id.ToString()), "view profile"); => createLink(CreateChunkFor(user.Username, true, CreateSpriteText, creationParameters), new LinkDetails(LinkAction.OpenUserProfile, user.Id.ToString()), "view profile");
private void createLink(IEnumerable<Drawable> drawables, LinkDetails link, string tooltipText, Action action = null) private void createLink(ITextPart textPart, LinkDetails link, LocalisableString tooltipText, Action action = null)
{ {
var linkCompiler = CreateLinkCompiler(drawables.OfType<SpriteText>()); Action onClickAction = () =>
linkCompiler.RelativeSizeAxes = Axes.Both;
linkCompiler.TooltipText = tooltipText;
linkCompiler.Action = () =>
{ {
if (action != null) if (action != null)
action(); action();
@ -101,10 +97,41 @@ namespace osu.Game.Graphics.Containers
host.OpenUrlExternally(link.Argument); host.OpenUrlExternally(link.Argument);
}; };
AddInternal(linkCompiler); AddPart(new TextLink(textPart, tooltipText, onClickAction));
} }
protected virtual DrawableLinkCompiler CreateLinkCompiler(IEnumerable<SpriteText> parts) => new DrawableLinkCompiler(parts); private class TextLink : TextPart
{
private readonly ITextPart innerPart;
private readonly LocalisableString tooltipText;
private readonly Action action;
public TextLink(ITextPart innerPart, LocalisableString tooltipText, Action action)
{
this.innerPart = innerPart;
this.tooltipText = tooltipText;
this.action = action;
}
protected override IEnumerable<Drawable> CreateDrawablesFor(TextFlowContainer textFlowContainer)
{
var linkFlowContainer = (LinkFlowContainer)textFlowContainer;
innerPart.RecreateDrawablesFor(linkFlowContainer);
var drawables = innerPart.Drawables.ToList();
drawables.Add(linkFlowContainer.CreateLinkCompiler(innerPart).With(c =>
{
c.RelativeSizeAxes = Axes.Both;
c.TooltipText = tooltipText;
c.Action = action;
}));
return drawables;
}
}
protected virtual DrawableLinkCompiler CreateLinkCompiler(ITextPart textPart) => new DrawableLinkCompiler(textPart);
// We want the compilers to always be visible no matter where they are, so RelativeSizeAxes is used. // We want the compilers to always be visible no matter where they are, so RelativeSizeAxes is used.
// However due to https://github.com/ppy/osu-framework/issues/2073, it's possible for the compilers to be relative size in the flow's auto-size axes - an unsupported operation. // However due to https://github.com/ppy/osu-framework/issues/2073, it's possible for the compilers to be relative size in the flow's auto-size axes - an unsupported operation.

View File

@ -2,7 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Collections.Generic; using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
@ -19,8 +19,8 @@ namespace osu.Game.Graphics.Containers
protected override SpriteText CreateSpriteText() => new OsuSpriteText(); protected override SpriteText CreateSpriteText() => new OsuSpriteText();
public void AddArbitraryDrawable(Drawable drawable) => AddInternal(drawable); public ITextPart AddArbitraryDrawable(Drawable drawable) => AddPart(new TextPartManual(drawable.Yield()));
public IEnumerable<Drawable> AddIcon(IconUsage icon, Action<SpriteText> creationParameters = null) => AddText(icon.Icon.ToString(), creationParameters); public ITextPart AddIcon(IconUsage icon, Action<SpriteText> creationParameters = null) => AddText(icon.Icon.ToString(), creationParameters);
} }
} }

View File

@ -2,7 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic; using System.Collections.Generic;
using osu.Framework.Graphics; using osu.Framework.Graphics.Containers;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osuTK.Graphics; using osuTK.Graphics;
@ -10,7 +10,7 @@ namespace osu.Game.Graphics
{ {
public class ErrorTextFlowContainer : OsuTextFlowContainer public class ErrorTextFlowContainer : OsuTextFlowContainer
{ {
private readonly List<Drawable> errorDrawables = new List<Drawable>(); private readonly List<ITextPart> errorTextParts = new List<ITextPart>();
public ErrorTextFlowContainer() public ErrorTextFlowContainer()
: base(cp => cp.Font = cp.Font.With(size: 12)) : base(cp => cp.Font = cp.Font.With(size: 12))
@ -19,7 +19,8 @@ namespace osu.Game.Graphics
public void ClearErrors() public void ClearErrors()
{ {
errorDrawables.ForEach(d => d.Expire()); foreach (var textPart in errorTextParts)
RemovePart(textPart);
} }
public void AddErrors(string[] errors) public void AddErrors(string[] errors)
@ -29,7 +30,7 @@ namespace osu.Game.Graphics
if (errors == null) return; if (errors == null) return;
foreach (string error in errors) foreach (string error in errors)
errorDrawables.AddRange(AddParagraph(error, cp => cp.Colour = Color4.Red)); errorTextParts.Add(AddParagraph(error, cp => cp.Colour = Color4.Red));
} }
} }
} }

View File

@ -84,8 +84,8 @@ namespace osu.Game.Input.Bindings
new KeyBinding(InputKey.ExtraMouseButton2, GlobalAction.SkipCutscene), new KeyBinding(InputKey.ExtraMouseButton2, GlobalAction.SkipCutscene),
new KeyBinding(InputKey.Tilde, GlobalAction.QuickRetry), new KeyBinding(InputKey.Tilde, GlobalAction.QuickRetry),
new KeyBinding(new[] { InputKey.Control, InputKey.Tilde }, GlobalAction.QuickExit), new KeyBinding(new[] { InputKey.Control, InputKey.Tilde }, GlobalAction.QuickExit),
new KeyBinding(new[] { InputKey.Control, InputKey.Plus }, GlobalAction.IncreaseScrollSpeed), new KeyBinding(new[] { InputKey.F3 }, GlobalAction.DecreaseScrollSpeed),
new KeyBinding(new[] { InputKey.Control, InputKey.Minus }, GlobalAction.DecreaseScrollSpeed), new KeyBinding(new[] { InputKey.F4 }, GlobalAction.IncreaseScrollSpeed),
new KeyBinding(new[] { InputKey.Shift, InputKey.Tab }, GlobalAction.ToggleInGameInterface), new KeyBinding(new[] { InputKey.Shift, InputKey.Tab }, GlobalAction.ToggleInGameInterface),
new KeyBinding(InputKey.MouseMiddle, GlobalAction.PauseGameplay), new KeyBinding(InputKey.MouseMiddle, GlobalAction.PauseGameplay),
new KeyBinding(InputKey.Space, GlobalAction.TogglePauseReplay), new KeyBinding(InputKey.Space, GlobalAction.TogglePauseReplay),

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

@ -26,13 +26,11 @@ namespace osu.Game.Online.API.Requests.Responses
[JsonProperty(@"user")] [JsonProperty(@"user")]
public User User { get; set; } public User User { get; set; }
public bool HasReplay { get; set; }
[JsonProperty(@"id")] [JsonProperty(@"id")]
public long OnlineID { get; set; } public long OnlineID { get; set; }
[JsonProperty(@"replay")] [JsonProperty(@"replay")]
public bool Replay { get; set; } public bool HasReplay { get; set; }
[JsonProperty(@"created_at")] [JsonProperty(@"created_at")]
public DateTimeOffset Date { get; set; } public DateTimeOffset Date { get; set; }
@ -52,8 +50,11 @@ namespace osu.Game.Online.API.Requests.Responses
set set
{ {
// in the deserialisation case we need to ferry this data across. // in the deserialisation case we need to ferry this data across.
if (Beatmap is APIBeatmap apiBeatmap) // the order of properties returned by the API guarantees that the beatmap is populated by this point.
apiBeatmap.BeatmapSet = value; if (!(Beatmap is APIBeatmap apiBeatmap))
throw new InvalidOperationException("Beatmap set metadata arrived before beatmap metadata in response");
apiBeatmap.BeatmapSet = value;
} }
} }
@ -91,13 +92,14 @@ namespace osu.Game.Online.API.Requests.Responses
{ {
TotalScore = TotalScore, TotalScore = TotalScore,
MaxCombo = MaxCombo, MaxCombo = MaxCombo,
BeatmapInfo = Beatmap.ToBeatmapInfo(rulesets),
User = User, User = User,
Accuracy = Accuracy, Accuracy = Accuracy,
OnlineScoreID = OnlineID, OnlineScoreID = OnlineID,
Date = Date, Date = Date,
PP = PP, PP = PP,
RulesetID = OnlineRulesetID, RulesetID = OnlineRulesetID,
Hash = Replay ? "online" : string.Empty, // todo: temporary? Hash = HasReplay ? "online" : string.Empty, // todo: temporary?
Rank = Rank, Rank = Rank,
Ruleset = ruleset, Ruleset = ruleset,
Mods = mods, Mods = mods,

View File

@ -17,7 +17,6 @@ namespace osu.Game.Online.API.Requests.Responses
public APIScoreInfo Score; public APIScoreInfo Score;
public ScoreInfo CreateScoreInfo(RulesetStore rulesets, BeatmapInfo beatmap = null) public ScoreInfo CreateScoreInfo(RulesetStore rulesets, BeatmapInfo beatmap = null)
{ {
var score = Score.CreateScoreInfo(rulesets, beatmap); var score = Score.CreateScoreInfo(rulesets, beatmap);
score.Position = Position; score.Position = Position;

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

@ -5,6 +5,8 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
@ -30,6 +32,11 @@ namespace osu.Game.Online.Chat
protected override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new LinkHoverSounds(sampleSet, Parts); protected override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new LinkHoverSounds(sampleSet, Parts);
public DrawableLinkCompiler(ITextPart part)
: this(part.Drawables.OfType<SpriteText>())
{
}
public DrawableLinkCompiler(IEnumerable<Drawable> parts) public DrawableLinkCompiler(IEnumerable<Drawable> parts)
: base(HoverSampleSet.Submit) : base(HoverSampleSet.Submit)
{ {

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

@ -2,7 +2,6 @@
// 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.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using osu.Framework.Allocation; using osu.Framework.Allocation;
@ -37,7 +36,7 @@ namespace osu.Game.Overlays.AccountCreation
private IAPIProvider api { get; set; } private IAPIProvider api { get; set; }
private ShakeContainer registerShake; private ShakeContainer registerShake;
private IEnumerable<Drawable> characterCheckText; private ITextPart characterCheckText;
private OsuTextBox[] textboxes; private OsuTextBox[] textboxes;
private LoadingLayer loadingLayer; private LoadingLayer loadingLayer;
@ -136,7 +135,7 @@ namespace osu.Game.Overlays.AccountCreation
characterCheckText = passwordDescription.AddText("8 characters long"); characterCheckText = passwordDescription.AddText("8 characters long");
passwordDescription.AddText(". Choose something long but also something you will remember, like a line from your favourite song."); passwordDescription.AddText(". Choose something long but also something you will remember, like a line from your favourite song.");
passwordTextBox.Current.ValueChanged += password => { characterCheckText.ForEach(s => s.Colour = password.NewValue.Length == 0 ? Color4.White : Interpolation.ValueAt(password.NewValue.Length, Color4.OrangeRed, Color4.YellowGreen, 0, 8, Easing.In)); }; passwordTextBox.Current.ValueChanged += password => { characterCheckText.Drawables.ForEach(s => s.Colour = password.NewValue.Length == 0 ? Color4.White : Interpolation.ValueAt(password.NewValue.Length, Color4.OrangeRed, Color4.YellowGreen, 0, 8, Easing.In)); };
} }
public override void OnEntering(IScreen last) public override void OnEntering(IScreen last)

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

@ -2,7 +2,6 @@
// 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.Collections.Generic;
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;
@ -166,12 +165,12 @@ namespace osu.Game.Overlays.Changelog
{ {
} }
protected override DrawableLinkCompiler CreateLinkCompiler(IEnumerable<SpriteText> parts) => new SupporterPromoLinkCompiler(parts); protected override DrawableLinkCompiler CreateLinkCompiler(ITextPart textPart) => new SupporterPromoLinkCompiler(textPart);
private class SupporterPromoLinkCompiler : DrawableLinkCompiler private class SupporterPromoLinkCompiler : DrawableLinkCompiler
{ {
public SupporterPromoLinkCompiler(IEnumerable<Drawable> parts) public SupporterPromoLinkCompiler(ITextPart part)
: base(parts) : base(part)
{ {
} }

View File

@ -5,17 +5,17 @@ using System.Collections.Generic;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps; using osu.Game.Online.API.Requests.Responses;
using osuTK; using osuTK;
namespace osu.Game.Overlays.Dashboard.Home namespace osu.Game.Overlays.Dashboard.Home
{ {
public class DashboardBeatmapListing : CompositeDrawable public class DashboardBeatmapListing : CompositeDrawable
{ {
private readonly List<BeatmapSetInfo> newBeatmaps; private readonly List<APIBeatmapSet> newBeatmaps;
private readonly List<BeatmapSetInfo> popularBeatmaps; private readonly List<APIBeatmapSet> popularBeatmaps;
public DashboardBeatmapListing(List<BeatmapSetInfo> newBeatmaps, List<BeatmapSetInfo> popularBeatmaps) public DashboardBeatmapListing(List<APIBeatmapSet> newBeatmaps, List<APIBeatmapSet> popularBeatmaps)
{ {
this.newBeatmaps = newBeatmaps; this.newBeatmaps = newBeatmaps;
this.popularBeatmaps = popularBeatmaps; this.popularBeatmaps = popularBeatmaps;

View File

@ -7,11 +7,11 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
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.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Online.API.Requests.Responses;
using osuTK; using osuTK;
namespace osu.Game.Overlays.Dashboard.Home namespace osu.Game.Overlays.Dashboard.Home
@ -24,14 +24,14 @@ namespace osu.Game.Overlays.Dashboard.Home
[Resolved(canBeNull: true)] [Resolved(canBeNull: true)]
private BeatmapSetOverlay beatmapOverlay { get; set; } private BeatmapSetOverlay beatmapOverlay { get; set; }
protected readonly BeatmapSetInfo SetInfo; protected readonly APIBeatmapSet BeatmapSet;
private Box hoverBackground; private Box hoverBackground;
private SpriteIcon chevron; private SpriteIcon chevron;
protected DashboardBeatmapPanel(BeatmapSetInfo setInfo) protected DashboardBeatmapPanel(APIBeatmapSet beatmapSet)
{ {
SetInfo = setInfo; BeatmapSet = beatmapSet;
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
@ -82,7 +82,7 @@ namespace osu.Game.Overlays.Dashboard.Home
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
OnlineInfo = SetInfo.OnlineInfo OnlineInfo = BeatmapSet
} }
}, },
new Container new Container
@ -103,14 +103,14 @@ namespace osu.Game.Overlays.Dashboard.Home
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Truncate = true, Truncate = true,
Font = OsuFont.GetFont(weight: FontWeight.Regular), Font = OsuFont.GetFont(weight: FontWeight.Regular),
Text = SetInfo.Metadata.Title Text = BeatmapSet.Title
}, },
new OsuSpriteText new OsuSpriteText
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Truncate = true, Truncate = true,
Font = OsuFont.GetFont(size: 12, weight: FontWeight.Regular), Font = OsuFont.GetFont(size: 12, weight: FontWeight.Regular),
Text = SetInfo.Metadata.Artist Text = BeatmapSet.Artist
}, },
new LinkFlowContainer(f => f.Font = OsuFont.GetFont(size: 10, weight: FontWeight.Regular)) new LinkFlowContainer(f => f.Font = OsuFont.GetFont(size: 10, weight: FontWeight.Regular))
{ {
@ -121,7 +121,7 @@ namespace osu.Game.Overlays.Dashboard.Home
}.With(c => }.With(c =>
{ {
c.AddText("by"); c.AddText("by");
c.AddUserLink(SetInfo.Metadata.Author); c.AddUserLink(BeatmapSet.Author);
c.AddArbitraryDrawable(CreateInfo()); c.AddArbitraryDrawable(CreateInfo());
}) })
} }
@ -143,8 +143,8 @@ namespace osu.Game.Overlays.Dashboard.Home
Action = () => Action = () =>
{ {
if (SetInfo.OnlineBeatmapSetID.HasValue) if (BeatmapSet.OnlineID > 0)
beatmapOverlay?.FetchAndShowBeatmapSet(SetInfo.OnlineBeatmapSetID.Value); beatmapOverlay?.FetchAndShowBeatmapSet(BeatmapSet.OnlineID);
}; };
} }

View File

@ -3,19 +3,19 @@
using System; using System;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Online.API.Requests.Responses;
namespace osu.Game.Overlays.Dashboard.Home namespace osu.Game.Overlays.Dashboard.Home
{ {
public class DashboardNewBeatmapPanel : DashboardBeatmapPanel public class DashboardNewBeatmapPanel : DashboardBeatmapPanel
{ {
public DashboardNewBeatmapPanel(BeatmapSetInfo setInfo) public DashboardNewBeatmapPanel(APIBeatmapSet beatmapSet)
: base(setInfo) : base(beatmapSet)
{ {
} }
protected override Drawable CreateInfo() => new DrawableDate(SetInfo.OnlineInfo.Ranked ?? DateTimeOffset.Now, 10, false) protected override Drawable CreateInfo() => new DrawableDate(BeatmapSet.Ranked ?? DateTimeOffset.Now, 10, false)
{ {
Colour = ColourProvider.Foreground1 Colour = ColourProvider.Foreground1
}; };

View File

@ -4,17 +4,17 @@
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Game.Beatmaps;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Online.API.Requests.Responses;
using osuTK; using osuTK;
namespace osu.Game.Overlays.Dashboard.Home namespace osu.Game.Overlays.Dashboard.Home
{ {
public class DashboardPopularBeatmapPanel : DashboardBeatmapPanel public class DashboardPopularBeatmapPanel : DashboardBeatmapPanel
{ {
public DashboardPopularBeatmapPanel(BeatmapSetInfo setInfo) public DashboardPopularBeatmapPanel(APIBeatmapSet beatmapSet)
: base(setInfo) : base(beatmapSet)
{ {
} }
@ -34,7 +34,7 @@ namespace osu.Game.Overlays.Dashboard.Home
new OsuSpriteText new OsuSpriteText
{ {
Font = OsuFont.GetFont(size: 10, weight: FontWeight.Regular), Font = OsuFont.GetFont(size: 10, weight: FontWeight.Regular),
Text = SetInfo.OnlineInfo.FavouriteCount.ToString() Text = BeatmapSet.FavouriteCount.ToString()
} }
} }
}; };

View File

@ -6,20 +6,20 @@ using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Online.API.Requests.Responses;
using osuTK; using osuTK;
namespace osu.Game.Overlays.Dashboard.Home namespace osu.Game.Overlays.Dashboard.Home
{ {
public abstract class DrawableBeatmapList : CompositeDrawable public abstract class DrawableBeatmapList : CompositeDrawable
{ {
private readonly List<BeatmapSetInfo> beatmaps; private readonly List<APIBeatmapSet> beatmapSets;
protected DrawableBeatmapList(List<BeatmapSetInfo> beatmaps) protected DrawableBeatmapList(List<APIBeatmapSet> beatmapSets)
{ {
this.beatmaps = beatmaps; this.beatmapSets = beatmapSets;
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
@ -46,11 +46,11 @@ namespace osu.Game.Overlays.Dashboard.Home
} }
}; };
flow.AddRange(beatmaps.Select(CreateBeatmapPanel)); flow.AddRange(beatmapSets.Select(CreateBeatmapPanel));
} }
protected abstract string Title { get; } protected abstract string Title { get; }
protected abstract DashboardBeatmapPanel CreateBeatmapPanel(BeatmapSetInfo setInfo); protected abstract DashboardBeatmapPanel CreateBeatmapPanel(APIBeatmapSet beatmapSet);
} }
} }

View File

@ -2,18 +2,18 @@
// 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.Collections.Generic; using System.Collections.Generic;
using osu.Game.Beatmaps; using osu.Game.Online.API.Requests.Responses;
namespace osu.Game.Overlays.Dashboard.Home namespace osu.Game.Overlays.Dashboard.Home
{ {
public class DrawableNewBeatmapList : DrawableBeatmapList public class DrawableNewBeatmapList : DrawableBeatmapList
{ {
public DrawableNewBeatmapList(List<BeatmapSetInfo> beatmaps) public DrawableNewBeatmapList(List<APIBeatmapSet> beatmapSets)
: base(beatmaps) : base(beatmapSets)
{ {
} }
protected override DashboardBeatmapPanel CreateBeatmapPanel(BeatmapSetInfo setInfo) => new DashboardNewBeatmapPanel(setInfo); protected override DashboardBeatmapPanel CreateBeatmapPanel(APIBeatmapSet beatmapSet) => new DashboardNewBeatmapPanel(beatmapSet);
protected override string Title => "New Ranked Beatmaps"; protected override string Title => "New Ranked Beatmaps";
} }

View File

@ -2,18 +2,18 @@
// 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.Collections.Generic; using System.Collections.Generic;
using osu.Game.Beatmaps; using osu.Game.Online.API.Requests.Responses;
namespace osu.Game.Overlays.Dashboard.Home namespace osu.Game.Overlays.Dashboard.Home
{ {
public class DrawablePopularBeatmapList : DrawableBeatmapList public class DrawablePopularBeatmapList : DrawableBeatmapList
{ {
public DrawablePopularBeatmapList(List<BeatmapSetInfo> beatmaps) public DrawablePopularBeatmapList(List<APIBeatmapSet> beatmapSets)
: base(beatmaps) : base(beatmapSets)
{ {
} }
protected override DashboardBeatmapPanel CreateBeatmapPanel(BeatmapSetInfo setInfo) => new DashboardPopularBeatmapPanel(setInfo); protected override DashboardBeatmapPanel CreateBeatmapPanel(APIBeatmapSet beatmapSet) => new DashboardPopularBeatmapPanel(beatmapSet);
protected override string Title => "Popular Beatmaps"; protected override string Title => "Popular Beatmaps";
} }

View File

@ -3,12 +3,10 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
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.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
@ -25,7 +23,7 @@ namespace osu.Game.Overlays.Music
public Action<BeatmapSetInfo> RequestSelection; public Action<BeatmapSetInfo> RequestSelection;
private TextFlowContainer text; private TextFlowContainer text;
private IEnumerable<Drawable> titleSprites; private ITextPart titlePart;
private ILocalisedBindableString title; private ILocalisedBindableString title;
private ILocalisedBindableString artist; private ILocalisedBindableString artist;
@ -63,11 +61,16 @@ namespace osu.Game.Overlays.Music
if (set.OldValue?.Equals(Model) != true && set.NewValue?.Equals(Model) != true) if (set.OldValue?.Equals(Model) != true && set.NewValue?.Equals(Model) != true)
return; return;
foreach (Drawable s in titleSprites) updateSelectionState(false);
s.FadeColour(set.NewValue.Equals(Model) ? selectedColour : Color4.White, FADE_DURATION);
}, true); }, true);
} }
private void updateSelectionState(bool instant)
{
foreach (Drawable s in titlePart.Drawables)
s.FadeColour(SelectedSet.Value?.Equals(Model) == true ? selectedColour : Color4.White, instant ? 0 : FADE_DURATION);
}
protected override Drawable CreateContent() => text = new OsuTextFlowContainer protected override Drawable CreateContent() => text = new OsuTextFlowContainer
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
@ -79,7 +82,8 @@ namespace osu.Game.Overlays.Music
text.Clear(); text.Clear();
// space after the title to put a space between the title and artist // space after the title to put a space between the title and artist
titleSprites = text.AddText(title.Value + @" ", sprite => sprite.Font = OsuFont.GetFont(weight: FontWeight.Regular)).OfType<SpriteText>(); titlePart = text.AddText(title.Value + @" ", sprite => sprite.Font = OsuFont.GetFont(weight: FontWeight.Regular));
updateSelectionState(true);
text.AddText(artist.Value, sprite => text.AddText(artist.Value, sprite =>
{ {

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

@ -29,7 +29,7 @@ namespace osu.Game.Scoring
IRulesetInfo Ruleset { get; } IRulesetInfo Ruleset { get; }
public ScoreRank Rank { get; } ScoreRank Rank { get; }
// Mods is currently missing from this interface as the `IMod` class has properties which can't be fulfilled by `APIMod`, // Mods is currently missing from this interface as the `IMod` class has properties which can't be fulfilled by `APIMod`,
// but also doesn't expose `Settings`. We can consider how to implement this in the future if required. // but also doesn't expose `Settings`. We can consider how to implement this in the future if required.

View File

@ -244,10 +244,18 @@ namespace osu.Game.Scoring
return ReferenceEquals(this, other); return ReferenceEquals(this, other);
} }
#region Implementation of IHasOnlineID
public long OnlineID => OnlineScoreID ?? -1; public long OnlineID => OnlineScoreID ?? -1;
#endregion
#region Implementation of IScoreInfo
IBeatmapInfo IScoreInfo.Beatmap => BeatmapInfo; IBeatmapInfo IScoreInfo.Beatmap => BeatmapInfo;
IRulesetInfo IScoreInfo.Ruleset => Ruleset; IRulesetInfo IScoreInfo.Ruleset => Ruleset;
bool IScoreInfo.HasReplay => Files.Any(); bool IScoreInfo.HasReplay => Files.Any();
#endregion
} }
} }

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

@ -37,7 +37,7 @@ namespace osu.Game.Screens.Menu
private readonly Bindable<User> currentUser = new Bindable<User>(); private readonly Bindable<User> currentUser = new Bindable<User>();
private FillFlowContainer fill; private FillFlowContainer fill;
private readonly List<Drawable> expendableText = new List<Drawable>(); private readonly List<ITextPart> expendableText = new List<ITextPart>();
public Disclaimer(OsuScreen nextScreen = null) public Disclaimer(OsuScreen nextScreen = null)
{ {
@ -97,7 +97,7 @@ namespace osu.Game.Screens.Menu
textFlow.AddText("this is osu!", t => t.Font = t.Font.With(Typeface.Torus, 30, FontWeight.Regular)); textFlow.AddText("this is osu!", t => t.Font = t.Font.With(Typeface.Torus, 30, FontWeight.Regular));
expendableText.AddRange(textFlow.AddText("lazer", t => expendableText.Add(textFlow.AddText("lazer", t =>
{ {
t.Font = t.Font.With(Typeface.Torus, 30, FontWeight.Regular); t.Font = t.Font.With(Typeface.Torus, 30, FontWeight.Regular);
t.Colour = colours.PinkLight; t.Colour = colours.PinkLight;
@ -114,7 +114,7 @@ namespace osu.Game.Screens.Menu
t.Font = t.Font.With(Typeface.Torus, 20, FontWeight.SemiBold); t.Font = t.Font.With(Typeface.Torus, 20, FontWeight.SemiBold);
t.Colour = colours.Pink; t.Colour = colours.Pink;
}); });
expendableText.AddRange(textFlow.AddText(" coming to osu!", formatRegular)); expendableText.Add(textFlow.AddText(" coming to osu!", formatRegular));
textFlow.AddText(".", formatRegular); textFlow.AddText(".", formatRegular);
textFlow.NewParagraph(); textFlow.NewParagraph();
@ -152,7 +152,7 @@ namespace osu.Game.Screens.Menu
t.Font = t.Font.With(size: 20); t.Font = t.Font.With(size: 20);
t.Origin = Anchor.Centre; t.Origin = Anchor.Centre;
t.Colour = colours.Pink; t.Colour = colours.Pink;
}).First(); }).Drawables.First();
if (IsLoaded) if (IsLoaded)
animateHeart(); animateHeart();
@ -193,7 +193,7 @@ namespace osu.Game.Screens.Menu
using (BeginDelayedSequence(520 + 160)) using (BeginDelayedSequence(520 + 160))
{ {
fill.MoveToOffset(new Vector2(0, 15), 160, Easing.OutQuart); fill.MoveToOffset(new Vector2(0, 15), 160, Easing.OutQuart);
Schedule(() => expendableText.ForEach(t => Schedule(() => expendableText.SelectMany(t => t.Drawables).ForEach(t =>
{ {
t.FadeOut(100); t.FadeOut(100);
t.ScaleTo(new Vector2(0, 1), 100, Easing.OutQuart); t.ScaleTo(new Vector2(0, 1), 100, Easing.OutQuart);

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

View File

@ -36,7 +36,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Realm" Version="10.6.0" /> <PackageReference Include="Realm" Version="10.6.0" />
<PackageReference Include="ppy.osu.Framework" Version="2021.1026.0" /> <PackageReference Include="ppy.osu.Framework" Version="2021.1029.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.1026.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2021.1026.0" />
<PackageReference Include="Sentry" Version="3.9.4" /> <PackageReference Include="Sentry" Version="3.9.4" />
<PackageReference Include="SharpCompress" Version="0.29.0" /> <PackageReference Include="SharpCompress" Version="0.29.0" />

View File

@ -70,7 +70,7 @@
<Reference Include="System.Net.Http" /> <Reference Include="System.Net.Http" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="ppy.osu.Framework.iOS" Version="2021.1026.0" /> <PackageReference Include="ppy.osu.Framework.iOS" Version="2021.1029.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.1026.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2021.1026.0" />
</ItemGroup> </ItemGroup>
<!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) --> <!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) -->
@ -93,7 +93,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="ppy.osu.Framework" Version="2021.1026.0" /> <PackageReference Include="ppy.osu.Framework" Version="2021.1029.0" />
<PackageReference Include="SharpCompress" Version="0.28.3" /> <PackageReference Include="SharpCompress" Version="0.28.3" />
<PackageReference Include="NUnit" Version="3.13.2" /> <PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="SharpRaven" Version="2.4.0" /> <PackageReference Include="SharpRaven" Version="2.4.0" />