Merge branch 'master' into last-lang

This commit is contained in:
smoogipoo
2021-06-18 23:30:59 +09:00
86 changed files with 1111 additions and 703 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

@ -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

@ -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

@ -190,29 +190,29 @@ namespace osu.Game
AddFont(Resources, @"Fonts/osuFont");
AddFont(Resources, @"Fonts/Torus-Regular");
AddFont(Resources, @"Fonts/Torus-Light");
AddFont(Resources, @"Fonts/Torus-SemiBold");
AddFont(Resources, @"Fonts/Torus-Bold");
AddFont(Resources, @"Fonts/Torus/Torus-Regular");
AddFont(Resources, @"Fonts/Torus/Torus-Light");
AddFont(Resources, @"Fonts/Torus/Torus-SemiBold");
AddFont(Resources, @"Fonts/Torus/Torus-Bold");
AddFont(Resources, @"Fonts/Inter-Regular");
AddFont(Resources, @"Fonts/Inter-RegularItalic");
AddFont(Resources, @"Fonts/Inter-Light");
AddFont(Resources, @"Fonts/Inter-LightItalic");
AddFont(Resources, @"Fonts/Inter-SemiBold");
AddFont(Resources, @"Fonts/Inter-SemiBoldItalic");
AddFont(Resources, @"Fonts/Inter-Bold");
AddFont(Resources, @"Fonts/Inter-BoldItalic");
AddFont(Resources, @"Fonts/Inter/Inter-Regular");
AddFont(Resources, @"Fonts/Inter/Inter-RegularItalic");
AddFont(Resources, @"Fonts/Inter/Inter-Light");
AddFont(Resources, @"Fonts/Inter/Inter-LightItalic");
AddFont(Resources, @"Fonts/Inter/Inter-SemiBold");
AddFont(Resources, @"Fonts/Inter/Inter-SemiBoldItalic");
AddFont(Resources, @"Fonts/Inter/Inter-Bold");
AddFont(Resources, @"Fonts/Inter/Inter-BoldItalic");
AddFont(Resources, @"Fonts/Noto-Basic");
AddFont(Resources, @"Fonts/Noto-Hangul");
AddFont(Resources, @"Fonts/Noto-CJK-Basic");
AddFont(Resources, @"Fonts/Noto-CJK-Compatibility");
AddFont(Resources, @"Fonts/Noto-Thai");
AddFont(Resources, @"Fonts/Noto/Noto-Basic");
AddFont(Resources, @"Fonts/Noto/Noto-Hangul");
AddFont(Resources, @"Fonts/Noto/Noto-CJK-Basic");
AddFont(Resources, @"Fonts/Noto/Noto-CJK-Compatibility");
AddFont(Resources, @"Fonts/Noto/Noto-Thai");
AddFont(Resources, @"Fonts/Venera-Light");
AddFont(Resources, @"Fonts/Venera-Bold");
AddFont(Resources, @"Fonts/Venera-Black");
AddFont(Resources, @"Fonts/Venera/Venera-Light");
AddFont(Resources, @"Fonts/Venera/Venera-Bold");
AddFont(Resources, @"Fonts/Venera/Venera-Black");
Audio.Samples.PlaybackConcurrency = SAMPLE_CONCURRENCY;

View File

@ -1,9 +1,9 @@
// 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 System.Linq;
using osu.Framework.Extensions;
using osu.Framework.Localisation;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Scoring;
@ -33,38 +33,7 @@ namespace osu.Game.Overlays.BeatmapListing
{
}
protected override LocalisableString LabelFor(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 ArgumentException("Unsupported value.", nameof(value));
}
}
protected override LocalisableString LabelFor(ScoreRank value) => value.GetLocalisableDescription();
}
}
}

View File

@ -67,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 LocalisableString 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

@ -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

@ -12,7 +12,7 @@ namespace osu.Game.Overlays.Mods
base.OnModSelected(mod);
foreach (var section in ModSectionsContainer.Children)
section.DeselectTypes(mod.IncompatibleMods, true);
section.DeselectTypes(mod.IncompatibleMods, true, mod);
}
}
}

View File

