Merge branch 'master' into realm-key-binding-store

This commit is contained in:
Dean Herbert
2021-06-18 16:52:35 +09:00
87 changed files with 1368 additions and 544 deletions

View File

@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using Newtonsoft.Json;
using osu.Framework.Bindables;
using osu.Framework.Lists;
@ -66,6 +67,7 @@ namespace osu.Game.Beatmaps.ControlPoints
/// </summary>
/// <param name="time">The time to find the difficulty control point at.</param>
/// <returns>The difficulty control point.</returns>
[NotNull]
public DifficultyControlPoint DifficultyPointAt(double time) => binarySearchWithFallback(DifficultyPoints, time, DifficultyControlPoint.DEFAULT);
/// <summary>
@ -73,6 +75,7 @@ namespace osu.Game.Beatmaps.ControlPoints
/// </summary>
/// <param name="time">The time to find the effect control point at.</param>
/// <returns>The effect control point.</returns>
[NotNull]
public EffectControlPoint EffectPointAt(double time) => binarySearchWithFallback(EffectPoints, time, EffectControlPoint.DEFAULT);
/// <summary>
@ -80,6 +83,7 @@ namespace osu.Game.Beatmaps.ControlPoints
/// </summary>
/// <param name="time">The time to find the sound control point at.</param>
/// <returns>The sound control point.</returns>
[NotNull]
public SampleControlPoint SamplePointAt(double time) => binarySearchWithFallback(SamplePoints, time, SamplePoints.Count > 0 ? SamplePoints[0] : SampleControlPoint.DEFAULT);
/// <summary>
@ -87,6 +91,7 @@ namespace osu.Game.Beatmaps.ControlPoints
/// </summary>
/// <param name="time">The time to find the timing control point at.</param>
/// <returns>The timing control point.</returns>
[NotNull]
public TimingControlPoint TimingPointAt(double time) => binarySearchWithFallback(TimingPoints, time, TimingPoints.Count > 0 ? TimingPoints[0] : TimingControlPoint.DEFAULT);
/// <summary>

View File

@ -101,10 +101,20 @@ namespace osu.Game.Beatmaps
/// Rulesets ordered descending by their respective recommended difficulties.
/// The currently selected ruleset will always be first.
/// </returns>
private IEnumerable<RulesetInfo> orderedRulesets =>
recommendedDifficultyMapping
.OrderByDescending(pair => pair.Value).Select(pair => pair.Key).Where(r => !r.Equals(ruleset.Value))
.Prepend(ruleset.Value);
private IEnumerable<RulesetInfo> orderedRulesets
{
get
{
if (LoadState < LoadState.Ready || ruleset.Value == null)
return Enumerable.Empty<RulesetInfo>();
return recommendedDifficultyMapping
.OrderByDescending(pair => pair.Value)
.Select(pair => pair.Key)
.Where(r => !r.Equals(ruleset.Value))
.Prepend(ruleset.Value);
}
}
private void onlineStateChanged(ValueChangedEvent<APIState> state) => Schedule(() =>
{

View File

@ -0,0 +1,33 @@
// 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.Globalization;
using osu.Game.Localisation;
namespace osu.Game.Extensions
{
/// <summary>
/// Conversion utilities for the <see cref="Language"/> enum.
/// </summary>
public static class LanguageExtensions
{
/// <summary>
/// Returns the culture code of the <see cref="CultureInfo"/> that corresponds to the supplied <paramref name="language"/>.
/// </summary>
/// <remarks>
/// This is required as enum member names are not allowed to contain hyphens.
/// </remarks>
public static string ToCultureCode(this Language language)
=> language.ToString().Replace("_", "-");
/// <summary>
/// Attempts to parse the supplied <paramref name="cultureCode"/> to a <see cref="Language"/> value.
/// </summary>
/// <param name="cultureCode">The code of the culture to parse.</param>
/// <param name="language">The parsed <see cref="Language"/>. Valid only if the return value of the method is <see langword="true" />.</param>
/// <returns>Whether the parsing succeeded.</returns>
public static bool TryParseCultureCode(string cultureCode, out Language language)
=> Enum.TryParse(cultureCode.Replace("-", "_"), out language);
}
}

View File

@ -99,5 +99,14 @@ namespace osu.Game.Graphics.Backgrounds
// ensure we're not loading in without a transition.
this.FadeInFromZero(200, Easing.InOutSine);
}
public override bool Equals(Background other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return other.GetType() == GetType()
&& ((SeasonalBackground)other).url == url;
}
}
}

View File

@ -160,7 +160,7 @@ namespace osu.Game.Graphics.UserInterface
Margin = new MarginPadding { Top = 5, Bottom = 5 },
Origin = Anchor.BottomLeft,
Anchor = Anchor.BottomLeft,
Text = (value as IHasDescription)?.Description ?? (value as Enum)?.GetDescription() ?? value.ToString(),
Text = (value as IHasDescription)?.Description ?? (value as Enum)?.GetLocalisableDescription() ?? value.ToString(),
Font = OsuFont.GetFont(size: 14)
},
Bar = new Box

View File

@ -11,6 +11,7 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Graphics.Sprites;
namespace osu.Game.Graphics.UserInterface
@ -81,7 +82,7 @@ namespace osu.Game.Graphics.UserInterface
Active.BindValueChanged(active => Text.Font = Text.Font.With(Typeface.Torus, weight: active.NewValue ? FontWeight.Bold : FontWeight.Medium), true);
}
protected virtual string CreateText() => (Value as Enum)?.GetDescription() ?? Value.ToString();
protected virtual LocalisableString CreateText() => (Value as Enum)?.GetLocalisableDescription() ?? Value.ToString();
protected override bool OnHover(HoverEvent e)
{

View File

@ -10,7 +10,104 @@ namespace osu.Game.Localisation
[Description(@"English")]
en,
// TODO: Requires Arabic glyphs to be added to resources (and possibly also RTL support).
// [Description(@"اَلْعَرَبِيَّةُ")]
// ar,
// TODO: Some accented glyphs are missing. Revisit when adding Inter.
// [Description(@"Беларуская мова")]
// be,
[Description(@"Български")]
bg,
[Description(@"Česky")]
cs,
[Description(@"Dansk")]
da,
[Description(@"Deutsch")]
de,
// TODO: Some accented glyphs are missing. Revisit when adding Inter.
// [Description(@"Ελληνικά")]
// el,
[Description(@"español")]
es,
[Description(@"Suomi")]
fi,
[Description(@"français")]
fr,
[Description(@"Magyar")]
hu,
[Description(@"Bahasa Indonesia")]
id,
[Description(@"Italiano")]
it,
[Description(@"日本語")]
ja
ja,
[Description(@"한국어")]
ko,
[Description(@"Nederlands")]
nl,
[Description(@"Norsk")]
no,
[Description(@"polski")]
pl,
[Description(@"Português")]
pt,
[Description(@"Português (Brasil)")]
pt_br,
[Description(@"Română")]
ro,
[Description(@"Русский")]
ru,
[Description(@"Slovenčina")]
sk,
[Description(@"Svenska")]
sv,
[Description(@"ไทย")]
th,
[Description(@"Tagalog")]
tl,
[Description(@"Türkçe")]
tr,
// TODO: Some accented glyphs are missing. Revisit when adding Inter.
// [Description(@"Українська мова")]
// uk,
[Description(@"Tiếng Việt")]
vi,
[Description(@"简体中文")]
zh,
[Description(@"繁體中文(香港)")]
zh_hk,
[Description(@"繁體中文(台灣)")]
zh_tw
}
}

View File

