1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-11 17:07:38 +08:00

Merge branch 'master' into multi-queueing-modes

This commit is contained in:
smoogipoo 2021-11-01 18:37:16 +09:00
commit 65b920e4c1
76 changed files with 1167 additions and 754 deletions

View File

@ -52,7 +52,7 @@
</ItemGroup>
<ItemGroup>
<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 Label="Transitive Dependencies">
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->

View File

@ -8,6 +8,7 @@ using osu.Framework.Bindables;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Online.API;
using osu.Game.Online.Solo;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods;
@ -88,33 +89,27 @@ namespace osu.Game.Tests.Online
}
[Test]
public void TestDeserialiseScoreInfoWithEmptyMods()
public void TestDeserialiseSubmittableScoreWithEmptyMods()
{
var score = new ScoreInfo { Ruleset = new OsuRuleset().RulesetInfo };
var score = new SubmittableScore(new ScoreInfo { Ruleset = new OsuRuleset().RulesetInfo });
var deserialised = JsonConvert.DeserializeObject<ScoreInfo>(JsonConvert.SerializeObject(score));
if (deserialised != null)
deserialised.Ruleset = new OsuRuleset().RulesetInfo;
var deserialised = JsonConvert.DeserializeObject<SubmittableScore>(JsonConvert.SerializeObject(score));
Assert.That(deserialised?.Mods.Length, Is.Zero);
}
[Test]
public void TestDeserialiseScoreInfoWithCustomModSetting()
public void TestDeserialiseSubmittableScoreWithCustomModSetting()
{
var score = new ScoreInfo
var score = new SubmittableScore(new ScoreInfo
{
Ruleset = new OsuRuleset().RulesetInfo,
Mods = new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 2 } } }
};
});
var deserialised = JsonConvert.DeserializeObject<ScoreInfo>(JsonConvert.SerializeObject(score));
var deserialised = JsonConvert.DeserializeObject<SubmittableScore>(JsonConvert.SerializeObject(score));
if (deserialised != null)
deserialised.Ruleset = new OsuRuleset().RulesetInfo;
Assert.That(((OsuModDoubleTime)deserialised?.Mods[0])?.SpeedChange.Value, Is.EqualTo(2));
Assert.That((deserialised?.Mods[0])?.Settings["speed_change"], Is.EqualTo(2));
}
private class TestRuleset : Ruleset

View File

@ -128,7 +128,7 @@ namespace osu.Game.Tests.Online
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)

View File

@ -29,6 +29,15 @@ namespace osu.Game.Tests.Skins.IO
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]
public Task TestSingleImportMatchingFilename() => runSkinTest(async osu =>
{
@ -190,11 +199,11 @@ namespace osu.Game.Tests.Skins.IO
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();
using var zip = ZipArchive.Create();
zip.AddEntry("skin.ini", generateSkinIni(name, author, makeUnique));
zip.AddEntry(iniFilename, generateSkinIni(name, author, makeUnique));
zip.SaveTo(zipStream);
return zipStream;
}

View File

@ -13,6 +13,7 @@ using osu.Framework.Bindables;
using osu.Framework.Testing;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Ranking;
using osuTK.Input;
@ -132,11 +133,12 @@ namespace osu.Game.Tests.Visual.Gameplay
private ScoreInfo getScoreInfo(bool replayAvailable)
{
return new APILegacyScoreInfo
return new APIScoreInfo
{
OnlineScoreID = 2553163309,
OnlineID = 2553163309,
OnlineRulesetID = 0,
Replay = replayAvailable,
Beatmap = CreateAPIBeatmapSet(new OsuRuleset().RulesetInfo).Beatmaps.First(),
HasReplay = replayAvailable,
User = new User
{
Id = 39828,

View File

@ -43,11 +43,11 @@ namespace osu.Game.Tests.Visual.Online
}
};
var allScores = new APILegacyScores
var allScores = new APIScoresCollection
{
Scores = new List<APILegacyScoreInfo>
Scores = new List<APIScoreInfo>
{
new APILegacyScoreInfo
new APIScoreInfo
{
User = new User
{
@ -72,7 +72,7 @@ namespace osu.Game.Tests.Visual.Online
TotalScore = 1234567890,
Accuracy = 1,
},
new APILegacyScoreInfo
new APIScoreInfo
{
User = new User
{
@ -96,7 +96,7 @@ namespace osu.Game.Tests.Visual.Online
TotalScore = 1234789,
Accuracy = 0.9997,
},
new APILegacyScoreInfo
new APIScoreInfo
{
User = new User
{
@ -119,7 +119,7 @@ namespace osu.Game.Tests.Visual.Online
TotalScore = 12345678,
Accuracy = 0.9854,
},
new APILegacyScoreInfo
new APIScoreInfo
{
User = new User
{
@ -141,7 +141,7 @@ namespace osu.Game.Tests.Visual.Online
TotalScore = 1234567,
Accuracy = 0.8765,
},
new APILegacyScoreInfo
new APIScoreInfo
{
User = new User
{
@ -162,9 +162,9 @@ namespace osu.Game.Tests.Visual.Online
}
};
var myBestScore = new APILegacyUserTopScoreInfo
var myBestScore = new APIScoreWithPosition
{
Score = new APILegacyScoreInfo
Score = new APIScoreInfo
{
User = new User
{
@ -185,9 +185,9 @@ namespace osu.Game.Tests.Visual.Online
Position = 1337,
};
var myBestScoreWithNullPosition = new APILegacyUserTopScoreInfo
var myBestScoreWithNullPosition = new APIScoreWithPosition
{
Score = new APILegacyScoreInfo
Score = new APIScoreInfo
{
User = new User
{
@ -208,11 +208,11 @@ namespace osu.Game.Tests.Visual.Online
Position = null,
};
var oneScore = new APILegacyScores
var oneScore = new APIScoresCollection
{
Scores = new List<APILegacyScoreInfo>
Scores = new List<APIScoreInfo>
{
new APILegacyScoreInfo
new APIScoreInfo
{
User = new User
{
@ -273,7 +273,7 @@ namespace osu.Game.Tests.Visual.Online
private class TestScoresContainer : ScoresContainer
{
public new APILegacyScores Scores
public new APIScoresCollection Scores
{
set => base.Scores = value;
}

View File

@ -15,6 +15,7 @@ using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Carousel;
using osu.Game.Screens.Select.Filter;
@ -684,6 +685,7 @@ namespace osu.Game.Tests.Visual.SongSelect
set.Beatmaps.Add(new BeatmapInfo
{
Version = $"Stars: {i}",
Ruleset = new OsuRuleset().RulesetInfo,
StarDifficulty = i,
});
}
@ -868,6 +870,7 @@ namespace osu.Game.Tests.Visual.SongSelect
OnlineBeatmapID = id++ * 10,
Version = version,
StarDifficulty = diff,
Ruleset = new OsuRuleset().RulesetInfo,
BaseDifficulty = new BeatmapDifficulty
{
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)
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));
}
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]",
Artist = "This artist has a really long name how is this possible",
Author = new User
{
Username = "author",
Id = 100
}
Username = "author",
Id = 100
},
OnlineInfo = new APIBeatmapSet
Covers = new BeatmapSetOnlineCovers
{
Covers = new BeatmapSetOnlineCovers
{
Cover = "https://assets.ppy.sh/beatmaps/1189904/covers/cover.jpg?1595456608",
},
Ranked = DateTimeOffset.Now
}
Cover = "https://assets.ppy.sh/beatmaps/1189904/covers/cover.jpg?1595456608",
},
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]",
Artist = "This artist has a really long name how is this possible",
Author = new User
{
Username = "author",
Id = 100
}
Username = "author",
Id = 100
},
OnlineInfo = new APIBeatmapSet
Covers = new BeatmapSetOnlineCovers
{
Covers = new BeatmapSetOnlineCovers
{
Cover = "https://assets.ppy.sh/beatmaps/1189904/covers/cover.jpg?1595456608",
},
Ranked = DateTimeOffset.MinValue
}
Cover = "https://assets.ppy.sh/beatmaps/1189904/covers/cover.jpg?1595456608",
},
Ranked = DateTimeOffset.Now
}
};
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",
Artist = "Artist",
Author = new User
{
Username = "author",
Id = 100
}
Username = "author",
Id = 100
},
OnlineInfo = new APIBeatmapSet
Covers = new BeatmapSetOnlineCovers
{
Covers = new BeatmapSetOnlineCovers
{
Cover = "https://assets.ppy.sh/beatmaps/1079428/covers/cover.jpg?1595295586",
},
FavouriteCount = 100
}
Cover = "https://assets.ppy.sh/beatmaps/1189904/covers/cover.jpg?1595456608",
},
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 = "Title 2",
Artist = "Artist 2",
Author = new User
{
Username = "someone",
Id = 100
}
Username = "author",
Id = 100
},
OnlineInfo = new APIBeatmapSet
Covers = new BeatmapSetOnlineCovers
{
Covers = new BeatmapSetOnlineCovers
{
Cover = "https://assets.ppy.sh/beatmaps/1079428/covers/cover.jpg?1595295586",
},
FavouriteCount = 10
}
Cover = "https://assets.ppy.sh/beatmaps/1189904/covers/cover.jpg?1595456608",
},
Ranked = DateTimeOffset.Now
}
};
}

