diff --git a/osu.Game/Online/API/Requests/DownloadReplayRequest.cs b/osu.Game/Online/API/Requests/DownloadReplayRequest.cs new file mode 100644 index 0000000000..11747f4f5f --- /dev/null +++ b/osu.Game/Online/API/Requests/DownloadReplayRequest.cs @@ -0,0 +1,13 @@ +using osu.Game.Scoring; +namespace osu.Game.Online.API.Requests +{ + public class DownloadReplayRequest : ArchiveDownloadModelRequest + { + public DownloadReplayRequest(ScoreInfo score) + : base(score) + { + } + + protected override string Target => $@"scores/{Info.Ruleset.ShortName}/{Info.OnlineScoreID}/download"; + } +} diff --git a/osu.Game/Online/API/Requests/Responses/APILegacyScoreInfo.cs b/osu.Game/Online/API/Requests/Responses/APILegacyScoreInfo.cs index 3060300077..5a18cf63f9 100644 --- a/osu.Game/Online/API/Requests/Responses/APILegacyScoreInfo.cs +++ b/osu.Game/Online/API/Requests/Responses/APILegacyScoreInfo.cs @@ -32,12 +32,15 @@ namespace osu.Game.Online.API.Requests.Responses set => User = value; } - [JsonProperty(@"score_id")] + [JsonProperty(@"id")] private long onlineScoreID { set => OnlineScoreID = value; } + [JsonProperty(@"replay")] + public bool Replay { get; set; } + [JsonProperty(@"created_at")] private DateTimeOffset date { diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 3bdf37d769..f589ba8a5a 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -170,7 +170,7 @@ namespace osu.Game dependencies.Cache(FileStore = new FileStore(contextFactory, Host.Storage)); // ordering is important here to ensure foreign keys rules are not broken in ModelStore.Cleanup() - dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Host.Storage, contextFactory, Host)); + dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Host.Storage, API, contextFactory, Host)); dependencies.Cache(BeatmapManager = new BeatmapManager(Host.Storage, contextFactory, RulesetStore, API, Audio, Host, defaultBeatmap)); // this should likely be moved to ArchiveModelManager when another case appers where it is necessary diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index 8bdc30ac94..266725a739 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -16,7 +16,7 @@ using osu.Game.Rulesets.Scoring; namespace osu.Game.Scoring { - public class ScoreInfo : IHasFiles, IHasPrimaryKey, ISoftDelete + public class ScoreInfo : IHasFiles, IHasPrimaryKey, ISoftDelete, IEquatable { public int ID { get; set; } @@ -182,5 +182,7 @@ namespace osu.Game.Scoring } public override string ToString() => $"{User} playing {Beatmap}"; + + public bool Equals(ScoreInfo other) => other?.OnlineScoreID == OnlineScoreID; } } diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index 6b737dc734..6d2ade5ecd 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -11,12 +11,14 @@ using osu.Framework.Platform; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.IO.Archives; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; using osu.Game.Rulesets; using osu.Game.Scoring.Legacy; namespace osu.Game.Scoring { - public class ScoreManager : ArchiveModelManager + public class ScoreManager : ArchiveDownloadModelManager { public override string[] HandledExtensions => new[] { ".osr" }; @@ -27,8 +29,8 @@ namespace osu.Game.Scoring private readonly RulesetStore rulesets; private readonly Func beatmaps; - public ScoreManager(RulesetStore rulesets, Func beatmaps, Storage storage, IDatabaseContextFactory contextFactory, IIpcHost importHost = null) - : base(storage, contextFactory, new ScoreStore(contextFactory, storage), importHost) + public ScoreManager(RulesetStore rulesets, Func beatmaps, Storage storage, IAPIProvider api, IDatabaseContextFactory contextFactory, IIpcHost importHost = null) + : base(storage, contextFactory, api, new ScoreStore(contextFactory, storage), importHost) { this.rulesets = rulesets; this.beatmaps = beatmaps; @@ -60,5 +62,7 @@ namespace osu.Game.Scoring public IEnumerable QueryScores(Expression> query) => ModelStore.ConsumableItems.AsNoTracking().Where(query); public ScoreInfo Query(Expression> query) => ModelStore.ConsumableItems.AsNoTracking().FirstOrDefault(query); + + protected override ArchiveDownloadModelRequest CreateDownloadRequest(ScoreInfo score, object[] options) => new DownloadReplayRequest(score); } } diff --git a/osu.Game/Screens/Play/ReplayDownloadButton.cs b/osu.Game/Screens/Play/ReplayDownloadButton.cs new file mode 100644 index 0000000000..14a6f942eb --- /dev/null +++ b/osu.Game/Screens/Play/ReplayDownloadButton.cs @@ -0,0 +1,53 @@ +using osu.Framework.Allocation; +using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online; +using osu.Game.Scoring; + +namespace osu.Game.Screens.Play +{ + public class ReplayDownloadButton : DownloadTrackingComposite + { + [Resolved] + private OsuGame game { get; set; } + + [Resolved] + private ScoreManager scores { get; set; } + + public ReplayDownloadButton(ScoreInfo score) + : base(score) + { + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + AddInternal(new TwoLayerButton + { + BackgroundColour = colours.Yellow, + Icon = FontAwesome.Solid.PlayCircle, + Text = @"Replay", + HoverColour = colours.YellowDark, + Action = onReplay, + }); + } + + private void onReplay() + { + if (scores.IsAvailableLocally(ModelInfo.Value)) + { + game.PresentScore(ModelInfo.Value); + return; + } + + scores.Download(ModelInfo.Value); + + scores.ItemAdded += (score, _) => + { + if (score.Equals(ModelInfo.Value)) + game.PresentScore(ModelInfo.Value); + }; + } + } +} diff --git a/osu.Game/Screens/Play/SoloResults.cs b/osu.Game/Screens/Play/SoloResults.cs index 2b9aec257c..5c747d2d31 100644 --- a/osu.Game/Screens/Play/SoloResults.cs +++ b/osu.Game/Screens/Play/SoloResults.cs @@ -2,6 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using osu.Framework.Allocation; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Scoring; using osu.Game.Screens.Ranking; using osu.Game.Screens.Ranking.Types; @@ -10,11 +12,31 @@ namespace osu.Game.Screens.Play { public class SoloResults : Results { + [Resolved] + ScoreManager scores { get; set; } + public SoloResults(ScoreInfo score) : base(score) { } + [BackgroundDependencyLoader] + private void load() + { + if (scores.IsAvailableLocally(Score) || hasOnlineReplay) + { + AddInternal(new ReplayDownloadButton(Score) + { + Anchor = Framework.Graphics.Anchor.BottomRight, + Origin = Framework.Graphics.Anchor.BottomRight, + Height = 80, + Width = 100, + }); + } + } + + private bool hasOnlineReplay => Score is APILegacyScoreInfo apiScore && apiScore.OnlineScoreID != null && apiScore.Replay; + protected override IEnumerable CreateResultPages() => new IResultPageInfo[] { new ScoreOverviewPageInfo(Score, Beatmap.Value),