@ -6,7 +6,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using MessagePack;
using osu.Game.Online.API;
@ -28,11 +27,9 @@ namespace osu.Game.Online.Multiplayer
[Key(3)]
public string Name { get; set; } = "Unnamed room";
[NotNull]
[Key(4)]
public IEnumerable<APIMod> RequiredMods { get; set; } = Enumerable.Empty<APIMod>();
[NotNull]
[Key(5)]
public IEnumerable<APIMod> AllowedMods { get; set; } = Enumerable.Empty<APIMod>();

View File

@ -6,7 +6,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using MessagePack;
using Newtonsoft.Json;
using osu.Game.Online.API;
@ -35,7 +34,6 @@ namespace osu.Game.Online.Multiplayer
/// Any mods applicable only to the local user.
/// </summary>
[Key(3)]
[NotNull]
public IEnumerable<APIMod> Mods { get; set; } = Enumerable.Empty<APIMod>();
[IgnoreMember]

View File

@ -50,8 +50,10 @@ using osu.Game.Updater;
using osu.Game.Utils;
using LogLevel = osu.Framework.Logging.LogLevel;
using osu.Game.Database;
using osu.Game.Extensions;
using osu.Game.IO;
using osu.Game.Localisation;
using osu.Game.Performance;
using osu.Game.Skinning.Editor;
namespace osu.Game
@ -426,9 +428,12 @@ namespace osu.Game
{
// The given ScoreInfo may have missing properties if it was retrieved from online data. Re-retrieve it from the database
// to ensure all the required data for presenting a replay are present.
var databasedScoreInfo = score.OnlineScoreID != null
? ScoreManager.Query(s => s.OnlineScoreID == score.OnlineScoreID)
: ScoreManager.Query(s => s.Hash == score.Hash);
ScoreInfo databasedScoreInfo = null;
if (score.OnlineScoreID != null)
databasedScoreInfo = ScoreManager.Query(s => s.OnlineScoreID == score.OnlineScoreID);
databasedScoreInfo ??= ScoreManager.Query(s => s.Hash == score.Hash);
if (databasedScoreInfo == null)
{
@ -484,6 +489,8 @@ namespace osu.Game
protected virtual UpdateManager CreateUpdateManager() => new UpdateManager();
protected virtual HighPerformanceSession CreateHighPerformanceSession() => new HighPerformanceSession();
protected override Container CreateScalingContainer() => new ScalingContainer(ScalingMode.Everything);
#region Beatmap progression
@ -577,7 +584,7 @@ namespace osu.Game
foreach (var language in Enum.GetValues(typeof(Language)).OfType<Language>())
{
var cultureCode = language.ToString();
var cultureCode = language.ToCultureCode();
Localisation.AddLanguage(cultureCode, new ResourceManagerLocalisationStore(cultureCode));
}
@ -712,7 +719,6 @@ namespace osu.Game
PostNotification = n => notifications.Post(n),
}, Add, true);
loadComponentSingleFile(difficultyRecommender, Add);
loadComponentSingleFile(stableImportManager, Add);
loadComponentSingleFile(screenshotManager, Add);
@ -753,8 +759,11 @@ namespace osu.Game
loadComponentSingleFile(new AccountCreationOverlay(), topMostOverlayContent.Add, true);
loadComponentSingleFile(new DialogOverlay(), topMostOverlayContent.Add, true);
loadComponentSingleFile(CreateHighPerformanceSession(), Add);
chatOverlay.State.ValueChanged += state => channelManager.HighPollRate.Value = state.NewValue == Visibility.Visible;
Add(difficultyRecommender);
Add(externalLinkOpener = new ExternalLinkOpener());
Add(new MusicKeyBindingHandler());

View File

@ -14,6 +14,7 @@ using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Resources.Localisation.Web;
using osuTK.Graphics;
using osu.Game.Rulesets;
using osu.Game.Scoring;
@ -126,15 +127,15 @@ namespace osu.Game.Overlays.BeatmapListing
Padding = new MarginPadding { Horizontal = 10 },
Children = new Drawable[]
{
generalFilter = new BeatmapSearchMultipleSelectionFilterRow<SearchGeneral>(@"General"),
generalFilter = new BeatmapSearchMultipleSelectionFilterRow<SearchGeneral>(BeatmapsStrings.ListingSearchFiltersGeneral),
modeFilter = new BeatmapSearchRulesetFilterRow(),
categoryFilter = new BeatmapSearchFilterRow<SearchCategory>(@"Categories"),
genreFilter = new BeatmapSearchFilterRow<SearchGenre>(@"Genre"),
languageFilter = new BeatmapSearchFilterRow<SearchLanguage>(@"Language"),
extraFilter = new BeatmapSearchMultipleSelectionFilterRow<SearchExtra>(@"Extra"),
categoryFilter = new BeatmapSearchFilterRow<SearchCategory>(BeatmapsStrings.ListingSearchFiltersStatus),
genreFilter = new BeatmapSearchFilterRow<SearchGenre>(BeatmapsStrings.ListingSearchFiltersGenre),
languageFilter = new BeatmapSearchFilterRow<SearchLanguage>(BeatmapsStrings.ListingSearchFiltersLanguage),
extraFilter = new BeatmapSearchMultipleSelectionFilterRow<SearchExtra>(BeatmapsStrings.ListingSearchFiltersExtra),
ranksFilter = new BeatmapSearchScoreFilterRow(),
playedFilter = new BeatmapSearchFilterRow<SearchPlayed>(@"Played"),
explicitContentFilter = new BeatmapSearchFilterRow<SearchExplicit>(@"Explicit Content"),
playedFilter = new BeatmapSearchFilterRow<SearchPlayed>(BeatmapsStrings.ListingSearchFiltersPlayed),
explicitContentFilter = new BeatmapSearchFilterRow<SearchExplicit>(BeatmapsStrings.ListingSearchFiltersNsfw),
}
}
}
@ -172,7 +173,7 @@ namespace osu.Game.Overlays.BeatmapListing
public BeatmapSearchTextBox()
{
PlaceholderText = @"type in keywords...";
PlaceholderText = BeatmapsStrings.ListingSearchPrompt;
}
protected override bool OnKeyDown(KeyDownEvent e)

View File

@ -11,8 +11,8 @@ using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osuTK;
using Humanizer;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Localisation;
namespace osu.Game.Overlays.BeatmapListing
{
@ -26,7 +26,7 @@ namespace osu.Game.Overlays.BeatmapListing
set => current.Current = value;
}
public BeatmapSearchFilterRow(string headerName)
public BeatmapSearchFilterRow(LocalisableString header)
{
Drawable filter;
AutoSizeAxes = Axes.Y;
@ -53,7 +53,7 @@ namespace osu.Game.Overlays.BeatmapListing
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Font = OsuFont.GetFont(size: 13),
Text = headerName.Titleize()
Text = header
},
filter = CreateFilter()
}

View File

@ -9,6 +9,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osuTK;
namespace osu.Game.Overlays.BeatmapListing
@ -19,8 +20,8 @@ namespace osu.Game.Overlays.BeatmapListing
private MultipleSelectionFilter filter;
public BeatmapSearchMultipleSelectionFilterRow(string headerName)
: base(headerName)
public BeatmapSearchMultipleSelectionFilterRow(LocalisableString header)
: base(header)
{
Current.BindTo(filter.Current);
}

View File

