mirror of
https://github.com/ppy/osu.git
synced 2026-05-22 21:00:58 +08:00
8cb81974eb
The way that this works is that it plugs into the online request to retrieve the beatmap set that the client is already performing, and stores user tag data to the local realm database. This means that for now user tags will only populate for beatmaps that the user has displayed on song select which is obviously subpar. I plan to follow this change up by adding user tag state dumps to `online.db` and using that data for initial tag population to make the majority case (ranked beatmaps) work. Note that several decisions were made here that are potential discussion points: - `RealmPopulatingOnlineLookupSource` is set up such that it can be the middle man / redirection point for similar flows that we need and we are currently missing, such as storing guest difficulty information, or storing the user's current best score on a beatmap (handy for rank achieved sorting / filtering / etc.) - The user tags are stored in `BeatmapMetadata` which breaks the longstanding assumption that you can arbitrarily pull out a metadata instance from any of the beatmaps in a set and get essentially the same object back. I've attempted to constrain this some by not adding user tags to the `IBeatmapMetadataInfo` interface through which `BeatmapSetInfo` exposes metadata further, but I warn in advance that this is a temporary state of affairs and I will make it worse in the future when `BeatmapMetadata.Author` becomes `Authors` plural in order to support guest mapper display (and direct guest difficulty submission). - The syntax for searching via user tags is chosen to mostly match web - it's `tag=`, with support for all of the string matching modes song select already has (bare word for substring, `""` quotes for phrase isolated by whitespace, `""!` for exact full match).
409 lines
18 KiB
C#
409 lines
18 KiB
C#
// 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.Linq;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using osu.Framework.Allocation;
|
|
using osu.Framework.Bindables;
|
|
using osu.Framework.Extensions;
|
|
using osu.Framework.Graphics;
|
|
using osu.Framework.Graphics.Containers;
|
|
using osu.Framework.Logging;
|
|
using osu.Game.Beatmaps;
|
|
using osu.Game.Database;
|
|
using osu.Game.Graphics.Containers;
|
|
using osu.Game.Localisation;
|
|
using osu.Game.Online;
|
|
using osu.Game.Online.API;
|
|
using osu.Game.Online.API.Requests.Responses;
|
|
using osu.Game.Online.Chat;
|
|
using osu.Game.Resources.Localisation.Web;
|
|
using osuTK;
|
|
|
|
namespace osu.Game.Screens.SelectV2
|
|
{
|
|
public partial class BeatmapMetadataWedge : VisibilityContainer
|
|
{
|
|
private MetadataDisplay creator = null!;
|
|
private MetadataDisplay source = null!;
|
|
private MetadataDisplay genre = null!;
|
|
private MetadataDisplay language = null!;
|
|
private MetadataDisplay userTags = null!;
|
|
private MetadataDisplay mapperTags = null!;
|
|
private MetadataDisplay submitted = null!;
|
|
private MetadataDisplay ranked = null!;
|
|
|
|
private Drawable ratingsWedge = null!;
|
|
private SuccessRateDisplay successRateDisplay = null!;
|
|
private UserRatingDisplay userRatingDisplay = null!;
|
|
private RatingSpreadDisplay ratingSpreadDisplay = null!;
|
|
|
|
private Drawable failRetryWedge = null!;
|
|
private FailRetryDisplay failRetryDisplay = null!;
|
|
|
|
public bool RatingsVisible => ratingsWedge.Alpha > 0;
|
|
public bool FailRetryVisible => failRetryWedge.Alpha > 0;
|
|
|
|
protected override bool StartHidden => true;
|
|
|
|
[Resolved]
|
|
private IBindable<WorkingBeatmap> beatmap { get; set; } = null!;
|
|
|
|
[Resolved]
|
|
private IAPIProvider api { get; set; } = null!;
|
|
|
|
[Resolved]
|
|
private RealmPopulatingOnlineLookupSource onlineLookupSource { get; set; } = null!;
|
|
|
|
[Resolved]
|
|
private RealmAccess realm { get; set; } = null!;
|
|
|
|
private IBindable<APIState> apiState = null!;
|
|
|
|
[Resolved]
|
|
private ILinkHandler? linkHandler { get; set; }
|
|
|
|
[Resolved]
|
|
private ISongSelect? songSelect { get; set; }
|
|
|
|
[BackgroundDependencyLoader]
|
|
private void load()
|
|
{
|
|
RelativeSizeAxes = Axes.X;
|
|
AutoSizeAxes = Axes.Y;
|
|
Padding = new MarginPadding { Top = 4f };
|
|
|
|
Width = 0.9f;
|
|
|
|
InternalChild = new FillFlowContainer
|
|
{
|
|
RelativeSizeAxes = Axes.X,
|
|
AutoSizeAxes = Axes.Y,
|
|
Direction = FillDirection.Vertical,
|
|
Spacing = new Vector2(0f, 4f),
|
|
Shear = OsuGame.SHEAR,
|
|
Children = new[]
|
|
{
|
|
new ShearAligningWrapper(new Container
|
|
{
|
|
CornerRadius = 10,
|
|
Masking = true,
|
|
RelativeSizeAxes = Axes.X,
|
|
AutoSizeAxes = Axes.Y,
|
|
Children = new Drawable[]
|
|
{
|
|
new WedgeBackground(),
|
|
new Container
|
|
{
|
|
RelativeSizeAxes = Axes.X,
|
|
AutoSizeAxes = Axes.Y,
|
|
Shear = -OsuGame.SHEAR,
|
|
Padding = new MarginPadding { Left = SongSelect.WEDGE_CONTENT_MARGIN, Right = 35, Vertical = 16 },
|
|
Children = new Drawable[]
|
|
{
|
|
new FillFlowContainer
|
|
{
|
|
RelativeSizeAxes = Axes.X,
|
|
AutoSizeAxes = Axes.Y,
|
|
Direction = FillDirection.Vertical,
|
|
Spacing = new Vector2(0f, 10f),
|
|
AutoSizeDuration = (float)transition_duration / 3,
|
|
AutoSizeEasing = Easing.OutQuint,
|
|
Children = new Drawable[]
|
|
{
|
|
new GridContainer
|
|
{
|
|
RelativeSizeAxes = Axes.X,
|
|
AutoSizeAxes = Axes.Y,
|
|
RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) },
|
|
ColumnDimensions = new[]
|
|
{
|
|
new Dimension(),
|
|
new Dimension(),
|
|
new Dimension(),
|
|
},
|
|
Content = new[]
|
|
{
|
|
new[]
|
|
{
|
|
new FillFlowContainer
|
|
{
|
|
RelativeSizeAxes = Axes.X,
|
|
AutoSizeAxes = Axes.Y,
|
|
Direction = FillDirection.Vertical,
|
|
Spacing = new Vector2(0f, 10f),
|
|
Children = new[]
|
|
{
|
|
creator = new MetadataDisplay(EditorSetupStrings.Creator),
|
|
genre = new MetadataDisplay(BeatmapsetsStrings.ShowInfoGenre),
|
|
},
|
|
},
|
|
new FillFlowContainer
|
|
{
|
|
RelativeSizeAxes = Axes.X,
|
|
AutoSizeAxes = Axes.Y,
|
|
Direction = FillDirection.Vertical,
|
|
Spacing = new Vector2(0f, 10f),
|
|
Children = new[]
|
|
{
|
|
source = new MetadataDisplay(BeatmapsetsStrings.ShowInfoSource),
|
|
language = new MetadataDisplay(BeatmapsetsStrings.ShowInfoLanguage),
|
|
},
|
|
},
|
|
new FillFlowContainer
|
|
{
|
|
RelativeSizeAxes = Axes.X,
|
|
AutoSizeAxes = Axes.Y,
|
|
Direction = FillDirection.Vertical,
|
|
Spacing = new Vector2(0f, 10f),
|
|
Children = new[]
|
|
{
|
|
submitted = new MetadataDisplay(SongSelectStrings.Submitted),
|
|
ranked = new MetadataDisplay(SongSelectStrings.Ranked),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
userTags = new MetadataDisplay(BeatmapsetsStrings.ShowInfoUserTags)
|
|
{
|
|
Alpha = 0,
|
|
},
|
|
mapperTags = new MetadataDisplay(BeatmapsetsStrings.ShowInfoMapperTags),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
new ShearAligningWrapper(ratingsWedge = new Container
|
|
{
|
|
Alpha = 0f,
|
|
CornerRadius = 10,
|
|
Masking = true,
|
|
RelativeSizeAxes = Axes.X,
|
|
AutoSizeAxes = Axes.Y,
|
|
Children = new Drawable[]
|
|
{
|
|
new WedgeBackground(),
|
|
new GridContainer
|
|
{
|
|
RelativeSizeAxes = Axes.X,
|
|
AutoSizeAxes = Axes.Y,
|
|
Shear = -OsuGame.SHEAR,
|
|
RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) },
|
|
ColumnDimensions = new[]
|
|
{
|
|
new Dimension(),
|
|
new Dimension(GridSizeMode.Absolute, 10),
|
|
new Dimension(),
|
|
new Dimension(GridSizeMode.Absolute, 10),
|
|
new Dimension(),
|
|
},
|
|
Padding = new MarginPadding { Left = SongSelect.WEDGE_CONTENT_MARGIN, Right = 40f, Vertical = 16 },
|
|
Content = new[]
|
|
{
|
|
new[]
|
|
{
|
|
successRateDisplay = new SuccessRateDisplay(),
|
|
Empty(),
|
|
userRatingDisplay = new UserRatingDisplay(),
|
|
Empty(),
|
|
ratingSpreadDisplay = new RatingSpreadDisplay(),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}),
|
|
new ShearAligningWrapper(failRetryWedge = new Container
|
|
{
|
|
Alpha = 0f,
|
|
CornerRadius = 10,
|
|
Masking = true,
|
|
RelativeSizeAxes = Axes.X,
|
|
AutoSizeAxes = Axes.Y,
|
|
Children = new Drawable[]
|
|
{
|
|
new WedgeBackground(),
|
|
new Container
|
|
{
|
|
RelativeSizeAxes = Axes.X,
|
|
AutoSizeAxes = Axes.Y,
|
|
Shear = -OsuGame.SHEAR,
|
|
Padding = new MarginPadding { Left = SongSelect.WEDGE_CONTENT_MARGIN, Right = 40f, Vertical = 16 },
|
|
Child = failRetryDisplay = new FailRetryDisplay(),
|
|
},
|
|
},
|
|
}),
|
|
}
|
|
};
|
|
}
|
|
|
|
protected override void LoadComplete()
|
|
{
|
|
base.LoadComplete();
|
|
beatmap.BindValueChanged(_ => updateDisplay());
|
|
|
|
apiState = api.State.GetBoundCopy();
|
|
apiState.BindValueChanged(_ => Scheduler.AddOnce(updateDisplay), true);
|
|
}
|
|
|
|
private const double transition_duration = 300;
|
|
|
|
protected override void PopIn()
|
|
{
|
|
this.FadeIn(transition_duration, Easing.OutQuint)
|
|
.MoveToX(0, transition_duration, Easing.OutQuint);
|
|
|
|
updateSubWedgeVisibility();
|
|
}
|
|
|
|
protected override void PopOut()
|
|
{
|
|
this.FadeOut(transition_duration, Easing.OutQuint)
|
|
.MoveToX(-100, transition_duration, Easing.OutQuint);
|
|
|
|
updateSubWedgeVisibility();
|
|
}
|
|
|
|
private void updateSubWedgeVisibility()
|
|
{
|
|
// We could consider hiding individual wedges based on zero data in the future.
|
|
// Needs some experimentation on what looks good.
|
|
|
|
var beatmapInfo = beatmap.Value.BeatmapInfo;
|
|
var currentOnlineBeatmap = currentOnlineBeatmapSet?.Beatmaps.SingleOrDefault(b => b.OnlineID == beatmapInfo.OnlineID);
|
|
|
|
if (State.Value == Visibility.Visible && currentOnlineBeatmap != null)
|
|
{
|
|
ratingsWedge.FadeIn(transition_duration, Easing.OutQuint)
|
|
.MoveToX(0, transition_duration, Easing.OutQuint);
|
|
|
|
failRetryWedge.Delay(100)
|
|
.FadeIn(transition_duration, Easing.OutQuint)
|
|
.MoveToX(0, transition_duration, Easing.OutQuint);
|
|
}
|
|
else
|
|
{
|
|
failRetryWedge.FadeOut(transition_duration, Easing.OutQuint)
|
|
.MoveToX(-50, transition_duration, Easing.OutQuint);
|
|
|
|
ratingsWedge.Delay(100)
|
|
.FadeOut(transition_duration, Easing.OutQuint)
|
|
.MoveToX(-50, transition_duration, Easing.OutQuint);
|
|
}
|
|
}
|
|
|
|
private void updateDisplay()
|
|
{
|
|
var metadata = beatmap.Value.Metadata;
|
|
var beatmapSetInfo = beatmap.Value.BeatmapSetInfo;
|
|
|
|
creator.Data = (metadata.Author.Username, () => linkHandler?.HandleLink(new LinkDetails(LinkAction.OpenUserProfile, metadata.Author)));
|
|
|
|
if (!string.IsNullOrEmpty(metadata.Source))
|
|
source.Data = (metadata.Source, () => songSelect?.Search(metadata.Source));
|
|
else
|
|
source.Data = ("-", null);
|
|
|
|
if (!string.IsNullOrEmpty(metadata.Tags))
|
|
mapperTags.Tags = (metadata.Tags.Split(' '), t => songSelect?.Search(t));
|
|
else
|
|
mapperTags.Tags = (Array.Empty<string>(), _ => { });
|
|
|
|
submitted.Date = beatmapSetInfo.DateSubmitted;
|
|
ranked.Date = beatmapSetInfo.DateRanked;
|
|
|
|
if (currentOnlineBeatmapSet == null || currentOnlineBeatmapSet.OnlineID != beatmapSetInfo.OnlineID)
|
|
refetchBeatmapSet();
|
|
|
|
updateOnlineDisplay();
|
|
}
|
|
|
|
private APIBeatmapSet? currentOnlineBeatmapSet;
|
|
private CancellationTokenSource? cancellationTokenSource;
|
|
private Task<APIBeatmapSet?>? currentFetchTask;
|
|
|
|
private void refetchBeatmapSet()
|
|
{
|
|
var beatmapSetInfo = beatmap.Value.BeatmapSetInfo;
|
|
|
|
cancellationTokenSource?.Cancel();
|
|
currentOnlineBeatmapSet = null;
|
|
|
|
if (beatmapSetInfo.OnlineID >= 1)
|
|
{
|
|
cancellationTokenSource = new CancellationTokenSource();
|
|
currentFetchTask = onlineLookupSource.GetBeatmapSetAsync(beatmapSetInfo.OnlineID);
|
|
currentFetchTask.ContinueWith(t =>
|
|
{
|
|
if (t.IsCompletedSuccessfully)
|
|
currentOnlineBeatmapSet = t.GetResultSafely();
|
|
if (t.Exception != null)
|
|
Logger.Log($"Error when fetching online beatmap set: {t.Exception}", LoggingTarget.Network);
|
|
Scheduler.AddOnce(updateOnlineDisplay);
|
|
});
|
|
}
|
|
}
|
|
|
|
private void updateOnlineDisplay()
|
|
{
|
|
if (currentFetchTask?.IsCompleted == false)
|
|
{
|
|
genre.Data = null;
|
|
language.Data = null;
|
|
userTags.Tags = null;
|
|
return;
|
|
}
|
|
|
|
if (currentOnlineBeatmapSet == null)
|
|
{
|
|
genre.Data = ("-", null);
|
|
language.Data = ("-", null);
|
|
}
|
|
else
|
|
{
|
|
var beatmapInfo = beatmap.Value.BeatmapInfo;
|
|
|
|
var onlineBeatmapSet = currentOnlineBeatmapSet;
|
|
var onlineBeatmap = onlineBeatmapSet.Beatmaps.SingleOrDefault(b => b.OnlineID == beatmapInfo.OnlineID);
|
|
|
|
genre.Data = (onlineBeatmapSet.Genre.Name, () => songSelect?.Search(onlineBeatmapSet.Genre.Name));
|
|
language.Data = (onlineBeatmapSet.Language.Name, () => songSelect?.Search(onlineBeatmapSet.Language.Name));
|
|
|
|
if (onlineBeatmap != null)
|
|
{
|
|
userRatingDisplay.Data = onlineBeatmapSet.Ratings;
|
|
ratingSpreadDisplay.Data = onlineBeatmapSet.Ratings;
|
|
successRateDisplay.Data = (onlineBeatmap.PassCount, onlineBeatmap.PlayCount);
|
|
failRetryDisplay.Data = onlineBeatmap.FailTimes ?? new APIFailTimes();
|
|
}
|
|
}
|
|
|
|
updateUserTags();
|
|
updateSubWedgeVisibility();
|
|
}
|
|
|
|
private void updateUserTags()
|
|
{
|
|
string[] tags = realm.Run(r =>
|
|
{
|
|
// need to refetch because `beatmap.Value.BeatmapInfo` is not going to have the latest tags
|
|
var refetchedBeatmap = r.Find<BeatmapInfo>(beatmap.Value.BeatmapInfo.ID);
|
|
return refetchedBeatmap?.Metadata.UserTags.ToArray() ?? [];
|
|
});
|
|
|
|
if (tags.Length == 0)
|
|
{
|
|
userTags.FadeOut(transition_duration, Easing.OutQuint);
|
|
return;
|
|
}
|
|
|
|
userTags.FadeIn(transition_duration, Easing.OutQuint);
|
|
userTags.Tags = (tags, t => songSelect?.Search($@"tag=""{t}""!"));
|
|
}
|
|
}
|
|
}
|