View File

@ -76,7 +76,7 @@ namespace osu.Game.Tournament.Components
{
new TournamentSpriteText
{
Text = Beatmap.GetDisplayTitleRomanisable(false),
Text = Beatmap.GetDisplayTitleRomanisable(false, false),
Font = OsuFont.Torus.With(weight: FontWeight.Bold),
},
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="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>
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);
@ -99,42 +99,45 @@ namespace osu.Game.Beatmaps
}
/// <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>
/// <remarks>
/// The bindable will not update to follow the currently-selected ruleset and mods or its settings.
/// </remarks>
/// <param name="beatmapInfo">The <see cref="BeatmapInfo"/> 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="beatmapInfo">The <see cref="IBeatmapInfo"/> to get the difficulty of.</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="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>
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)
=> createBindable(beatmapInfo, rulesetInfo, mods, cancellationToken);
/// <summary>
/// Retrieves the difficulty of a <see cref="BeatmapInfo"/>.
/// Retrieves the difficulty of a <see cref="IBeatmapInfo"/>.
/// </summary>
/// <param name="beatmapInfo">The <see cref="BeatmapInfo"/> to get the difficulty of.</param>
/// <param name="rulesetInfo">The <see cref="RulesetInfo"/> to get the difficulty with.</param>
/// <param name="beatmapInfo">The <see cref="IBeatmapInfo"/> to get the difficulty of.</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="cancellationToken">An optional <see cref="CancellationToken"/> which stops computing the star difficulty.</param>
/// <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)
{
// In the case that the user hasn't given us a ruleset, use the beatmap's default 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.
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).
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)
@ -227,12 +230,12 @@ namespace osu.Game.Beatmaps
/// <summary>
/// Creates a new <see cref="BindableStarDifficulty"/> and triggers an initial value update.
/// </summary>
/// <param name="beatmapInfo">The <see cref="BeatmapInfo"/> that star difficulty should correspond to.</param>
/// <param name="initialRulesetInfo">The initial <see cref="RulesetInfo"/> to get the difficulty with.</param>
/// <param name="beatmapInfo">The <see cref="IBeatmapInfo"/> that star difficulty should correspond to.</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="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>
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)
{
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.
/// </summary>
/// <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="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)
GetDifficultyAsync(bindable.BeatmapInfo, rulesetInfo, mods, cancellationToken)
.ContinueWith(t =>
@ -343,10 +346,10 @@ namespace osu.Game.Beatmaps
private class BindableStarDifficulty : Bindable<StarDifficulty?>
{
public readonly BeatmapInfo BeatmapInfo;
public readonly IBeatmapInfo BeatmapInfo;
public readonly CancellationToken CancellationToken;
public BindableStarDifficulty(BeatmapInfo beatmapInfo, CancellationToken cancellationToken)
public BindableStarDifficulty(IBeatmapInfo beatmapInfo, CancellationToken cancellationToken)
{
BeatmapInfo = beatmapInfo;
CancellationToken = cancellationToken;

View File

@ -16,9 +16,9 @@ namespace osu.Game.Beatmaps
/// <summary>
/// A user-presentable display title representing this beatmap, with localisation handling for potentially romanisable fields.
/// </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)
{

View File

@ -34,9 +34,9 @@ namespace osu.Game.Beatmaps
/// <summary>
/// A user-presentable display title representing this beatmap, with localisation handling for potentially romanisable fields.
/// </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 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) =>
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)
: base(beatmapModelManager, api, host)
{

View File

@ -37,10 +37,10 @@ namespace osu.Game.Beatmaps.Drawables
}
[NotNull]
private readonly BeatmapInfo beatmapInfo;
private readonly IBeatmapInfo beatmapInfo;
[CanBeNull]
private readonly RulesetInfo ruleset;
private readonly IRulesetInfo ruleset;
[CanBeNull]
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="mods">The mods to show the difficulty with.</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.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="shouldShowTooltip">Whether to display a tooltip when hovered.</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.shouldShowTooltip = shouldShowTooltip;
@ -84,6 +84,9 @@ namespace osu.Game.Beatmaps.Drawables
InternalChild = iconContainer = new Container { Size = new Vector2(20f) };
}
[Resolved]
private RulesetStore rulesets { get; set; }
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
@ -105,7 +108,7 @@ namespace osu.Game.Beatmaps.Drawables
Child = background = new Box
{
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
@ -114,18 +117,28 @@ namespace osu.Game.Beatmaps.Drawables
Origin = Anchor.Centre,
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)
Icon = (ruleset ?? beatmapInfo.Ruleset)?.CreateInstance()?.CreateIcon() ?? new SpriteIcon { Icon = FontAwesome.Regular.QuestionCircle }
Icon = getRulesetIcon()
},
};
if (performBackgroundDifficultyLookup)
iconContainer.Add(new DelayedLoadUnloadWrapper(() => new DifficultyRetriever(beatmapInfo, ruleset, mods) { StarDifficulty = { BindTarget = difficultyBindable } }, 0));
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));
}
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();
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>();
private readonly BeatmapInfo beatmapInfo;
private readonly RulesetInfo ruleset;
private readonly IBeatmapInfo beatmapInfo;
private readonly IRulesetInfo ruleset;
private readonly IReadOnlyList<Mod> mods;
private CancellationTokenSource difficultyCancellation;
@ -143,7 +156,7 @@ namespace osu.Game.Beatmaps.Drawables
[Resolved]
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.ruleset = ruleset;