@ -3,6 +3,7 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Rulesets;
namespace osu.Game.Overlays.BeatmapListing
@ -10,7 +11,7 @@ namespace osu.Game.Overlays.BeatmapListing
public class BeatmapSearchRulesetFilterRow : BeatmapSearchFilterRow<RulesetInfo>
{
public BeatmapSearchRulesetFilterRow()
: base(@"Mode")
: base(BeatmapsStrings.ListingSearchFiltersMode)
{
}

View File

@ -4,6 +4,8 @@
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Extensions;
using osu.Framework.Localisation;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Scoring;
namespace osu.Game.Overlays.BeatmapListing
@ -11,7 +13,7 @@ namespace osu.Game.Overlays.BeatmapListing
public class BeatmapSearchScoreFilterRow : BeatmapSearchMultipleSelectionFilterRow<ScoreRank>
{
public BeatmapSearchScoreFilterRow()
: base(@"Rank Achieved")
: base(BeatmapsStrings.ListingSearchFiltersRank)
{
}
@ -31,20 +33,7 @@ namespace osu.Game.Overlays.BeatmapListing
{
}
protected override string LabelFor(ScoreRank value)
{
switch (value)
{
case ScoreRank.XH:
return @"Silver SS";
case ScoreRank.SH:
return @"Silver S";
default:
return value.GetDescription();
}
}
protected override LocalisableString LabelFor(ScoreRank value) => value.GetLocalisableDescription();
}
}
}

View File