@ -159,12 +159,16 @@ namespace osu.Game.Overlays.Mods
/// </summary>
/// <param name="modTypes">The types of <see cref="Mod"/>s which should be deselected.</param>
/// <param name="immediate">Whether the deselection should happen immediately. Should only be used when required to ensure correct selection flow.</param>
public void DeselectTypes(IEnumerable<Type> modTypes, bool immediate = false)
/// <param name="newSelection">If this deselection is triggered by a user selection, this should contain the newly selected type. This type will never be deselected, even if it matches one provided in <paramref name="modTypes"/>.</param>
public void DeselectTypes(IEnumerable<Type> modTypes, bool immediate = false, Mod newSelection = null)
{
foreach (var button in Buttons)
{
if (button.SelectedMod == null) continue;
if (button.SelectedMod == newSelection)
continue;
foreach (var type in modTypes)
{
if (type.IsInstanceOfType(button.SelectedMod))

View File

@ -144,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

@ -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

@ -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

@ -96,13 +96,25 @@ namespace osu.Game.Rulesets
context.SaveChanges();
// add any other modes
var existingRulesets = context.RulesetInfo.ToList();
// add any other rulesets which have assemblies present but are not yet in the database.
foreach (var r in instances.Where(r => !(r is ILegacyRuleset)))
{
if (existingRulesets.FirstOrDefault(ri => ri.InstantiationInfo.Equals(r.RulesetInfo.InstantiationInfo, StringComparison.Ordinal)) == null)
context.RulesetInfo.Add(r.RulesetInfo);
{
var existingSameShortName = existingRulesets.FirstOrDefault(ri => ri.ShortName == r.RulesetInfo.ShortName);
if (existingSameShortName != null)
{
// even if a matching InstantiationInfo was not found, there may be an existing ruleset with the same ShortName.
// this generally means the user or ruleset provider has renamed their dll but the underlying ruleset is *likely* the same one.
// in such cases, update the instantiation info of the existing entry to point to the new one.
existingSameShortName.InstantiationInfo = r.RulesetInfo.InstantiationInfo;
}
else
context.RulesetInfo.Add(r.RulesetInfo);
}
}
context.SaveChanges();

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

@ -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

@ -4,6 +4,7 @@
using System;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Screens;
@ -54,9 +55,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
return new PlaylistsResultsScreen(score, RoomId.Value.Value, PlaylistItem, true);
}
protected override void PrepareScoreForResults()
protected override async Task PrepareScoreForResultsAsync(Score score)
{
base.PrepareScoreForResults();
await base.PrepareScoreForResultsAsync(score).ConfigureAwait(false);
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,12 +295,12 @@ namespace osu.Game.Screens.Play
DimmableStoryboard.HasStoryboardEnded.ValueChanged += storyboardEnded =>
{
if (storyboardEnded.NewValue && completionProgressDelegate == null)
updateCompletionState();
if (storyboardEnded.NewValue)
progressToResults(true);
};
// Bind the judgement processors to ourselves
ScoreProcessor.HasCompleted.BindValueChanged(_ => updateCompletionState());
ScoreProcessor.HasCompleted.BindValueChanged(scoreCompletionChanged);
HealthProcessor.Failed += onFail;
foreach (var mod in Mods.Value.OfType<IApplicableToScoreProcessor>())
@ -380,7 +374,7 @@ namespace osu.Game.Screens.Play
},
skipOutroOverlay = new SkipOverlay(Beatmap.Value.Storyboard.LatestEventTime ?? 0)
{
RequestSkip = () => updateCompletionState(true),
RequestSkip = () => progressToResults(false),
Alpha = 0
},
FailOverlay = new FailOverlay
@ -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,98 +624,141 @@ 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="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>
/// Handles changes in player state which may progress the completion of gameplay / this screen's lifetime.
/// </summary>
/// <param name="skipStoryboardOutro">If in a state where a storyboard outro is to be played, offers the choice of skipping beyond it.</param>
/// <exception cref="InvalidOperationException">Thrown if this method is called more than once without changing state.</exception>
private void updateCompletionState(bool skipStoryboardOutro = false)
private void scoreCompletionChanged(ValueChangedEvent<bool> completed)
{
// 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;
if (!ScoreProcessor.HasCompleted.Value)
// 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 (!completed.NewValue)
{
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");
// 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;
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;
});
if (skipStoryboardOutro)
{
scheduleCompletion();
if (!Configuration.ShowResults)
return;
}
prepareScoreForDisplayTask ??= Task.Run(prepareScoreForResults);
bool storyboardHasOutro = DimmableStoryboard.ContentDisplayed && !DimmableStoryboard.HasStoryboardEnded.Value;
if (storyboardHasOutro)
{
// if the current beatmap has a storyboard, the progression to results will be handled by the storyboard ending
// or the user pressing the skip outro button.
skipOutroOverlay.Show();
return;
}
using (BeginDelayedSequence(RESULTS_DISPLAY_DELAY))
scheduleCompletion();
progressToResults(true);
}
private void scheduleCompletion() => completionProgressDelegate = Schedule(() =>
/// <summary>
/// Asynchronously run score preparation operations (database import, online submission etc.).
/// </summary>
/// <returns>The final score.</returns>
private async Task<ScoreInfo> prepareScoreForResults()
{
if (!prepareScoreForDisplayTask.IsCompleted)
try
{
scheduleCompletion();
return;
await PrepareScoreForResultsAsync(Score).ConfigureAwait(false);
}
catch (Exception ex)
{
Logger.Error(ex, @"Score preparation failed!");
}
// screen may be in the exiting transition phase.
if (this.IsCurrentScreen())
try
{
await ImportScore(Score).ConfigureAwait(false);
}
catch (Exception ex)
{
Logger.Error(ex, @"Score import failed!");
}
return Score.ScoreInfo;
}
/// <summary>
/// Queue the results screen for display.
/// </summary>
/// <remarks>
/// A final display will only occur once all work is completed in <see cref="PrepareScoreForResultsAsync"/>. This means that even after calling this method, the results screen will never be shown until <see cref="JudgementProcessor.HasCompleted">ScoreProcessor.HasCompleted</see> becomes <see langword="true"/>.
///
/// Calling this method multiple times will have no effect.
/// </remarks>
/// <param name="withDelay">Whether a minimum delay (<see cref="RESULTS_DISPLAY_DELAY"/>) should be added before the screen is displayed.</param>
private void progressToResults(bool withDelay)
{
if (resultsDisplayDelegate != null)
// Note that if progressToResults is called one withDelay=true and then withDelay=false, this no-delay timing will not be
// accounted for. shouldn't be a huge concern (a user pressing the skip button after a results progression has already been queued
// may take x00 more milliseconds than expected in the very rare edge case).
//
// If required we can handle this more correctly by rescheduling here.
return;
double delay = withDelay ? RESULTS_DISPLAY_DELAY : 0;
resultsDisplayDelegate = new ScheduledDelegate(() =>
{
if (prepareScoreForDisplayTask?.IsCompleted != true)
// If the asynchronous preparation has not completed, keep repeating this delegate.
return;
resultsDisplayDelegate?.Cancel();
if (!this.IsCurrentScreen())
// This player instance may already be in the process of exiting.
return;
this.Push(CreateResults(prepareScoreForDisplayTask.Result));
});
}, Time.Current + delay, 50);
Scheduler.Add(resultsDisplayDelegate);
}
protected override bool OnScroll(ScrollEvent e) => mouseWheelDisabled.Value && !GameplayClockContainer.IsPaused.Value;
@ -915,13 +956,6 @@ namespace osu.Game.Screens.Play
{
screenSuspension?.Expire();
if (completionProgressDelegate != null && !completionProgressDelegate.Cancelled && !completionProgressDelegate.Completed)
{
// proceed to result screen if beatmap already finished playing
completionProgressDelegate.RunTask();
return true;
}
// EndPlaying() is typically called from ReplayRecorder.Dispose(). Disposal is currently asynchronous.
// To resolve test failures, forcefully end playing synchronously when this screen exits.
// Todo: Replace this with a more permanent solution once osu-framework has a synchronous cleanup method.
@ -984,7 +1018,13 @@ namespace osu.Game.Screens.Play
/// </summary>
/// <param name="score">The <see cref="Scoring.Score"/> to prepare.</param>
/// <returns>A task that prepares the provided score. On completion, the score is assumed to be ready for display.</returns>
protected virtual Task PrepareScoreForResultsAsync(Score score) => Task.CompletedTask;
protected virtual Task PrepareScoreForResultsAsync(Score score)
{
// perform one final population to ensure everything is up-to-date.
ScoreProcessor.PopulateScore(score.ScoreInfo);
return Task.CompletedTask;
}
/// <summary>
/// Creates the <see cref="ResultsScreen"/> for a <see cref="ScoreInfo"/>.

View File

@ -25,7 +25,7 @@ namespace osu.Game.Storyboards.Drawables
/// </summary>
public IBindable<bool> HasStoryboardEnded => hasStoryboardEnded;
private readonly BindableBool hasStoryboardEnded = new BindableBool();
private readonly BindableBool hasStoryboardEnded = new BindableBool(true);
protected override Container<DrawableStoryboardLayer> Content { get; }

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

@ -60,6 +60,9 @@ namespace osu.Game.Utils
{
foreach (var invalid in combination.Where(m => type.IsInstanceOfType(m)))
{
if (invalid == mod)
continue;
invalidMods ??= new List<Mod>();
invalidMods.Add(invalid);
}

View File

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