View File

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

View File

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

View File

@ -11,7 +11,7 @@ namespace osu.Game.Beatmaps
/// <summary>
/// A single beatmap difficulty.
/// </summary>
public interface IBeatmapInfo : IHasOnlineID
public interface IBeatmapInfo : IHasOnlineID<int>
{
/// <summary>
/// The user-specified name given to this beatmap.

View File

@ -12,7 +12,7 @@ namespace osu.Game.Beatmaps
/// <summary>
/// A representation of a collection of beatmap difficulties, generally packaged as an ".osz" archive.
/// </summary>
public interface IBeatmapSetInfo : IHasOnlineID
public interface IBeatmapSetInfo : IHasOnlineID<int>
{
/// <summary>
/// The date when this beatmap was imported.

View File

@ -5,15 +5,15 @@
namespace osu.Game.Database
{
public interface IHasOnlineID
public interface IHasOnlineID<out T>
{
/// <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>
/// <remarks>
/// Generally we use -1 when specifying "missing" in code, but values of 0 are also considered missing as the online source
/// is generally a MySQL autoincrement value, which can never be 0.
/// </remarks>
int OnlineID { get; }
T OnlineID { get; }
}
}

View File

@ -30,7 +30,7 @@ namespace osu.Game.Database
private readonly IModelManager<TModel> modelManager;
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)
{
@ -74,7 +74,7 @@ namespace osu.Game.Database
if (!imported.Any())
downloadFailed.Value = new WeakReference<ArchiveDownloadRequest<TModel>>(request);
currentDownloads.Remove(request);
CurrentDownloads.Remove(request);
}, TaskCreationOptions.LongRunning);
};
@ -86,7 +86,7 @@ namespace osu.Game.Database
return true;
};
currentDownloads.Add(request);
CurrentDownloads.Add(request);
PostNotification?.Invoke(notification);
api.PerformAsync(request);
@ -96,7 +96,7 @@ namespace osu.Game.Database
void triggerFailure(Exception error)
{
currentDownloads.Remove(request);
CurrentDownloads.Remove(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;

View File

@ -6,9 +6,11 @@ using System.Linq;
using System.Threading;
using osu.Framework.Allocation;
using osu.Framework.Development;
using osu.Framework.Input.Bindings;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Framework.Statistics;
using osu.Game.Input.Bindings;
using osu.Game.Models;
using Realms;
@ -32,8 +34,9 @@ namespace osu.Game.Database
/// Version history:
/// 6 First tracked version (~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>
private const int schema_version = 7;
private const int schema_version = 8;
/// <summary>
/// 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)
{
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)
{
convertOnlineIDs<RealmBeatmap>();

View File

@ -9,6 +9,7 @@ using osu.Framework.Graphics.Sprites;
using System.Collections.Generic;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Localisation;
using osu.Framework.Platform;
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) =>
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)
=> 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)
=> 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)
{
var spriteText = new OsuSpriteText { Text = text };
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)
{
foreach (var t in text)
AddArbitraryDrawable(t);
createLink(text, new LinkDetails(action, linkArgument), tooltipText);
createLink(new TextPartManual(text), new LinkDetails(action, linkArgument), tooltipText);
}
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>());
linkCompiler.RelativeSizeAxes = Axes.Both;
linkCompiler.TooltipText = tooltipText;
linkCompiler.Action = () =>
Action onClickAction = () =>
{
if (action != null)
action();
@ -101,10 +97,41 @@ namespace osu.Game.Graphics.Containers
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.
// 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.
using System;
using System.Collections.Generic;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
@ -19,8 +19,8 @@ namespace osu.Game.Graphics.Containers
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.
using System.Collections.Generic;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics.Containers;
using osuTK.Graphics;
@ -10,7 +10,7 @@ namespace osu.Game.Graphics
{
public class ErrorTextFlowContainer : OsuTextFlowContainer
{
private readonly List<Drawable> errorDrawables = new List<Drawable>();
private readonly List<ITextPart> errorTextParts = new List<ITextPart>();
public ErrorTextFlowContainer()
: base(cp => cp.Font = cp.Font.With(size: 12))
@ -19,7 +19,8 @@ namespace osu.Game.Graphics
public void ClearErrors()
{
errorDrawables.ForEach(d => d.Expire());
foreach (var textPart in errorTextParts)
RemovePart(textPart);
}
public void AddErrors(string[] errors)
@ -29,7 +30,7 @@ namespace osu.Game.Graphics
if (errors == null) return;
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.Tilde, GlobalAction.QuickRetry),
new KeyBinding(new[] { InputKey.Control, InputKey.Tilde }, GlobalAction.QuickExit),
new KeyBinding(new[] { InputKey.Control, InputKey.Plus }, GlobalAction.IncreaseScrollSpeed),
new KeyBinding(new[] { InputKey.Control, InputKey.Minus }, GlobalAction.DecreaseScrollSpeed),
new KeyBinding(new[] { InputKey.F3 }, GlobalAction.DecreaseScrollSpeed),
new KeyBinding(new[] { InputKey.F4 }, GlobalAction.IncreaseScrollSpeed),
new KeyBinding(new[] { InputKey.Shift, InputKey.Tab }, GlobalAction.ToggleInGameInterface),
new KeyBinding(InputKey.MouseMiddle, GlobalAction.PauseGameplay),
new KeyBinding(InputKey.Space, GlobalAction.TogglePauseReplay),

View File

@ -119,6 +119,16 @@ namespace osu.Game.Localisation
/// </summary>
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}";
}
}

View File