@ -7,6 +7,7 @@ using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
@ -66,7 +67,7 @@ namespace osu.Game.Overlays.BeatmapListing
/// <summary>
/// Returns the label text to be used for the supplied <paramref name="value"/>.
/// </summary>
protected virtual string LabelFor(T value) => (value as Enum)?.GetDescription() ?? value.ToString();
protected virtual LocalisableString LabelFor(T value) => (value as Enum)?.GetLocalisableDescription() ?? value.ToString();
private void updateState()
{

View File

@ -1,10 +1,14 @@
// 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.ComponentModel;
using osu.Framework.Localisation;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Overlays.BeatmapListing
{
[LocalisableEnum(typeof(SearchCategoryEnumLocalisationMapper))]
public enum SearchCategory
{
Any,
@ -23,4 +27,43 @@ namespace osu.Game.Overlays.BeatmapListing
[Description("My Maps")]
Mine,
}
public class SearchCategoryEnumLocalisationMapper : EnumLocalisationMapper<SearchCategory>
{
public override LocalisableString Map(SearchCategory value)
{
switch (value)
{
case SearchCategory.Any:
return BeatmapsStrings.StatusAny;
case SearchCategory.Leaderboard:
return BeatmapsStrings.StatusLeaderboard;
case SearchCategory.Ranked:
return BeatmapsStrings.StatusRanked;
case SearchCategory.Qualified:
return BeatmapsStrings.StatusQualified;
case SearchCategory.Loved:
return BeatmapsStrings.StatusLoved;
case SearchCategory.Favourites:
return BeatmapsStrings.StatusFavourites;
case SearchCategory.Pending:
return BeatmapsStrings.StatusPending;
case SearchCategory.Graveyard:
return BeatmapsStrings.StatusGraveyard;
case SearchCategory.Mine:
return BeatmapsStrings.StatusMine;
default:
throw new ArgumentOutOfRangeException(nameof(value), value, null);
}
}
}
}

View File

@ -1,11 +1,34 @@
// 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.Localisation;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Overlays.BeatmapListing
{
[LocalisableEnum(typeof(SearchExplicitEnumLocalisationMapper))]
public enum SearchExplicit
{
Hide,
Show
}
public class SearchExplicitEnumLocalisationMapper : EnumLocalisationMapper<SearchExplicit>
{
public override LocalisableString Map(SearchExplicit value)
{
switch (value)
{
case SearchExplicit.Hide:
return BeatmapsStrings.NsfwExclude;
case SearchExplicit.Show:
return BeatmapsStrings.NsfwInclude;
default:
throw new ArgumentOutOfRangeException(nameof(value), value, null);
}
}
}
}

View File

@ -1,10 +1,14 @@
// 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.ComponentModel;
using osu.Framework.Localisation;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Overlays.BeatmapListing
{
[LocalisableEnum(typeof(SearchExtraEnumLocalisationMapper))]
public enum SearchExtra
{
[Description("Has Video")]
@ -13,4 +17,22 @@ namespace osu.Game.Overlays.BeatmapListing
[Description("Has Storyboard")]
Storyboard
}
public class SearchExtraEnumLocalisationMapper : EnumLocalisationMapper<SearchExtra>
{
public override LocalisableString Map(SearchExtra value)
{
switch (value)
{
case SearchExtra.Video:
return BeatmapsStrings.ExtraVideo;
case SearchExtra.Storyboard:
return BeatmapsStrings.ExtraStoryboard;
default:
throw new ArgumentOutOfRangeException(nameof(value), value, null);
}
}
}
}

View File

@ -1,10 +1,14 @@
// 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.ComponentModel;
using osu.Framework.Localisation;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Overlays.BeatmapListing
{
[LocalisableEnum(typeof(SearchGeneralEnumLocalisationMapper))]
public enum SearchGeneral
{
[Description("Recommended difficulty")]
@ -16,4 +20,25 @@ namespace osu.Game.Overlays.BeatmapListing
[Description("Subscribed mappers")]
Follows
}
public class SearchGeneralEnumLocalisationMapper : EnumLocalisationMapper<SearchGeneral>
{
public override LocalisableString Map(SearchGeneral value)
{
switch (value)
{
case SearchGeneral.Recommended:
return BeatmapsStrings.GeneralRecommended;
case SearchGeneral.Converts:
return BeatmapsStrings.GeneralConverts;
case SearchGeneral.Follows:
return BeatmapsStrings.GeneralFollows;
default:
throw new ArgumentOutOfRangeException(nameof(value), value, null);
}
}
}
}

View File

@ -1,10 +1,14 @@
// 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.ComponentModel;
using osu.Framework.Localisation;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Overlays.BeatmapListing
{
[LocalisableEnum(typeof(SearchGenreEnumLocalisationMapper))]
public enum SearchGenre
{
Any = 0,
@ -26,4 +30,58 @@ namespace osu.Game.Overlays.BeatmapListing
Folk = 13,
Jazz = 14
}
public class SearchGenreEnumLocalisationMapper : EnumLocalisationMapper<SearchGenre>
{
public override LocalisableString Map(SearchGenre value)
{
switch (value)
{
case SearchGenre.Any:
return BeatmapsStrings.GenreAny;
case SearchGenre.Unspecified:
return BeatmapsStrings.GenreUnspecified;
case SearchGenre.VideoGame:
return BeatmapsStrings.GenreVideoGame;
case SearchGenre.Anime:
return BeatmapsStrings.GenreAnime;
case SearchGenre.Rock:
return BeatmapsStrings.GenreRock;
case SearchGenre.Pop:
return BeatmapsStrings.GenrePop;
case SearchGenre.Other:
return BeatmapsStrings.GenreOther;
case SearchGenre.Novelty:
return BeatmapsStrings.GenreNovelty;
case SearchGenre.HipHop:
return BeatmapsStrings.GenreHipHop;
case SearchGenre.Electronic:
return BeatmapsStrings.GenreElectronic;
case SearchGenre.Metal:
return BeatmapsStrings.GenreMetal;
case SearchGenre.Classical:
return BeatmapsStrings.GenreClassical;
case SearchGenre.Folk:
return BeatmapsStrings.GenreFolk;
case SearchGenre.Jazz:
return BeatmapsStrings.GenreJazz;
default:
throw new ArgumentOutOfRangeException(nameof(value), value, null);
}
}
}
}

View File

@ -1,10 +1,14 @@
// 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.Localisation;
using osu.Framework.Utils;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Overlays.BeatmapListing
{
[LocalisableEnum(typeof(SearchLanguageEnumLocalisationMapper))]
[HasOrderedElements]
public enum SearchLanguage
{
@ -53,4 +57,61 @@ namespace osu.Game.Overlays.BeatmapListing
[Order(13)]
Other
}
public class SearchLanguageEnumLocalisationMapper : EnumLocalisationMapper<SearchLanguage>
{
public override LocalisableString Map(SearchLanguage value)
{
switch (value)
{
case SearchLanguage.Any:
return BeatmapsStrings.LanguageAny;
case SearchLanguage.Unspecified:
return BeatmapsStrings.LanguageUnspecified;
case SearchLanguage.English:
return BeatmapsStrings.LanguageEnglish;
case SearchLanguage.Japanese:
return BeatmapsStrings.LanguageJapanese;
case SearchLanguage.Chinese:
return BeatmapsStrings.LanguageChinese;
case SearchLanguage.Instrumental:
return BeatmapsStrings.LanguageInstrumental;
case SearchLanguage.Korean:
return BeatmapsStrings.LanguageKorean;
case SearchLanguage.French:
return BeatmapsStrings.LanguageFrench;
case SearchLanguage.German:
return BeatmapsStrings.LanguageGerman;
case SearchLanguage.Swedish:
return BeatmapsStrings.LanguageSwedish;
case SearchLanguage.Spanish:
return BeatmapsStrings.LanguageSpanish;
case SearchLanguage.Italian:
return BeatmapsStrings.LanguageItalian;
case SearchLanguage.Russian:
return BeatmapsStrings.LanguageRussian;
case SearchLanguage.Polish:
return BeatmapsStrings.LanguagePolish;
case SearchLanguage.Other:
return BeatmapsStrings.LanguageOther;
default:
throw new ArgumentOutOfRangeException(nameof(value), value, null);
}
}
}
}

View File

@ -1,12 +1,38 @@
// 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.Localisation;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Overlays.BeatmapListing
{
[LocalisableEnum(typeof(SearchPlayedEnumLocalisationMapper))]
public enum SearchPlayed
{
Any,
Played,
Unplayed
}
public class SearchPlayedEnumLocalisationMapper : EnumLocalisationMapper<SearchPlayed>
{
public override LocalisableString Map(SearchPlayed value)
{
switch (value)
{
case SearchPlayed.Any:
return BeatmapsStrings.PlayedAny;
case SearchPlayed.Played:
return BeatmapsStrings.PlayedPlayed;
case SearchPlayed.Unplayed:
return BeatmapsStrings.PlayedUnplayed;
default:
throw new ArgumentOutOfRangeException(nameof(value), value, null);
}
}
}
}

View File

@ -1,8 +1,13 @@
// 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.Localisation;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Overlays.BeatmapListing
{
[LocalisableEnum(typeof(SortCriteriaLocalisationMapper))]
public enum SortCriteria
{
Title,
@ -14,4 +19,40 @@ namespace osu.Game.Overlays.BeatmapListing
Favourites,
Relevance
}
public class SortCriteriaLocalisationMapper : EnumLocalisationMapper<SortCriteria>
{
public override LocalisableString Map(SortCriteria value)
{
switch (value)
{
case SortCriteria.Title:
return BeatmapsStrings.ListingSearchSortingTitle;
case SortCriteria.Artist:
return BeatmapsStrings.ListingSearchSortingArtist;
case SortCriteria.Difficulty:
return BeatmapsStrings.ListingSearchSortingDifficulty;
case SortCriteria.Ranked:
return BeatmapsStrings.ListingSearchSortingRanked;
case SortCriteria.Rating:
return BeatmapsStrings.ListingSearchSortingRating;
case SortCriteria.Plays:
return BeatmapsStrings.ListingSearchSortingPlays;
case SortCriteria.Favourites:
return BeatmapsStrings.ListingSearchSortingFavourites;
case SortCriteria.Relevance:
return BeatmapsStrings.ListingSearchSortingRelevance;
default:
throw new ArgumentOutOfRangeException(nameof(value), value, null);
}
}
}
}

View File

@ -18,6 +18,7 @@ using osu.Game.Beatmaps;
using osu.Game.Graphics.Sprites;
using osu.Game.Overlays.BeatmapListing;
using osu.Game.Overlays.BeatmapListing.Panels;
using osu.Game.Resources.Localisation.Web;
using osuTK;
using osuTK.Graphics;
@ -232,7 +233,7 @@ namespace osu.Game.Overlays
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Text = @"... nope, nothing found.",
Text = BeatmapsStrings.ListingSearchNotFoundQuote,
}
}
});

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@ -22,8 +23,8 @@ namespace osu.Game.Overlays.BeatmapSet
{
private const float height = 50;
private readonly UpdateableAvatar avatar;
private readonly FillFlowContainer fields;
private UpdateableAvatar avatar;
private FillFlowContainer fields;
private BeatmapSetInfo beatmapSet;
@ -35,11 +36,46 @@ namespace osu.Game.Overlays.BeatmapSet
if (value == beatmapSet) return;
beatmapSet = value;
updateDisplay();
Scheduler.AddOnce(updateDisplay);
}
}
[BackgroundDependencyLoader]
private void load()
{
RelativeSizeAxes = Axes.X;
Height = height;
Children = new Drawable[]
{
new Container
{
AutoSizeAxes = Axes.Both,
CornerRadius = 4,
Masking = true,
Child = avatar = new UpdateableAvatar(showGuestOnNull: false)
{
Size = new Vector2(height),
},
EdgeEffect = new EdgeEffectParameters
{
Colour = Color4.Black.Opacity(0.25f),
Type = EdgeEffectType.Shadow,
Radius = 4,
Offset = new Vector2(0f, 1f),
},
},
fields = new FillFlowContainer
{
RelativeSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Padding = new MarginPadding { Left = height + 5 },
},
};
Scheduler.AddOnce(updateDisplay);
}
private void updateDisplay()
{
avatar.User = BeatmapSet?.Metadata.Author;
@ -69,45 +105,6 @@ namespace osu.Game.Overlays.BeatmapSet
}
}
public AuthorInfo()
{
RelativeSizeAxes = Axes.X;
Height = height;
Children = new Drawable[]
{
new Container
{
AutoSizeAxes = Axes.Both,
CornerRadius = 4,
Masking = true,
Child = avatar = new UpdateableAvatar
{
ShowGuestOnNull = false,
Size = new Vector2(height),
},
EdgeEffect = new EdgeEffectParameters
{
Colour = Color4.Black.Opacity(0.25f),
Type = EdgeEffectType.Shadow,
Radius = 4,
Offset = new Vector2(0f, 1f),
},
},
fields = new FillFlowContainer
{
RelativeSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Padding = new MarginPadding { Left = height + 5 },
},
};
}
private void load()
{
updateDisplay();
}
private class Field : FillFlowContainer
{
public Field(string first, string second, FontUsage secondFont)

View File

@ -61,7 +61,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
},
}
},
avatar = new UpdateableAvatar
avatar = new UpdateableAvatar(showGuestOnNull: false)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@ -75,7 +75,6 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
Offset = new Vector2(0, 2),
Radius = 1,
},
ShowGuestOnNull = false,
},
new FillFlowContainer
{

View File

@ -51,7 +51,7 @@ namespace osu.Game.Overlays.Chat.Tabs
Child = new DelayedLoadWrapper(avatar = new ClickableAvatar(value.Users.First())
{
RelativeSizeAxes = Axes.Both,
OpenOnClick = { Value = false },
OpenOnClick = false,
})
{
RelativeSizeAxes = Axes.Both,

View File

@ -18,6 +18,7 @@ using JetBrains.Annotations;
using System;
using osu.Framework.Extensions;
using osu.Framework.Localisation;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Overlays
{
@ -54,7 +55,7 @@ namespace osu.Game.Overlays
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold),
Text = @"Sort by"
Text = SortStrings.Default
},
CreateControl().With(c =>
{
@ -143,7 +144,7 @@ namespace osu.Game.Overlays
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold),
Text = (value as Enum)?.GetDescription() ?? value.ToString()
Text = (value as Enum)?.GetLocalisableDescription() ?? value.ToString()
}
}
});

View File

@ -58,13 +58,11 @@ namespace osu.Game.Overlays.Profile.Header
Origin = Anchor.CentreLeft,
Children = new Drawable[]
{
avatar = new UpdateableAvatar
avatar = new UpdateableAvatar(openOnClick: false, showGuestOnNull: false)
{
Size = new Vector2(avatar_size),
Masking = true,
CornerRadius = avatar_size * 0.25f,
OpenOnClick = { Value = false },
ShowGuestOnNull = false,
},
new Container
{

View File

@ -1,11 +1,11 @@
// 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.Framework.Configuration;
using osu.Framework.Graphics;
using osu.Game.Extensions;
using osu.Game.Localisation;
namespace osu.Game.Overlays.Settings.Sections.General
@ -35,11 +35,11 @@ namespace osu.Game.Overlays.Settings.Sections.General
},
};
if (!Enum.TryParse<Language>(frameworkLocale.Value, out var locale))
if (!LanguageExtensions.TryParseCultureCode(frameworkLocale.Value, out var locale))
locale = Language.en;
languageSelection.Current.Value = locale;
languageSelection.Current.BindValueChanged(val => frameworkLocale.Value = val.NewValue.ToString());
languageSelection.Current.BindValueChanged(val => frameworkLocale.Value = val.NewValue.ToCultureCode());
}
}
}

View File

@ -19,6 +19,8 @@ namespace osu.Game.Overlays.Settings
Margin = new MarginPadding { Top = 5 };
RelativeSizeAxes = Axes.X;
}
protected override DropdownMenu CreateMenu() => base.CreateMenu().With(m => m.MaxHeight = 200);
}
}
}

View File

@ -106,7 +106,19 @@ namespace osu.Game.Overlays
public OverlayHeaderTabItem(T value)
: base(value)
{
Text.Text = ((Value as Enum)?.GetDescription() ?? Value.ToString()).ToLower();
if (!(Value is Enum enumValue))
Text.Text = Value.ToString().ToLower();
else
{
var localisableDescription = enumValue.GetLocalisableDescription();
var nonLocalisableDescription = enumValue.GetDescription();
// If localisable == non-localisable, then we must have a basic string, so .ToLower() is used.
Text.Text = localisableDescription.Equals(nonLocalisableDescription)
? nonLocalisableDescription.ToLower()
: localisableDescription;
}
Text.Font = OsuFont.GetFont(size: 14);
Text.Margin = new MarginPadding { Vertical = 16.5f }; // 15px padding + 1.5px line-height difference compensation
Bar.Margin = new MarginPadding { Bottom = bar_height };

View File

@ -32,14 +32,13 @@ namespace osu.Game.Overlays.Toolbar
Add(new OpaqueBackground { Depth = 1 });
Flow.Add(avatar = new UpdateableAvatar
Flow.Add(avatar = new UpdateableAvatar(openOnClick: false)
{
Masking = true,
Size = new Vector2(32),
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
CornerRadius = 4,
OpenOnClick = { Value = false },
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Shadow,

View File

@ -0,0 +1,47 @@
// 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.Runtime;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
namespace osu.Game.Performance
{
public class HighPerformanceSession : Component
{
private readonly IBindable<bool> localUserPlaying = new Bindable<bool>();
private GCLatencyMode originalGCMode;
[BackgroundDependencyLoader]
private void load(OsuGame game)
{
localUserPlaying.BindTo(game.LocalUserPlaying);
}
protected override void LoadComplete()
{
base.LoadComplete();
localUserPlaying.BindValueChanged(playing =>
{
if (playing.NewValue)
EnableHighPerformanceSession();
else
DisableHighPerformanceSession();
}, true);
}
protected virtual void EnableHighPerformanceSession()
{
originalGCMode = GCSettings.LatencyMode;
GCSettings.LatencyMode = GCLatencyMode.LowLatency;
}
protected virtual void DisableHighPerformanceSession()
{
if (GCSettings.LatencyMode == GCLatencyMode.LowLatency)
GCSettings.LatencyMode = originalGCMode;
}
}
}

View File

@ -43,6 +43,9 @@ namespace osu.Game.Rulesets.Edit
protected readonly Ruleset Ruleset;
// Provides `Playfield`
private DependencyContainer dependencies;
[Resolved]
protected EditorClock EditorClock { get; private set; }
@ -69,6 +72,9 @@ namespace osu.Game.Rulesets.Edit
Ruleset = ruleset;
}
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) =>
dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
[BackgroundDependencyLoader]
private void load()
{
@ -88,6 +94,8 @@ namespace osu.Game.Rulesets.Edit
return;
}
dependencies.CacheAs(Playfield);
const float toolbar_width = 200;
InternalChildren = new Drawable[]

View File

@ -1,7 +1,6 @@
// 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 osu.Game.Rulesets.Objects.Drawables;
namespace osu.Game.Rulesets.Mods
@ -9,13 +8,12 @@ namespace osu.Game.Rulesets.Mods
/// <summary>
/// An interface for <see cref="Mod"/>s that can be applied to <see cref="DrawableHitObject"/>s.
/// </summary>
public interface IApplicableToDrawableHitObjects : IApplicableMod
public interface IApplicableToDrawableHitObject : IApplicableMod
{
/// <summary>
/// Applies this <see cref="IApplicableToDrawableHitObjects"/> to a list of <see cref="DrawableHitObject"/>s.
/// Applies this <see cref="IApplicableToDrawableHitObject"/> to a <see cref="DrawableHitObject"/>.
/// This will only be invoked with top-level <see cref="DrawableHitObject"/>s. Access <see cref="DrawableHitObject.NestedHitObjects"/> if adjusting nested objects is necessary.
/// </summary>
/// <param name="drawables">The list of <see cref="DrawableHitObject"/>s to apply to.</param>
void ApplyToDrawableHitObjects(IEnumerable<DrawableHitObject> drawables);
void ApplyToDrawableHitObject(DrawableHitObject drawable);
}
}

View File

@ -0,0 +1,18 @@
// 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 osu.Framework.Extensions.IEnumerableExtensions;
using osu.Game.Rulesets.Objects.Drawables;
namespace osu.Game.Rulesets.Mods
{
[Obsolete(@"Use the singular version IApplicableToDrawableHitObject instead.")] // Can be removed 20211216
public interface IApplicableToDrawableHitObjects : IApplicableToDrawableHitObject
{
void ApplyToDrawableHitObjects(IEnumerable<DrawableHitObject> drawables);
void IApplicableToDrawableHitObject.ApplyToDrawableHitObject(DrawableHitObject drawable) => ApplyToDrawableHitObjects(drawable.Yield());
}
}

View File

@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Mods
/// A <see cref="Mod"/> which applies visibility adjustments to <see cref="DrawableHitObject"/>s
/// with an optional increased visibility adjustment depending on the user's "increase first object visibility" setting.
/// </summary>
public abstract class ModWithVisibilityAdjustment : Mod, IReadFromConfig, IApplicableToBeatmap, IApplicableToDrawableHitObjects
public abstract class ModWithVisibilityAdjustment : Mod, IReadFromConfig, IApplicableToBeatmap, IApplicableToDrawableHitObject
{
/// <summary>
/// The first adjustable object.
@ -73,19 +73,16 @@ namespace osu.Game.Rulesets.Mods
}
}
public virtual void ApplyToDrawableHitObjects(IEnumerable<DrawableHitObject> drawables)
public virtual void ApplyToDrawableHitObject(DrawableHitObject dho)
{
foreach (var dho in drawables)
dho.ApplyCustomUpdateState += (o, state) =>
{
dho.ApplyCustomUpdateState += (o, state) =>
{
// Increased visibility is applied to the entire first object, including all of its nested hitobjects.
if (IncreaseFirstObjectVisibility.Value && isObjectEqualToOrNestedIn(o.HitObject, FirstObject))
ApplyIncreasedVisibilityState(o, state);
else
ApplyNormalVisibilityState(o, state);
};
}
// Increased visibility is applied to the entire first object, including all of its nested hitobjects.
if (IncreaseFirstObjectVisibility.Value && isObjectEqualToOrNestedIn(o.HitObject, FirstObject))
ApplyIncreasedVisibilityState(o, state);
else
ApplyNormalVisibilityState(o, state);
};
}
/// <summary>