@ -9,11 +9,10 @@ using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets.Mods;
using System.Text;
using System.Collections.Generic;
using System.Diagnostics;
namespace osu.Game.Online.API.Requests
{
public class GetScoresRequest : APIRequest<APILegacyScores>
public class GetScoresRequest : APIRequest<APIScoresCollection>
{
private readonly BeatmapInfo beatmapInfo;
private readonly BeatmapLeaderboardScope scope;
@ -32,27 +31,6 @@ namespace osu.Game.Online.API.Requests
this.scope = scope;
this.ruleset = ruleset ?? throw new ArgumentNullException(nameof(ruleset));
this.mods = mods ?? Array.Empty<IMod>();
Success += onSuccess;
}
private void onSuccess(APILegacyScores r)
{
Debug.Assert(ruleset.ID != null, "ruleset.ID != null");
foreach (APILegacyScoreInfo score in r.Scores)
{
score.BeatmapInfo = beatmapInfo;
score.OnlineRulesetID = ruleset.ID.Value;
}
var userScore = r.UserScore;
if (userScore != null)
{
userScore.Score.BeatmapInfo = beatmapInfo;
userScore.Score.OnlineRulesetID = ruleset.ID.Value;
}
}
protected override string Target => $@"beatmaps/{beatmapInfo.OnlineBeatmapID}/scores{createQueryParameters()}";

View File

@ -8,7 +8,7 @@ using osu.Game.Rulesets;
namespace osu.Game.Online.API.Requests
{
public class GetUserScoresRequest : PaginatedAPIRequest<List<APILegacyScoreInfo>>
public class GetUserScoresRequest : PaginatedAPIRequest<List<APIScoreInfo>>
{
private readonly long userId;
private readonly ScoreType type;

View File

@ -1,35 +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.Collections.Generic;
using Newtonsoft.Json;
using osu.Game.Rulesets;
using osu.Game.Scoring;
namespace osu.Game.Online.API.Requests.Responses
{
public class APILegacyScores
{
[JsonProperty(@"scores")]
public List<APILegacyScoreInfo> Scores;
[JsonProperty(@"userScore")]
public APILegacyUserTopScoreInfo UserScore;
}
public class APILegacyUserTopScoreInfo
{
[JsonProperty(@"position")]
public int? Position;
[JsonProperty(@"score")]
public APILegacyScoreInfo Score;
public ScoreInfo CreateScoreInfo(RulesetStore rulesets)
{
var score = Score.CreateScoreInfo(rulesets);
score.Position = Position;
return score;
}
}
}

View File

@ -15,9 +15,69 @@ using osu.Game.Users;
namespace osu.Game.Online.API.Requests.Responses
{
public class APILegacyScoreInfo
public class APIScoreInfo : IScoreInfo
{
public ScoreInfo CreateScoreInfo(RulesetStore rulesets)
[JsonProperty(@"score")]
public long TotalScore { get; set; }
[JsonProperty(@"max_combo")]
public int MaxCombo { get; set; }
[JsonProperty(@"user")]
public User User { get; set; }
[JsonProperty(@"id")]
public long OnlineID { get; set; }
[JsonProperty(@"replay")]
public bool HasReplay { get; set; }
[JsonProperty(@"created_at")]
public DateTimeOffset Date { get; set; }
[JsonProperty(@"beatmap")]
public APIBeatmap Beatmap { get; set; }
[JsonProperty("accuracy")]
public double Accuracy { get; set; }
[JsonProperty(@"pp")]
public double? PP { get; set; }
[JsonProperty(@"beatmapset")]
public APIBeatmapSet BeatmapSet
{
set
{
// in the deserialisation case we need to ferry this data across.
// the order of properties returned by the API guarantees that the beatmap is populated by this point.
if (!(Beatmap is APIBeatmap apiBeatmap))
throw new InvalidOperationException("Beatmap set metadata arrived before beatmap metadata in response");
apiBeatmap.BeatmapSet = value;
}
}
[JsonProperty("statistics")]
public Dictionary<string, int> Statistics { get; set; }
[JsonProperty(@"mode_int")]
public int OnlineRulesetID { get; set; }
[JsonProperty(@"mods")]
public string[] Mods { get; set; }
[JsonProperty("rank")]
[JsonConverter(typeof(StringEnumConverter))]
public ScoreRank Rank { get; set; }
/// <summary>
/// Create a <see cref="ScoreInfo"/> from an API score instance.
/// </summary>
/// <param name="rulesets">A ruleset store, used to populate a ruleset instance in the returned score.</param>
/// <param name="beatmap">An optional beatmap, copied into the returned score (for cases where the API does not populate the beatmap).</param>
/// <returns></returns>
public ScoreInfo CreateScoreInfo(RulesetStore rulesets, BeatmapInfo beatmap = null)
{
var ruleset = rulesets.GetRuleset(OnlineRulesetID);
@ -32,19 +92,22 @@ namespace osu.Game.Online.API.Requests.Responses
{
TotalScore = TotalScore,
MaxCombo = MaxCombo,
BeatmapInfo = Beatmap.ToBeatmapInfo(rulesets),
User = User,
Accuracy = Accuracy,
OnlineScoreID = OnlineScoreID,
OnlineScoreID = OnlineID,
Date = Date,
PP = PP,
BeatmapInfo = BeatmapInfo,
RulesetID = OnlineRulesetID,
Hash = Replay ? "online" : string.Empty, // todo: temporary?
Hash = HasReplay ? "online" : string.Empty, // todo: temporary?
Rank = Rank,
Ruleset = ruleset,
Mods = mods,
};
if (beatmap != null)
scoreInfo.BeatmapInfo = beatmap;
if (Statistics != null)
{
foreach (var kvp in Statistics)
@ -81,57 +144,8 @@ namespace osu.Game.Online.API.Requests.Responses
return scoreInfo;
}
[JsonProperty(@"score")]
public int TotalScore { get; set; }
public IRulesetInfo Ruleset => new RulesetInfo { ID = OnlineRulesetID };
[JsonProperty(@"max_combo")]
public int MaxCombo { get; set; }
[JsonProperty(@"user")]
public User User { get; set; }
[JsonProperty(@"id")]
public long OnlineScoreID { get; set; }
[JsonProperty(@"replay")]
public bool Replay { get; set; }
[JsonProperty(@"created_at")]
public DateTimeOffset Date { get; set; }
[JsonProperty(@"beatmap")]
public BeatmapInfo BeatmapInfo { get; set; }
[JsonProperty("accuracy")]
public double Accuracy { get; set; }
[JsonProperty(@"pp")]
public double? PP { get; set; }
[JsonProperty(@"beatmapset")]
public BeatmapMetadata Metadata
{
set
{
// extract the set ID to its correct place.
BeatmapInfo.BeatmapSet = new BeatmapSetInfo { OnlineBeatmapSetID = value.ID };
value.ID = 0;
BeatmapInfo.Metadata = value;
}
}
[JsonProperty(@"statistics")]
public Dictionary<string, int> Statistics { get; set; }
[JsonProperty(@"mode_int")]
public int OnlineRulesetID { get; set; }
[JsonProperty(@"mods")]
public string[] Mods { get; set; }
[JsonProperty("rank")]
[JsonConverter(typeof(StringEnumConverter))]
public ScoreRank Rank { get; set; }
IBeatmapInfo IScoreInfo.Beatmap => Beatmap;
}
}

View File

@ -0,0 +1,26 @@
// 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 Newtonsoft.Json;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osu.Game.Scoring;
namespace osu.Game.Online.API.Requests.Responses
{
public class APIScoreWithPosition
{
[JsonProperty(@"position")]
public int? Position;
[JsonProperty(@"score")]
public APIScoreInfo Score;
public ScoreInfo CreateScoreInfo(RulesetStore rulesets, BeatmapInfo beatmap = null)
{
var score = Score.CreateScoreInfo(rulesets, beatmap);
score.Position = Position;
return score;
}
}
}

View File

@ -0,0 +1,17 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using Newtonsoft.Json;
namespace osu.Game.Online.API.Requests.Responses
{
public class APIScoresCollection
{
[JsonProperty(@"scores")]
public List<APIScoreInfo> Scores;
[JsonProperty(@"userScore")]
public APIScoreWithPosition UserScore;
}
}

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 osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
@ -30,6 +32,11 @@ namespace osu.Game.Online.Chat
protected override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new LinkHoverSounds(sampleSet, Parts);
public DrawableLinkCompiler(ITextPart part)
: this(part.Drawables.OfType<SpriteText>())
{
}
public DrawableLinkCompiler(IEnumerable<Drawable> parts)
: 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.
using System;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Logging;
using osu.Framework.Threading;
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
/// databased beatmap set's checksum, to disallow from playing with an altered version of the beatmap.
/// </summary>
public class OnlinePlayBeatmapAvailabilityTracker : DownloadTrackingComposite<BeatmapSetInfo, BeatmapManager>
public sealed class OnlinePlayBeatmapAvailabilityTracker : CompositeDrawable
{
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>
/// The availability state of the currently selected playlist item.
/// </summary>
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 BeatmapDownloadTracker downloadTracker;
protected override void LoadComplete()
{
base.LoadComplete();
@ -40,58 +50,38 @@ namespace osu.Game.Online.Rooms
if (item.NewValue == null)
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);
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()
{
switch (State.Value)
if (downloadTracker == null)
return;
switch (downloadTracker.State.Value)
{
case DownloadState.NotDownloaded:
availability.Value = BeatmapAvailability.NotDownloaded();
break;
case DownloadState.Downloading:
availability.Value = BeatmapAvailability.Downloading((float)Progress.Value);
availability.Value = BeatmapAvailability.Downloading((float)downloadTracker.Progress.Value);
break;
case DownloadState.Importing:
@ -99,12 +89,27 @@ namespace osu.Game.Online.Rooms
break;
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;
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,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.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))
UpdateState(DownloadState.LocallyAvailable);
});
}
}
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

@ -16,13 +16,13 @@ namespace osu.Game.Online.Solo
private readonly int beatmapId;
private readonly ScoreInfo scoreInfo;
private readonly SubmittableScore score;
public SubmitSoloScoreRequest(int beatmapId, long scoreId, ScoreInfo scoreInfo)
{
this.beatmapId = beatmapId;
this.scoreId = scoreId;
this.scoreInfo = scoreInfo;
score = new SubmittableScore(scoreInfo);
}
protected override WebRequest CreateWebRequest()
@ -32,7 +32,7 @@ namespace osu.Game.Online.Solo
req.ContentType = "application/json";
req.Method = HttpMethod.Put;
req.AddRaw(JsonConvert.SerializeObject(scoreInfo, new JsonSerializerSettings
req.AddRaw(JsonConvert.SerializeObject(score, new JsonSerializerSettings
{
ReferenceLoopHandling = ReferenceLoopHandling.Ignore
}));

View File

@ -0,0 +1,75 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using JetBrains.Annotations;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Users;
namespace osu.Game.Online.Solo
{
/// <summary>
/// A class specifically for sending scores to the API during score submission.
/// This is used instead of <see cref="APIScoreInfo"/> due to marginally different serialisation naming requirements.
/// </summary>
[Serializable]
public class SubmittableScore
{
[JsonProperty("rank")]
[JsonConverter(typeof(StringEnumConverter))]
public ScoreRank Rank { get; set; }
[JsonProperty("total_score")]
public long TotalScore { get; set; }
[JsonProperty("accuracy")]
public double Accuracy { get; set; }
[JsonProperty(@"pp")]
public double? PP { get; set; }
[JsonProperty("max_combo")]
public int MaxCombo { get; set; }
[JsonProperty("ruleset_id")]
public int RulesetID { get; set; }
[JsonProperty("passed")]
public bool Passed { get; set; }
// Used for API serialisation/deserialisation.
[JsonProperty("mods")]
public APIMod[] Mods { get; set; }
[JsonProperty("user")]
public User User { get; set; }
[JsonProperty("statistics")]
public Dictionary<HitResult, int> Statistics { get; set; }
[UsedImplicitly]
public SubmittableScore()
{
}
public SubmittableScore(ScoreInfo score)
{
Rank = score.Rank;
TotalScore = score.TotalScore;
Accuracy = score.Accuracy;
PP = score.PP;
MaxCombo = score.MaxCombo;
RulesetID = score.RulesetID;
Passed = score.Passed;
Mods = score.APIMods;
User = score.User;
Statistics = score.Statistics;
}
}
}

View File

@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using osu.Framework.Allocation;
@ -37,7 +36,7 @@ namespace osu.Game.Overlays.AccountCreation
private IAPIProvider api { get; set; }
private ShakeContainer registerShake;
private IEnumerable<Drawable> characterCheckText;
private ITextPart characterCheckText;
private OsuTextBox[] textboxes;
private LoadingLayer loadingLayer;
@ -136,7 +135,7 @@ namespace osu.Game.Overlays.AccountCreation
characterCheckText = passwordDescription.AddText("8 characters long");
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)

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

View File

@ -4,6 +4,7 @@
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
@ -12,13 +13,22 @@ using osuTK.Graphics;
namespace osu.Game.Overlays.BeatmapListing.Panels
{
public class DownloadProgressBar : BeatmapDownloadTrackingComposite
public class DownloadProgressBar : CompositeDrawable
{
private readonly ProgressBar progressBar;
private readonly BeatmapDownloadTracker downloadTracker;
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)
{
Height = 0,
@ -34,9 +44,9 @@ namespace osu.Game.Overlays.BeatmapListing.Panels
{
progressBar.FillColour = colours.Blue;
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)
{

View File

@ -3,12 +3,14 @@
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
@ -21,8 +23,10 @@ using osuTK;
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 buttons_height = 45;
private const float buttons_spacing = 5;
@ -45,6 +49,8 @@ namespace osu.Game.Overlays.BeatmapSet
private readonly FillFlowContainer fadeContent;
private readonly LoadingSpinner loading;
private BeatmapDownloadTracker downloadTracker;
[Resolved]
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));
State.BindValueChanged(_ => updateDownloadButtons());
BeatmapSet.BindValueChanged(setInfo =>
{
Picker.BeatmapSet = rulesetSelector.BeatmapSet = author.BeatmapSet = beatmapAvailability.BeatmapSet = Details.BeatmapSet = setInfo.NewValue;
cover.OnlineInfo = setInfo.NewValue?.OnlineInfo;
downloadTracker?.RemoveAndDisposeImmediately();
if (setInfo.NewValue == null)
{
onlineStatusPill.FadeTo(0.5f, 500, Easing.OutQuint);
@ -241,6 +247,10 @@ namespace osu.Game.Overlays.BeatmapSet
}
else
{
downloadTracker = new BeatmapDownloadTracker(setInfo.NewValue);
downloadTracker.State.BindValueChanged(_ => updateDownloadButtons());
AddInternal(downloadTracker);
fadeContent.FadeIn(500, Easing.OutQuint);
loading.Hide();
@ -266,13 +276,13 @@ namespace osu.Game.Overlays.BeatmapSet
{
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();
return;
}
switch (State.Value)
switch (downloadTracker.State.Value)
{
case DownloadState.LocallyAvailable:
// temporary for UX until new design is implemented.

View File

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

View File

@ -52,7 +52,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
private CancellationTokenSource loadCancellationSource;
protected APILegacyScores Scores
protected APIScoresCollection Scores
{
set => Schedule(() =>
{
@ -66,7 +66,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
if (value?.Scores.Any() != true)
return;
scoreManager.OrderByTotalScoreAsync(value.Scores.Select(s => s.CreateScoreInfo(rulesets)).ToArray(), loadCancellationSource.Token)
scoreManager.OrderByTotalScoreAsync(value.Scores.Select(s => s.CreateScoreInfo(rulesets, Beatmap.Value)).ToArray(), loadCancellationSource.Token)
.ContinueWith(ordered => Schedule(() =>
{
if (loadCancellationSource.IsCancellationRequested)
@ -78,7 +78,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
scoreTable.Show();
var userScore = value.UserScore;
var userScoreInfo = userScore?.Score.CreateScoreInfo(rulesets);
var userScoreInfo = userScore?.Score.CreateScoreInfo(rulesets, Beatmap.Value);
topScoresContainer.Add(new DrawableTopScore(topScore));

View File

@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
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
{
public SupporterPromoLinkCompiler(IEnumerable<Drawable> parts)
: base(parts)
public SupporterPromoLinkCompiler(ITextPart part)
: base(part)
{
}

View File

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

View File

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

View File

@ -3,19 +3,19 @@
using System;
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Online.API.Requests.Responses;
namespace osu.Game.Overlays.Dashboard.Home
{
public class DashboardNewBeatmapPanel : DashboardBeatmapPanel
{
public DashboardNewBeatmapPanel(BeatmapSetInfo setInfo)
: base(setInfo)
public DashboardNewBeatmapPanel(APIBeatmapSet beatmapSet)
: 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
};

View File

@ -4,17 +4,17 @@
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.API.Requests.Responses;
using osuTK;
namespace osu.Game.Overlays.Dashboard.Home
{
public class DashboardPopularBeatmapPanel : DashboardBeatmapPanel
{
public DashboardPopularBeatmapPanel(BeatmapSetInfo setInfo)
: base(setInfo)
public DashboardPopularBeatmapPanel(APIBeatmapSet beatmapSet)
: base(beatmapSet)
{
}
@ -34,7 +34,7 @@ namespace osu.Game.Overlays.Dashboard.Home
new OsuSpriteText
{
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.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.API.Requests.Responses;
using osuTK;
namespace osu.Game.Overlays.Dashboard.Home
{
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]
@ -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 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.
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Online.API.Requests.Responses;
namespace osu.Game.Overlays.Dashboard.Home
{
public class DrawableNewBeatmapList : DrawableBeatmapList
{
public DrawableNewBeatmapList(List<BeatmapSetInfo> beatmaps)
: base(beatmaps)
public DrawableNewBeatmapList(List<APIBeatmapSet> beatmapSets)
: 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";
}

View File

@ -2,18 +2,18 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Online.API.Requests.Responses;
namespace osu.Game.Overlays.Dashboard.Home
{
public class DrawablePopularBeatmapList : DrawableBeatmapList
{
public DrawablePopularBeatmapList(List<BeatmapSetInfo> beatmaps)
: base(beatmaps)
public DrawablePopularBeatmapList(List<APIBeatmapSet> beatmapSets)
: 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";
}

View File

@ -3,12 +3,10 @@
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
@ -25,7 +23,7 @@ namespace osu.Game.Overlays.Music
public Action<BeatmapSetInfo> RequestSelection;
private TextFlowContainer text;
private IEnumerable<Drawable> titleSprites;
private ITextPart titlePart;
private ILocalisedBindableString title;
private ILocalisedBindableString artist;
@ -63,11 +61,16 @@ namespace osu.Game.Overlays.Music
if (set.OldValue?.Equals(Model) != true && set.NewValue?.Equals(Model) != true)
return;
foreach (Drawable s in titleSprites)
s.FadeColour(set.NewValue.Equals(Model) ? selectedColour : Color4.White, FADE_DURATION);
updateSelectionState(false);
}, 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
{
RelativeSizeAxes = Axes.X,
@ -79,7 +82,8 @@ namespace osu.Game.Overlays.Music
text.Clear();
// 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 =>
{

View File

@ -15,7 +15,7 @@ using osu.Framework.Localisation;
namespace osu.Game.Overlays.Profile.Sections.Ranks
{
public class PaginatedScoreContainer : PaginatedProfileSubsection<APILegacyScoreInfo>
public class PaginatedScoreContainer : PaginatedProfileSubsection<APIScoreInfo>
{
private readonly ScoreType type;
@ -51,7 +51,7 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks
}
}
protected override void OnItemsReceived(List<APILegacyScoreInfo> items)
protected override void OnItemsReceived(List<APIScoreInfo> items)
{
if (VisiblePages == 0)
drawableItemIndex = 0;
@ -59,12 +59,12 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks
base.OnItemsReceived(items);
}
protected override APIRequest<List<APILegacyScoreInfo>> CreateRequest() =>
protected override APIRequest<List<APIScoreInfo>> CreateRequest() =>
new GetUserScoresRequest(User.Value.Id, type, VisiblePages++, ItemsPerPage);
private int drawableItemIndex;
protected override Drawable CreateDrawableItem(APILegacyScoreInfo model)
protected override Drawable CreateDrawableItem(APIScoreInfo model)
{
switch (type)
{

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 RendererSettings(),
new VideoSettings(),
new ScreenshotSettings(),
};
}

View File

@ -11,7 +11,7 @@ namespace osu.Game.Rulesets
/// <summary>
/// A representation of a ruleset's metadata.
/// </summary>
public interface IRulesetInfo : IHasOnlineID
public interface IRulesetInfo : IHasOnlineID<int>
{
/// <summary>
/// The user-exposed name of this ruleset.

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 System;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Rulesets;
using osu.Game.Users;
namespace osu.Game.Scoring
{
public interface IScoreInfo : IHasOnlineID<long>
{
User User { get; }
long TotalScore { get; }
int MaxCombo { get; }
double Accuracy { get; }
bool HasReplay { get; }
DateTimeOffset Date { get; }
double? PP { get; }
IBeatmapInfo Beatmap { get; }
IRulesetInfo Ruleset { get; }
ScoreRank Rank { get; }
// 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.
// Statistics is also missing. This can be reconsidered once changes in serialisation have been completed.
}
}

View File

@ -6,7 +6,6 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Database;
@ -19,47 +18,36 @@ using osu.Game.Utils;
namespace osu.Game.Scoring
{
public class ScoreInfo : IHasFiles<ScoreFileInfo>, IHasPrimaryKey, ISoftDelete, IEquatable<ScoreInfo>, IDeepCloneable<ScoreInfo>
public class ScoreInfo : IScoreInfo, IHasFiles<ScoreFileInfo>, IHasPrimaryKey, ISoftDelete, IEquatable<ScoreInfo>, IDeepCloneable<ScoreInfo>
{
public int ID { get; set; }
[JsonProperty("rank")]
[JsonConverter(typeof(StringEnumConverter))]
public ScoreRank Rank { get; set; }
[JsonProperty("total_score")]
public long TotalScore { get; set; }
[JsonProperty("accuracy")]
[Column(TypeName = "DECIMAL(1,4)")] // TODO: This data type is wrong (should contain more precision). But at the same time, we probably don't need to be storing this in the database.
public double Accuracy { get; set; }
[JsonIgnore]
public LocalisableString DisplayAccuracy => Accuracy.FormatAccuracy();
[JsonProperty(@"pp")]
public double? PP { get; set; }
[JsonProperty("max_combo")]
public int MaxCombo { get; set; }
[JsonIgnore]
public int Combo { get; set; } // Todo: Shouldn't exist in here
[JsonProperty("ruleset_id")]
public int RulesetID { get; set; }
[JsonProperty("passed")]
[NotMapped]
public bool Passed { get; set; } = true;
[JsonIgnore]
public virtual RulesetInfo Ruleset { get; set; }
public RulesetInfo Ruleset { get; set; }
private APIMod[] localAPIMods;
private Mod[] mods;
[JsonIgnore]
[NotMapped]
public Mod[] Mods
{
@ -74,7 +62,7 @@ namespace osu.Game.Scoring
if (mods != null)
scoreMods = mods;
else if (localAPIMods != null)
scoreMods = apiMods.Select(m => m.ToMod(rulesetInstance)).ToArray();
scoreMods = APIMods.Select(m => m.ToMod(rulesetInstance)).ToArray();
return scoreMods;
}
@ -86,9 +74,8 @@ namespace osu.Game.Scoring
}
// Used for API serialisation/deserialisation.
[JsonProperty("mods")]
[NotMapped]
private APIMod[] apiMods
public APIMod[] APIMods
{
get
{
@ -110,19 +97,16 @@ namespace osu.Game.Scoring
}
// Used for database serialisation/deserialisation.
[JsonIgnore]
[Column("Mods")]
public string ModsJson
{
get => JsonConvert.SerializeObject(apiMods);
set => apiMods = JsonConvert.DeserializeObject<APIMod[]>(value);
get => JsonConvert.SerializeObject(APIMods);
set => APIMods = JsonConvert.DeserializeObject<APIMod[]>(value);
}
[NotMapped]
[JsonProperty("user")]
public User User { get; set; }
[JsonIgnore]
[Column("User")]
public string UserString
{
@ -134,7 +118,6 @@ namespace osu.Game.Scoring
}
}
[JsonIgnore]
[Column("UserID")]
public int? UserID
{
@ -146,23 +129,18 @@ namespace osu.Game.Scoring
}
}
[JsonIgnore]
public int BeatmapInfoID { get; set; }
[JsonIgnore]
[Column("Beatmap")]
public virtual BeatmapInfo BeatmapInfo { get; set; }
public BeatmapInfo BeatmapInfo { get; set; }
[JsonIgnore]
public long? OnlineScoreID { get; set; }
[JsonIgnore]
public DateTimeOffset Date { get; set; }
[JsonProperty("statistics")]
public Dictionary<HitResult, int> Statistics = new Dictionary<HitResult, int>();
[NotMapped]
public Dictionary<HitResult, int> Statistics { get; set; } = new Dictionary<HitResult, int>();
[JsonIgnore]
[Column("Statistics")]
public string StatisticsJson
{
@ -180,29 +158,23 @@ namespace osu.Game.Scoring
}
[NotMapped]
[JsonIgnore]
public List<HitEvent> HitEvents { get; set; }
[JsonIgnore]
public List<ScoreFileInfo> Files { get; set; }
[JsonIgnore]
public string Hash { get; set; }
[JsonIgnore]
public bool DeletePending { get; set; }
/// <summary>
/// The position of this score, starting at 1.
/// </summary>
[NotMapped]
[JsonProperty("position")]
public int? Position { get; set; }
public int? Position { get; set; } // TODO: remove after all calls to `CreateScoreInfo` are gone.
/// <summary>
/// Whether this <see cref="ScoreInfo"/> represents a legacy (osu!stable) score.
/// </summary>
[JsonIgnore]
[NotMapped]
public bool IsLegacyScore => Mods.OfType<ModClassic>().Any();
@ -271,5 +243,19 @@ namespace osu.Game.Scoring
return ReferenceEquals(this, other);
}
#region Implementation of IHasOnlineID
public long OnlineID => OnlineScoreID ?? -1;
#endregion
#region Implementation of IScoreInfo
IBeatmapInfo IScoreInfo.Beatmap => BeatmapInfo;
IRulesetInfo IScoreInfo.Ruleset => Ruleset;
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);
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 FillFlowContainer fill;
private readonly List<Drawable> expendableText = new List<Drawable>();
private readonly List<ITextPart> expendableText = new List<ITextPart>();
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));
expendableText.AddRange(textFlow.AddText("lazer", t =>
expendableText.Add(textFlow.AddText("lazer", t =>
{
t.Font = t.Font.With(Typeface.Torus, 30, FontWeight.Regular);
t.Colour = colours.PinkLight;
@ -114,7 +114,7 @@ namespace osu.Game.Screens.Menu
t.Font = t.Font.With(Typeface.Torus, 20, FontWeight.SemiBold);
t.Colour = colours.Pink;
});
expendableText.AddRange(textFlow.AddText(" coming to osu!", formatRegular));
expendableText.Add(textFlow.AddText(" coming to osu!", formatRegular));
textFlow.AddText(".", formatRegular);
textFlow.NewParagraph();
@ -152,7 +152,7 @@ namespace osu.Game.Screens.Menu
t.Font = t.Font.With(size: 20);
t.Origin = Anchor.Centre;
t.Colour = colours.Pink;
}).First();
}).Drawables.First();
if (IsLoaded)
animateHeart();
@ -193,7 +193,7 @@ namespace osu.Game.Screens.Menu
using (BeginDelayedSequence(520 + 160))
{
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.ScaleTo(new Vector2(0, 1), 100, Easing.OutQuart);

View File

@ -253,10 +253,7 @@ namespace osu.Game.Screens.OnlinePlay
protected virtual IEnumerable<Drawable> CreateButtons() =>
new Drawable[]
{
new PlaylistDownloadButton(Item)
{
Size = new Vector2(50, 30)
},
new PlaylistDownloadButton(Item),
new PlaylistRemoveButton
{
Size = new Vector2(30, 30),
@ -287,28 +284,33 @@ namespace osu.Game.Screens.OnlinePlay
return true;
}
private class PlaylistDownloadButton : BeatmapPanelDownloadButton
private sealed class PlaylistDownloadButton : BeatmapPanelDownloadButton
{
private readonly PlaylistItem playlistItem;
[Resolved]
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)
: base(playlistItem.Beatmap.Value.BeatmapSet)
{
this.playlistItem = playlistItem;
Size = new Vector2(width, 30);
Alpha = 0;
}
protected override void LoadComplete()
{
base.LoadComplete();
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)
@ -320,12 +322,16 @@ namespace osu.Game.Screens.OnlinePlay
if (beatmapManager.QueryBeatmap(b => b.MD5Hash == playlistItem.Beatmap.Value.MD5Hash) == null)
State.Value = DownloadState.NotDownloaded;
else
this.FadeTo(0, 500);
{
this.FadeTo(0, 500)
.ResizeWidthTo(0, 500, Easing.OutQuint);
}
break;
default:
this.FadeTo(1, 500);
this.ResizeWidthTo(width, 500, Easing.OutQuint)
.FadeTo(1, 500);
break;
}
}

View File

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

View File

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

View File

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

View File

@ -31,7 +31,7 @@ namespace osu.Game.Screens.Ranking
return null;
getScoreRequest = new GetScoresRequest(Score.BeatmapInfo, Score.Ruleset);
getScoreRequest.Success += r => scoresCallback?.Invoke(r.Scores.Where(s => s.OnlineScoreID != Score.OnlineScoreID).Select(s => s.CreateScoreInfo(rulesets)));
getScoreRequest.Success += r => scoresCallback?.Invoke(r.Scores.Where(s => s.OnlineID != Score.OnlineScoreID).Select(s => s.CreateScoreInfo(rulesets, Beatmap.Value.BeatmapInfo)));
return getScoreRequest;
}

View File

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

View File

@ -192,14 +192,14 @@ namespace osu.Game.Screens.Select.Leaderboards
req.Success += r =>
{
scoreManager.OrderByTotalScoreAsync(r.Scores.Select(s => s.CreateScoreInfo(rulesets)).ToArray(), loadCancellationSource.Token)
scoreManager.OrderByTotalScoreAsync(r.Scores.Select(s => s.CreateScoreInfo(rulesets, BeatmapInfo)).ToArray(), loadCancellationSource.Token)
.ContinueWith(ordered => Schedule(() =>
{
if (loadCancellationSource.IsCancellationRequested)
return;
scoresCallback?.Invoke(ordered.Result);
TopScore = r.UserScore?.CreateScoreInfo(rulesets);
TopScore = r.UserScore?.CreateScoreInfo(rulesets, BeatmapInfo);
}), TaskContinuationOptions.OnlyOnRanToCompletion);
};

View File

@ -93,7 +93,7 @@ namespace osu.Game.Skinning
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))
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>
/// 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);
}
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;
@ -164,7 +164,7 @@ namespace osu.Game.Skinning
// `Skin` will parse the skin.ini and populate `Skin.Configuration` during construction above.
string skinIniSourcedName = instance.Configuration.SkinInfo.Name;
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;
@ -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 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)
item.Name = $"{item.Name} [{archiveName}]";
item.Name = @$"{item.Name} [{archiveName}]";
}
// By this point, the metadata in SkinInfo will be correct.
@ -191,10 +191,10 @@ namespace osu.Game.Skinning
private void updateSkinIniMetadata(SkinInfo item)
{
string nameLine = $"Name: {item.Name}";
string authorLine = $"Author: {item.Creator}";
string nameLine = @$"Name: {item.Name}";
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)
{
@ -210,12 +210,12 @@ namespace osu.Game.Skinning
while ((line = sr.ReadLine()) != null)
{
if (line.StartsWith("Name:", StringComparison.Ordinal))
if (line.StartsWith(@"Name:", StringComparison.Ordinal))
{
outputLines.Add(nameLine);
addedName = true;
}
else if (line.StartsWith("Author:", StringComparison.Ordinal))
else if (line.StartsWith(@"Author:", StringComparison.Ordinal))
{
outputLines.Add(authorLine);
addedAuthor = true;
@ -229,7 +229,7 @@ namespace osu.Game.Skinning
{
outputLines.AddRange(new[]
{
"[General]",
@"[General]",
nameLine,
authorLine,
});
@ -252,13 +252,13 @@ namespace osu.Game.Skinning
{
using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true))
{
sw.WriteLine("[General]");
sw.WriteLine(@"[General]");
sw.WriteLine(nameLine);
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.
CurrentSkinInfo.Value = Import(new SkinInfo
{
Name = skin.SkinInfo.Name + " (modified)",
Name = skin.SkinInfo.Name + @" (modified)",
Creator = skin.SkinInfo.Creator,
InstantiationInfo = skin.SkinInfo.InstantiationInfo,
}).Result.Value;
@ -312,7 +312,7 @@ namespace osu.Game.Skinning
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);

View File

@ -36,7 +36,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<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="Sentry" Version="3.9.4" />
<PackageReference Include="SharpCompress" Version="0.29.0" />

View File

@ -70,7 +70,7 @@
<Reference Include="System.Net.Http" />
</ItemGroup>
<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" />
</ItemGroup>
<!-- 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.Core" Version="2.2.6" />
<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="NUnit" Version="3.13.2" />
<PackageReference Include="SharpRaven" Version="2.4.0" />