View File

@ -6,7 +6,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using osu.Game.Input.Handlers;
using osu.Game.Replays;
@ -117,7 +116,7 @@ namespace osu.Game.Rulesets.Replays
}
}
protected virtual bool IsImportant([NotNull] TFrame frame) => false;
protected virtual bool IsImportant(TFrame frame) => false;
/// <summary>
/// Update the current frame based on an incoming time value.

View File

@ -199,8 +199,11 @@ namespace osu.Game.Rulesets.UI
Playfield.PostProcess();
foreach (var mod in Mods.OfType<IApplicableToDrawableHitObjects>())
mod.ApplyToDrawableHitObjects(Playfield.AllHitObjects);
foreach (var mod in Mods.OfType<IApplicableToDrawableHitObject>())
{
foreach (var drawableHitObject in Playfield.AllHitObjects)
mod.ApplyToDrawableHitObject(drawableHitObject);
}
}
public override void RequestResume(Action continueResume)

View File

@ -356,8 +356,8 @@ namespace osu.Game.Rulesets.UI
// This is done before Apply() so that the state is updated once when the hitobject is applied.
if (mods != null)
{
foreach (var m in mods.OfType<IApplicableToDrawableHitObjects>())
m.ApplyToDrawableHitObjects(dho.Yield());
foreach (var m in mods.OfType<IApplicableToDrawableHitObject>())
m.ApplyToDrawableHitObject(dho);
}
}

View File

@ -69,7 +69,7 @@ namespace osu.Game.Rulesets.UI.Scrolling
/// </remarks>
public double TimeAtPosition(float localPosition, double currentTime)
{
float scrollPosition = axisInverted ? scrollLength - localPosition : localPosition;
float scrollPosition = axisInverted ? -localPosition : localPosition;
return scrollingInfo.Algorithm.TimeAt(scrollPosition, currentTime, timeRange.Value, scrollLength);
}
@ -81,8 +81,10 @@ namespace osu.Game.Rulesets.UI.Scrolling
/// </remarks>
public double TimeAtScreenSpacePosition(Vector2 screenSpacePosition)
{
Vector2 localPosition = ToLocalSpace(screenSpacePosition);
return TimeAtPosition(scrollingAxis == Direction.Horizontal ? localPosition.X : localPosition.Y, Time.Current);
Vector2 pos = ToLocalSpace(screenSpacePosition);
float localPosition = scrollingAxis == Direction.Horizontal ? pos.X : pos.Y;
localPosition -= axisInverted ? scrollLength : 0;
return TimeAtPosition(localPosition, Time.Current);
}
/// <summary>
@ -91,7 +93,7 @@ namespace osu.Game.Rulesets.UI.Scrolling
public float PositionAtTime(double time, double currentTime)
{
float scrollPosition = scrollingInfo.Algorithm.PositionAt(time, currentTime, timeRange.Value, scrollLength);
return axisInverted ? scrollLength - scrollPosition : scrollPosition;
return axisInverted ? -scrollPosition : scrollPosition;
}
/// <summary>
@ -106,6 +108,7 @@ namespace osu.Game.Rulesets.UI.Scrolling
public Vector2 ScreenSpacePositionAtTime(double time)
{
float localPosition = PositionAtTime(time, Time.Current);
localPosition += axisInverted ? scrollLength : 0;
return scrollingAxis == Direction.Horizontal
? ToScreenSpace(new Vector2(localPosition, DrawHeight / 2))
: ToScreenSpace(new Vector2(DrawWidth / 2, localPosition));
@ -236,14 +239,10 @@ namespace osu.Game.Rulesets.UI.Scrolling
{
float position = PositionAtTime(hitObject.HitObject.StartTime, currentTime);
// The position returned from `PositionAtTime` is assuming the `TopLeft` anchor.
// A correction is needed because the hit objects are using a different anchor for each direction (e.g. `BottomCentre` for `Bottom` direction).
float anchorCorrection = axisInverted ? scrollLength : 0;
if (scrollingAxis == Direction.Horizontal)
hitObject.X = position - anchorCorrection;
hitObject.X = position;
else
hitObject.Y = position - anchorCorrection;
hitObject.Y = position;
}
}
}

View File

@ -1,10 +1,14 @@
// 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.ComponentModel;
using osu.Framework.Localisation;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Scoring
{
[LocalisableEnum(typeof(ScoreRankEnumLocalisationMapper))]
public enum ScoreRank
{
[Description(@"D")]
@ -31,4 +35,40 @@ namespace osu.Game.Scoring
[Description(@"SS+")]
XH,
}
public class ScoreRankEnumLocalisationMapper : EnumLocalisationMapper<ScoreRank>
{
public override LocalisableString Map(ScoreRank value)
{
switch (value)
{
case ScoreRank.XH:
return BeatmapsStrings.RankXH;
case ScoreRank.X:
return BeatmapsStrings.RankX;
case ScoreRank.SH:
return BeatmapsStrings.RankSH;
case ScoreRank.S:
return BeatmapsStrings.RankS;
case ScoreRank.A:
return BeatmapsStrings.RankA;
case ScoreRank.B:
return BeatmapsStrings.RankB;
case ScoreRank.C:
return BeatmapsStrings.RankC;
case ScoreRank.D:
return BeatmapsStrings.RankD;
default:
throw new ArgumentOutOfRangeException(nameof(value), value, null);
}
}
}
}

View File

@ -5,7 +5,6 @@ using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Threading;
using osu.Game.Users;
@ -91,7 +90,7 @@ namespace osu.Game.Screens.OnlinePlay.Components
});
}
private class UserTile : CompositeDrawable, IHasTooltip
private class UserTile : CompositeDrawable
{
public User User
{
@ -99,8 +98,6 @@ namespace osu.Game.Screens.OnlinePlay.Components
set => avatar.User = value;
}
public string TooltipText => User?.Username ?? string.Empty;
private readonly UpdateableAvatar avatar;
public UserTile()
@ -116,7 +113,7 @@ namespace osu.Game.Screens.OnlinePlay.Components
RelativeSizeAxes = Axes.Both,
Colour = Color4Extensions.FromHex(@"27252d"),
},
avatar = new UpdateableAvatar { RelativeSizeAxes = Axes.Both },
avatar = new UpdateableAvatar(showUsernameTooltip: true) { RelativeSizeAxes = Axes.Both },
};
}
}

View File

@ -54,9 +54,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
return new PlaylistsResultsScreen(score, RoomId.Value.Value, PlaylistItem, true);
}
protected override void PrepareScoreForResults()
protected override void PrepareScoreForResults(Score score)
{
base.PrepareScoreForResults();
base.PrepareScoreForResults(score);
Score.ScoreInfo.TotalScore = (int)Math.Round(ScoreProcessor.GetStandardisedScore());
}

View File

@ -181,12 +181,6 @@ namespace osu.Game.Screens.Play
DrawableRuleset.SetRecordTarget(Score);
}
protected virtual void PrepareScoreForResults()
{
// perform one final population to ensure everything is up-to-date.
ScoreProcessor.PopulateScore(Score.ScoreInfo);
}
[BackgroundDependencyLoader(true)]
private void load(AudioManager audio, OsuConfigManager config, OsuGameBase game)
{
@ -301,7 +295,7 @@ namespace osu.Game.Screens.Play
DimmableStoryboard.HasStoryboardEnded.ValueChanged += storyboardEnded =>
{
if (storyboardEnded.NewValue && completionProgressDelegate == null)
if (storyboardEnded.NewValue && resultsDisplayDelegate == null)
updateCompletionState();
};
@ -512,19 +506,25 @@ namespace osu.Game.Screens.Play
}
/// <summary>
/// Exits the <see cref="Player"/>.
/// Attempts to complete a user request to exit gameplay.
/// </summary>
/// <remarks>
/// <list type="bullet">
/// <item>This should only be called in response to a user interaction. Exiting is not guaranteed.</item>
/// <item>This will interrupt any pending progression to the results screen, even if the transition has begun.</item>
/// </list>
/// </remarks>
/// <param name="showDialogFirst">
/// Whether the pause or fail dialog should be shown before performing an exit.
/// If true and a dialog is not yet displayed, the exit will be blocked the the relevant dialog will display instead.
/// If <see langword="true"/> and a dialog is not yet displayed, the exit will be blocked and the relevant dialog will display instead.
/// </param>
protected void PerformExit(bool showDialogFirst)
{
// if a restart has been requested, cancel any pending completion (user has shown intent to restart).
completionProgressDelegate?.Cancel();
// if an exit has been requested, cancel any pending completion (the user has shown intention to exit).
resultsDisplayDelegate?.Cancel();
// there is a chance that the exit was performed after the transition to results has started.
// we want to give the user what they want, so forcefully return to this screen (to proceed with the upwards exit process).
// there is a chance that an exit request occurs after the transition to results has already started.
// even in such a case, the user has shown intent, so forcefully return to this screen (to proceed with the upwards exit process).
if (!this.IsCurrentScreen())
{
ValidForResume = false;
@ -547,7 +547,7 @@ namespace osu.Game.Screens.Play
return;
}
// there's a chance the pausing is not supported in the current state, at which point immediate exit should be preferred.
// even if this call has requested a dialog, there is a chance the current player mode doesn't support pausing.
if (pausingSupportedByCurrentState)
{
// in the case a dialog needs to be shown, attempt to pause and show it.
@ -555,14 +555,12 @@ namespace osu.Game.Screens.Play
Pause();
return;
}
// if the score is ready for display but results screen has not been pushed yet (e.g. storyboard is still playing beyond gameplay), then transition to results screen instead of exiting.
if (prepareScoreForDisplayTask != null && completionProgressDelegate == null)
{
updateCompletionState(true);
}
}
// The actual exit is performed if
// - the pause / fail dialog was not requested
// - the pause / fail dialog was requested but is already displayed (user showing intention to exit).
// - the pause / fail dialog was requested but couldn't be displayed due to the type or state of this Player instance.
this.Exit();
}
@ -626,7 +624,20 @@ namespace osu.Game.Screens.Play
PerformExit(false);
}
private ScheduledDelegate completionProgressDelegate;
/// <summary>
/// This delegate, when set, means the results screen has been queued to appear.
/// The display of the results screen may be delayed by any work being done in <see cref="PrepareScoreForResults"/> and <see cref="PrepareScoreForResultsAsync"/>.
/// </summary>
/// <remarks>
/// Once set, this can *only* be cancelled by rewinding, ie. if <see cref="JudgementProcessor.HasCompleted">ScoreProcessor.HasCompleted</see> becomes <see langword="false"/>.
/// Even if the user requests an exit, it will forcefully proceed to the results screen (see special case in <see cref="OnExiting"/>).
/// </remarks>
private ScheduledDelegate resultsDisplayDelegate;
/// <summary>
/// A task which asynchronously prepares a completed score for display at results.
/// This may include performing net requests or importing the score into the database, generally to ensure things are in a sane state for the play session.
/// </summary>
private Task<ScoreInfo> prepareScoreForDisplayTask;
/// <summary>
@ -636,57 +647,44 @@ namespace osu.Game.Screens.Play
/// <exception cref="InvalidOperationException">Thrown if this method is called more than once without changing state.</exception>
private void updateCompletionState(bool skipStoryboardOutro = false)
{
// screen may be in the exiting transition phase.
// If this player instance is in the middle of an exit, don't attempt any kind of state update.
if (!this.IsCurrentScreen())
return;
// Special case to handle rewinding post-completion. This is the only way already queued forward progress can be cancelled.
// TODO: Investigate whether this can be moved to a RewindablePlayer subclass or similar.
// Currently, even if this scenario is hit, prepareScoreForDisplay has already been queued (and potentially run).
// In scenarios where rewinding is possible (replay, spectating) this is a non-issue as no submission/import work is done,
// but it still doesn't feel right that this exists here.
if (!ScoreProcessor.HasCompleted.Value)
{
completionProgressDelegate?.Cancel();
completionProgressDelegate = null;
resultsDisplayDelegate?.Cancel();
resultsDisplayDelegate = null;
ValidForResume = true;
skipOutroOverlay.Hide();
return;
}
if (completionProgressDelegate != null)
throw new InvalidOperationException($"{nameof(updateCompletionState)} was fired more than once");
if (resultsDisplayDelegate != null)
throw new InvalidOperationException(@$"{nameof(updateCompletionState)} should never be fired more than once.");
// Only show the completion screen if the player hasn't failed
if (HealthProcessor.HasFailed)
return;
// Setting this early in the process means that even if something were to go wrong in the order of events following, there
// is no chance that a user could return to the (already completed) Player instance from a child screen.
ValidForResume = false;
// ensure we are not writing to the replay any more, as we are about to consume and store the score.
// Ensure we are not writing to the replay any more, as we are about to consume and store the score.
DrawableRuleset.SetRecordTarget(null);
if (!Configuration.ShowResults) return;
if (!Configuration.ShowResults)
return;
prepareScoreForDisplayTask ??= Task.Run(async () =>
{
PrepareScoreForResults();
try
{
await PrepareScoreForResultsAsync(Score).ConfigureAwait(false);
}
catch (Exception ex)
{
Logger.Error(ex, "Score preparation failed!");
}
try
{
await ImportScore(Score).ConfigureAwait(false);
}
catch (Exception ex)
{
Logger.Error(ex, "Score import failed!");
}
return Score.ScoreInfo;
});
// Asynchronously run score preparation operations (database import, online submission etc.).
prepareScoreForDisplayTask ??= Task.Run(prepareScoreForResults);
if (skipStoryboardOutro)
{
@ -706,7 +704,33 @@ namespace osu.Game.Screens.Play
scheduleCompletion();
}
private void scheduleCompletion() => completionProgressDelegate = Schedule(() =>
private async Task<ScoreInfo> prepareScoreForResults()
{
// ReSharper disable once MethodHasAsyncOverload
PrepareScoreForResults(Score);
try
{
await PrepareScoreForResultsAsync(Score).ConfigureAwait(false);
}
catch (Exception ex)
{
Logger.Error(ex, @"Score preparation failed!");
}
try
{
await ImportScore(Score).ConfigureAwait(false);
}
catch (Exception ex)
{
Logger.Error(ex, @"Score import failed!");
}
return Score.ScoreInfo;
}
private void scheduleCompletion() => resultsDisplayDelegate = Schedule(() =>
{
if (!prepareScoreForDisplayTask.IsCompleted)
{
@ -915,10 +939,11 @@ namespace osu.Game.Screens.Play
{
screenSuspension?.Expire();
if (completionProgressDelegate != null && !completionProgressDelegate.Cancelled && !completionProgressDelegate.Completed)
// if the results screen is prepared to be displayed, forcefully show it on an exit request.
// usually if a user has completed a play session they do want to see results. and if they don't they can hit the same key a second time.
if (resultsDisplayDelegate != null && !resultsDisplayDelegate.Cancelled && !resultsDisplayDelegate.Completed)
{
// proceed to result screen if beatmap already finished playing
completionProgressDelegate.RunTask();
resultsDisplayDelegate.RunTask();
return true;
}
@ -979,6 +1004,19 @@ namespace osu.Game.Screens.Play
score.ScoreInfo.OnlineScoreID = onlineScoreId;
}
/// <summary>
/// Prepare the <see cref="Scoring.Score"/> for display at results.
/// </summary>
/// <remarks>
/// This is run synchronously before <see cref="PrepareScoreForResultsAsync"/> is run.
/// </remarks>
/// <param name="score">The <see cref="Scoring.Score"/> to prepare.</param>
protected virtual void PrepareScoreForResults(Score score)
{
// perform one final population to ensure everything is up-to-date.
ScoreProcessor.PopulateScore(score.ScoreInfo);
}
/// <summary>
/// Prepare the <see cref="Scoring.Score"/> for display at results.
/// </summary>

View File

@ -18,6 +18,8 @@ namespace osu.Game.Skinning
private readonly BindableList<ISkinnableDrawable> components = new BindableList<ISkinnableDrawable>();
public bool ComponentsLoaded { get; private set; }
public SkinnableTargetContainer(SkinnableTarget target)
{
Target = target;
@ -30,6 +32,7 @@ namespace osu.Game.Skinning
{
ClearInternal();
components.Clear();
ComponentsLoaded = false;
content = CurrentSkin.GetDrawableComponent(new SkinnableTargetComponent(Target)) as SkinnableTargetComponentsContainer;
@ -39,8 +42,11 @@ namespace osu.Game.Skinning
{
AddInternal(wrapper);
components.AddRange(wrapper.Children.OfType<ISkinnableDrawable>());
ComponentsLoaded = true;
});
}
else
ComponentsLoaded = true;
}
/// <inheritdoc cref="ISkinnableTarget"/>

View File

@ -1,6 +1,7 @@
// 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.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions.IEnumerableExtensions;
@ -47,6 +48,8 @@ namespace osu.Game.Tests.Visual
LegacySkin.ResetDrawableTarget(t);
t.Reload();
}));
AddUntilStep("wait for components to load", () => this.ChildrenOfType<SkinnableTargetContainer>().All(t => t.ComponentsLoaded));
}
public class SkinProvidingPlayer : TestPlayer

View File

@ -350,7 +350,7 @@ namespace osu.Game.Tests.Visual
if (CurrentTime >= Length)
{
Stop();
RaiseCompleted();
// `RaiseCompleted` is not called here to prevent transitioning to the next song.
}
}
}

View File

@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Textures;
@ -13,16 +12,32 @@ namespace osu.Game.Users.Drawables
{
public class ClickableAvatar : Container
{
private const string default_tooltip_text = "view profile";
/// <summary>
/// Whether to open the user's profile when clicked.
/// </summary>
public readonly BindableBool OpenOnClick = new BindableBool(true);
public bool OpenOnClick
{
set => clickableArea.Enabled.Value = value;
}
/// <summary>
/// By default, the tooltip will show "view profile" as avatars are usually displayed next to a username.
/// Setting this to <c>true</c> exposes the username via tooltip for special cases where this is not true.
/// </summary>
public bool ShowUsernameTooltip
{
set => clickableArea.TooltipText = value ? (user?.Username ?? string.Empty) : default_tooltip_text;
}
private readonly User user;
[Resolved(CanBeNull = true)]
private OsuGame game { get; set; }
private readonly ClickableArea clickableArea;
/// <summary>
/// A clickable avatar for the specified user, with UI sounds included.
/// If <see cref="OpenOnClick"/> is <c>true</c>, clicking will open the user's profile.
@ -31,35 +46,35 @@ namespace osu.Game.Users.Drawables
public ClickableAvatar(User user = null)
{
this.user = user;
}
[BackgroundDependencyLoader]
private void load(LargeTextureStore textures)
{
ClickableArea clickableArea;
Add(clickableArea = new ClickableArea
{
RelativeSizeAxes = Axes.Both,
Action = openProfile
});
}
[BackgroundDependencyLoader]
private void load(LargeTextureStore textures)
{
LoadComponentAsync(new DrawableAvatar(user), clickableArea.Add);
clickableArea.Enabled.BindTo(OpenOnClick);
}
private void openProfile()
{
if (!OpenOnClick.Value)
return;
if (user?.Id > 1)
game?.ShowUser(user.Id);
}
private class ClickableArea : OsuClickableContainer
{
public override string TooltipText => Enabled.Value ? @"view profile" : null;
private string tooltip = default_tooltip_text;
public override string TooltipText
{
get => Enabled.Value ? tooltip : null;
set => tooltip = value;
}
protected override bool OnClick(ClickEvent e)
{

View File

@ -1,7 +1,6 @@
// 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;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
@ -45,33 +44,38 @@ namespace osu.Game.Users.Drawables
protected override double LoadDelay => 200;
/// <summary>
/// Whether to show a default guest representation on null user (as opposed to nothing).
/// </summary>
public bool ShowGuestOnNull = true;
private readonly bool openOnClick;
private readonly bool showUsernameTooltip;
private readonly bool showGuestOnNull;
/// <summary>
/// Whether to open the user's profile when clicked.
/// Construct a new UpdateableAvatar.
/// </summary>
public readonly BindableBool OpenOnClick = new BindableBool(true);
public UpdateableAvatar(User user = null)
/// <param name="user">The initial user to display.</param>
/// <param name="openOnClick">Whether to open the user's profile when clicked.</param>
/// <param name="showUsernameTooltip">Whether to show the username rather than "view profile" on the tooltip.</param>
/// <param name="showGuestOnNull">Whether to show a default guest representation on null user (as opposed to nothing).</param>
public UpdateableAvatar(User user = null, bool openOnClick = true, bool showUsernameTooltip = false, bool showGuestOnNull = true)
{
this.openOnClick = openOnClick;
this.showUsernameTooltip = showUsernameTooltip;
this.showGuestOnNull = showGuestOnNull;
User = user;
}
protected override Drawable CreateDrawable(User user)
{
if (user == null && !ShowGuestOnNull)
if (user == null && !showGuestOnNull)
return null;
var avatar = new ClickableAvatar(user)
{
OpenOnClick = openOnClick,
ShowUsernameTooltip = showUsernameTooltip,
RelativeSizeAxes = Axes.Both,
};
avatar.OpenOnClick.BindTo(OpenOnClick);
return avatar;
}
}

View File

@ -48,11 +48,7 @@ namespace osu.Game.Users
statusIcon.FinishTransforms();
}
protected UpdateableAvatar CreateAvatar() => new UpdateableAvatar
{
User = User,
OpenOnClick = { Value = false }
};
protected UpdateableAvatar CreateAvatar() => new UpdateableAvatar(User, false);
protected UpdateableFlag CreateFlag() => new UpdateableFlag(User.Country)
{

View File

@ -36,8 +36,8 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Realm" Version="10.2.0" />
<PackageReference Include="ppy.osu.Framework" Version="2021.614.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.611.0" />
<PackageReference Include="ppy.osu.Framework" Version="2021.616.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.614.0" />
<PackageReference Include="Sentry" Version="3.4.0" />
<PackageReference Include="SharpCompress" Version="0.28.2" />
<PackageReference Include="NUnit" Version="3.13.2" />