diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs index ca2ae74183..5c80183094 100644 --- a/osu.Game.Rulesets.Catch/CatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs @@ -185,15 +185,20 @@ namespace osu.Game.Rulesets.Catch public override Drawable CreateIcon() => new SpriteIcon { Icon = OsuIcon.RulesetCatch }; - protected override IEnumerable GetValidHitResults() + public override IEnumerable GetValidHitResults() { return new[] { HitResult.Great, + HitResult.Miss, HitResult.LargeTickHit, + HitResult.LargeTickMiss, HitResult.SmallTickHit, + HitResult.SmallTickMiss, HitResult.LargeBonus, + HitResult.IgnoreHit, + HitResult.IgnoreMiss, }; } diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index 4c43a78174..5707becafd 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -14,7 +14,6 @@ using osu.Game.Beatmaps; using osu.Game.Beatmaps.Legacy; using osu.Game.Configuration; using osu.Game.Graphics; -using osu.Game.LAsEzExtensions.Background; using osu.Game.LAsEzExtensions.Statistics; using osu.Game.Localisation; using osu.Game.Overlays.Settings; @@ -436,7 +435,7 @@ namespace osu.Game.Rulesets.Mania return (PlayfieldType)Enum.GetValues(typeof(PlayfieldType)).Cast().OrderDescending().First(v => variant >= v); } - protected override IEnumerable GetValidHitResults() + public override IEnumerable GetValidHitResults() { return new[] { @@ -445,13 +444,11 @@ namespace osu.Game.Rulesets.Mania HitResult.Good, HitResult.Ok, HitResult.Meh, - HitResult.IgnoreHit, - HitResult.IgnoreMiss, - HitResult.ComboBreak, + HitResult.Miss, HitResult.Poor, - - // HitResult.SmallBonus is used for awarding perfect bonus score but is not included here as - // it would be a bit redundant to show this to the user. + HitResult.IgnoreHit, + HitResult.ComboBreak, + HitResult.IgnoreMiss, }; } diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 0044df94f4..02468b3fb5 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -286,19 +286,24 @@ namespace osu.Game.Rulesets.Osu public override IRulesetConfigManager CreateConfig(SettingsStore? settings) => new OsuRulesetConfigManager(settings, RulesetInfo); - protected override IEnumerable GetValidHitResults() + public override IEnumerable GetValidHitResults() { return new[] { HitResult.Great, HitResult.Ok, HitResult.Meh, + HitResult.Miss, HitResult.LargeTickHit, + HitResult.LargeTickMiss, HitResult.SmallTickHit, + HitResult.SmallTickMiss, HitResult.SliderTailHit, HitResult.SmallBonus, HitResult.LargeBonus, + HitResult.IgnoreHit, + HitResult.IgnoreMiss, }; } diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index 17eab78e6a..64a28d5186 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -231,15 +231,18 @@ namespace osu.Game.Rulesets.Taiko public override RulesetSettingsSubsection CreateSettings() => new TaikoSettingsSubsection(this); - protected override IEnumerable GetValidHitResults() + public override IEnumerable GetValidHitResults() { return new[] { HitResult.Great, HitResult.Ok, + HitResult.Miss, HitResult.SmallBonus, HitResult.LargeBonus, + HitResult.IgnoreHit, + HitResult.IgnoreMiss, }; } diff --git a/osu.Game.Tests/Database/RulesetStoreTests.cs b/osu.Game.Tests/Database/RulesetStoreTests.cs index 29aec73770..7ef2429491 100644 --- a/osu.Game.Tests/Database/RulesetStoreTests.cs +++ b/osu.Game.Tests/Database/RulesetStoreTests.cs @@ -7,6 +7,7 @@ using System.Linq; using NUnit.Framework; using osu.Game.Beatmaps; using osu.Game.Rulesets; +using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; @@ -116,6 +117,69 @@ namespace osu.Game.Tests.Database }); } + [Test] + public void TestFakedRulesetIdIsDetected() + { + RunTestWithRealm((realm, storage) => + { + LoadTestRuleset.HasImplementations = true; + LoadTestRuleset.Version = Ruleset.CURRENT_RULESET_API_VERSION; + + var ruleset = new LoadTestRuleset(); + string rulesetShortName = ruleset.RulesetInfo.ShortName; + + realm.Write(r => r.Add(new RulesetInfo(rulesetShortName, ruleset.RulesetInfo.Name, ruleset.RulesetInfo.InstantiationInfo, 0) + { + Available = true, + })); + + Assert.That(realm.Run(r => r.Find(rulesetShortName)!.Available), Is.True); + + // Availability is updated on construction of a RealmRulesetStore + using var _ = new RealmRulesetStore(realm, storage); + + Assert.That(realm.Run(r => r.Find(rulesetShortName)!.Available), Is.False); + }); + } + + [Test] + public void TestMultipleRulesetWithSameOnlineIdsAreDetected() + { + RunTestWithRealm((realm, storage) => + { + LoadTestRuleset.HasImplementations = true; + LoadTestRuleset.Version = Ruleset.CURRENT_RULESET_API_VERSION; + LoadTestRuleset.OnlineID = 2; + + var first = new LoadTestRuleset(); + var second = new CatchRuleset(); + + realm.Write(r => r.Add(new RulesetInfo(first.ShortName, first.RulesetInfo.Name, first.RulesetInfo.InstantiationInfo, first.RulesetInfo.OnlineID) + { + Available = true, + })); + realm.Write(r => r.Add(new RulesetInfo(second.ShortName, second.RulesetInfo.Name, second.RulesetInfo.InstantiationInfo, second.RulesetInfo.OnlineID) + { + Available = true, + })); + + Assert.That(realm.Run(r => r.Find(first.ShortName)!.Available), Is.True); + Assert.That(realm.Run(r => r.Find(second.ShortName)!.Available), Is.True); + + // Availability is updated on construction of a RealmRulesetStore + using var _ = new RealmRulesetStore(realm, storage); + + Assert.That(realm.Run(r => r.Find(first.ShortName)!.Available), Is.False); + Assert.That(realm.Run(r => r.Find(second.ShortName)!.Available), Is.False); + + realm.Write(r => r.Remove(r.Find(first.ShortName)!)); + + using var __ = new RealmRulesetStore(realm, storage); + + Assert.That(realm.Run(r => r.Find(second.ShortName)!.Available), Is.True); + }); + } + private class LoadTestRuleset : Ruleset { public override string RulesetAPIVersionSupported => Version; @@ -124,6 +188,13 @@ namespace osu.Game.Tests.Database public static string Version { get; set; } = CURRENT_RULESET_API_VERSION; + public static int OnlineID { get; set; } = -1; + + public LoadTestRuleset() + { + RulesetInfo.OnlineID = OnlineID; + } + public override IEnumerable GetModsFor(ModType type) { if (!HasImplementations) diff --git a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs index f45422e0c4..9b87ce4bb4 100644 --- a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs +++ b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs @@ -526,7 +526,7 @@ namespace osu.Game.Tests.Rulesets.Scoring // ReSharper disable once MemberHidesStaticFromOuterClass private class TestRuleset : Ruleset { - protected override IEnumerable GetValidHitResults() => new[] { HitResult.Great }; + public override IEnumerable GetValidHitResults() => new[] { HitResult.Great }; public override IEnumerable GetModsFor(ModType type) => throw new NotImplementedException(); diff --git a/osu.Game.Tests/Visual/Online/TestSceneClickableTeamFlag.cs b/osu.Game.Tests/Visual/Online/TestSceneClickableTeamFlag.cs new file mode 100644 index 0000000000..525cc97581 --- /dev/null +++ b/osu.Game.Tests/Visual/Online/TestSceneClickableTeamFlag.cs @@ -0,0 +1,61 @@ +// Copyright (c) ppy Pty Ltd . 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.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; +using osu.Game.Graphics.Cursor; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Users.Drawables; +using osuTK; + +namespace osu.Game.Tests.Visual.Online +{ + public partial class TestSceneClickableTeamFlag : OsuManualInputManagerTestScene + { + [SetUpSteps] + public void SetUp() + { + AddStep("create flags", () => + { + Child = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Spacing = new Vector2(10f), + Children = new[] + { + new ClickableTeamFlag( + new APITeam + { + Id = 1, + Name = "Collective Wangs", + ShortName = "WANG", + }, showTooltipOnHover: false) { Width = 300, Height = 150 }, + new ClickableTeamFlag( + new APITeam + { + Id = 2, + Name = "mom?", + ShortName = "MOM", + FlagUrl = "https://assets.ppy.sh/teams/flag/1/b46fb10dbfd8a35dc50e6c00296c0dc6172dffc3ed3d3a4b379277ba498399fe.png", + }, showTooltipOnHover: true) { Width = 300, Height = 150 }, + }, + }; + }); + } + + [Test] + public void TestHover() + { + AddStep("hover flag with no tooltip", () => InputManager.MoveMouseTo(this.ChildrenOfType().ElementAt(0))); + AddWaitStep("wait", 3); + AddAssert("tooltip is not visible", () => this.ChildrenOfType().FirstOrDefault()?.State.Value, () => Is.EqualTo(Visibility.Hidden)); + AddStep("hover flag with tooltip", () => InputManager.MoveMouseTo(this.ChildrenOfType().ElementAt(1))); + AddUntilStep("wait for tooltip to show", () => this.ChildrenOfType().FirstOrDefault()?.State.Value, () => Is.EqualTo(Visibility.Visible)); + } + } +} diff --git a/osu.Game.Tests/Visual/Online/TestSceneUpdateableTeamFlag.cs b/osu.Game.Tests/Visual/Online/TestSceneUpdateableTeamFlag.cs new file mode 100644 index 0000000000..1813ede830 --- /dev/null +++ b/osu.Game.Tests/Visual/Online/TestSceneUpdateableTeamFlag.cs @@ -0,0 +1,43 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Users.Drawables; + +namespace osu.Game.Tests.Visual.Online +{ + [TestFixture] + public partial class TestSceneUpdateableTeamFlag : OsuTestScene + { + [Test] + public void TestHideOnNull() + { + UpdateableTeamFlag flag = null!; + + AddStep("create flag with team", () => Child = flag = new UpdateableTeamFlag(createTeam(), hideOnNull: true) { Width = 300, Height = 150 }); + AddAssert("flag is present", () => flag.IsPresent, () => Is.True); + AddStep("set team to null", () => flag.Team = null); + AddAssert("flag is not present", () => flag.IsPresent, () => Is.False); + } + + [Test] + public void DontHideOnNull() + { + UpdateableTeamFlag flag = null!; + + AddStep("create flag with team", () => Child = flag = new UpdateableTeamFlag(createTeam(), hideOnNull: false) { Width = 300, Height = 150 }); + AddAssert("flag is present", () => flag.IsPresent, () => Is.True); + AddStep("set team to null", () => flag.Team = null); + AddAssert("flag is present", () => flag.IsPresent, () => Is.True); + } + + private static APITeam createTeam() => new APITeam + { + Id = 2, + Name = "mom?", + ShortName = "MOM", + FlagUrl = @"https://assets.ppy.sh/teams/flag/1/b46fb10dbfd8a35dc50e6c00296c0dc6172dffc3ed3d3a4b379277ba498399fe.png", + }; + } +} diff --git a/osu.Game/Database/StandardisedScoreMigrationTools.cs b/osu.Game/Database/StandardisedScoreMigrationTools.cs index 15e3da3c19..204198a410 100644 --- a/osu.Game/Database/StandardisedScoreMigrationTools.cs +++ b/osu.Game/Database/StandardisedScoreMigrationTools.cs @@ -51,7 +51,7 @@ namespace osu.Game.Database var beatmap = new Beatmap(); - HitResult maxRulesetJudgement = ruleset.GetHitResults().First().result; + HitResult maxRulesetJudgement = ruleset.GetHitResultsForDisplay().First().result; // This is a list of all results, ordered from best to worst. // We are constructing a "best possible" score from the statistics provided because it's the best we can do. diff --git a/osu.Game/Online/API/Requests/Responses/APITeam.cs b/osu.Game/Online/API/Requests/Responses/APITeam.cs index b4fcc2d26e..1d4aa17cda 100644 --- a/osu.Game/Online/API/Requests/Responses/APITeam.cs +++ b/osu.Game/Online/API/Requests/Responses/APITeam.cs @@ -18,6 +18,6 @@ namespace osu.Game.Online.API.Requests.Responses public string ShortName { get; set; } = string.Empty; [JsonProperty(@"flag_url")] - public string FlagUrl = string.Empty; + public string? FlagUrl = string.Empty; } } diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs index 0c8943ba7d..1e62b23780 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs @@ -106,7 +106,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores var ruleset = scores.First().Ruleset.CreateInstance(); - foreach (var resultGroup in ruleset.GetHitResults().GroupBy(r => r.displayName)) + foreach (var resultGroup in ruleset.GetHitResultsForDisplay().GroupBy(r => r.displayName)) { if (!resultGroup.Any(r => allScoreStatistics.Contains(r.result))) continue; diff --git a/osu.Game/Rulesets/RealmRulesetStore.cs b/osu.Game/Rulesets/RealmRulesetStore.cs index b93110426b..52ea5f7f4b 100644 --- a/osu.Game/Rulesets/RealmRulesetStore.cs +++ b/osu.Game/Rulesets/RealmRulesetStore.cs @@ -93,6 +93,12 @@ namespace osu.Game.Rulesets $"Ruleset API version is too old (was {instance.RulesetAPIVersionSupported}, expected {Ruleset.CURRENT_RULESET_API_VERSION})"); } + if (r.OnlineID != instanceInfo.OnlineID) + throw new InvalidOperationException($@"Online ID mismatch for ruleset {r.ShortName}: database has {r.OnlineID}, constructed instance has {instanceInfo.OnlineID}"); + + if (r.OnlineID > 0 && rulesets.Any(otherRuleset => otherRuleset.ShortName != r.ShortName && otherRuleset.OnlineID == r.OnlineID)) + throw new InvalidOperationException($@"Ruleset {r.ShortName} shares online ID {r.OnlineID} with another ruleset"); + // If a ruleset isn't up-to-date with the API, it could cause a crash at an arbitrary point of execution. // To eagerly handle cases of missing implementations, enumerate all types here and mark as non-available on throw. resolvedType.Assembly.GetTypes(); diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index c2562fbb3d..9a4883abdd 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -13,6 +13,7 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Bindings; using osu.Framework.IO.Stores; using osu.Framework.Localisation; +using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Legacy; using osu.Game.Configuration; @@ -343,13 +344,17 @@ namespace osu.Game.Rulesets public virtual StatisticItem[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => Array.Empty(); /// - /// Get all valid s for this ruleset. - /// Generally used for results display purposes, where it can't be determined if zero-count means the user has not achieved any or the type is not used by this ruleset. + /// Get all s for this ruleset which are important enough to displayed to the end user. + /// Used for results display purposes, where it can't be determined if zero-count means the user has not achieved any or the type is not used by this ruleset. /// + /// + /// is implicitly included. Special types like are not returned by this method. + /// Values are returned as ordered by . + /// /// - /// All valid s along with a display-friendly name. + /// All relevant s along with a display-friendly name. /// - public IEnumerable<(HitResult result, LocalisableString displayName)> GetHitResults() + public IEnumerable<(HitResult result, LocalisableString displayName)> GetHitResultsForDisplay() { var validResults = GetValidHitResults(); @@ -362,6 +367,7 @@ namespace osu.Game.Rulesets case HitResult.None: case HitResult.IgnoreHit: case HitResult.IgnoreMiss: + case HitResult.ComboBreak: // display is handled as a completion count with corresponding "hit" type. case HitResult.LargeTickMiss: case HitResult.SmallTickMiss: @@ -375,12 +381,10 @@ namespace osu.Game.Rulesets /// /// Get all valid s for this ruleset. - /// Generally used for results display purposes, where it can't be determined if zero-count means the user has not achieved any or the type is not used by this ruleset. + /// Used for strict validation purposes. The ruleset should return ALL applicable types here + /// (except and obsolete types). /// - /// - /// is implicitly included. Special types like are ignored even when specified. - /// - protected virtual IEnumerable GetValidHitResults() => EnumExtensions.GetValuesInOrder(); + public virtual IEnumerable GetValidHitResults() => EnumExtensions.GetValuesInOrder(); /// /// Get a display friendly name for the specified result type. diff --git a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs index 393df65cc8..05070d96ec 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs @@ -211,7 +211,7 @@ namespace osu.Game.Scoring.Legacy var scoreProcessor = rulesetInstance.CreateScoreProcessor(); // Populate the maximum statistics. - HitResult maxBasicResult = rulesetInstance.GetHitResults() + HitResult maxBasicResult = rulesetInstance.GetHitResultsForDisplay() .Select(h => h.result) .Where(h => h.IsBasic()).MaxBy(scoreProcessor.GetBaseScoreForResult); diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index f12e79aba2..56cf38c0b1 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -402,7 +402,7 @@ namespace osu.Game.Scoring public IEnumerable GetStatisticsForDisplay() { - foreach (var r in Ruleset.CreateInstance().GetHitResults()) + foreach (var r in Ruleset.CreateInstance().GetHitResultsForDisplay()) { int value = Statistics.GetValueOrDefault(r.result); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs index 19868082fa..c9804a1bf8 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs @@ -245,7 +245,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants userFlag.CountryCode = user?.CountryCode ?? default; teamFlagContainer.Child = new UpdateableTeamFlag(user?.Team) { - Size = new Vector2(40, 20) + Size = new Vector2(40, 20), }; username.Text = user?.Username ?? string.Empty; diff --git a/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCountController.cs b/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCountController.cs index c00cb3487b..a979ed03fd 100644 --- a/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCountController.cs +++ b/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCountController.cs @@ -32,7 +32,7 @@ namespace osu.Game.Screens.Play.HUD.JudgementCounter { // Due to weirdness in judgements, some results have the same name and should be aggregated for display purposes. // There's only one case of this right now ("slider end"). - foreach (var group in ruleset.Value.CreateInstance().GetHitResults().GroupBy(r => r.displayName)) + foreach (var group in ruleset.Value.CreateInstance().GetHitResultsForDisplay().GroupBy(r => r.displayName)) { var judgementCount = new JudgementCount { diff --git a/osu.Game/Users/Drawables/ClickableTeamFlag.cs b/osu.Game/Users/Drawables/ClickableTeamFlag.cs new file mode 100644 index 0000000000..69d592054e --- /dev/null +++ b/osu.Game/Users/Drawables/ClickableTeamFlag.cs @@ -0,0 +1,52 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests.Responses; + +namespace osu.Game.Users.Drawables +{ + public partial class ClickableTeamFlag : OsuClickableContainer + { + private readonly APITeam? team; + + [Resolved] + private OsuGame? game { get; set; } + + [Resolved] + private IAPIProvider api { get; set; } = null!; + + /// + /// A clickable flag component for the specified team, with UI sounds and a tooltip. + /// + /// The team. A null value will show a placeholder background. + /// If set to true, the team's name is displayed in the tooltip. + public ClickableTeamFlag(APITeam? team, bool showTooltipOnHover = true) + { + this.team = team; + + if (team == null) + return; + + Action = openProfile; + + if (showTooltipOnHover) + TooltipText = team.Name; + } + + [BackgroundDependencyLoader] + private void load() + { + LoadComponentAsync(new DrawableTeamFlag(team) { RelativeSizeAxes = Axes.Both }, Add); + } + + private void openProfile() + { + if (team != null) + game?.OpenUrlExternally($@"{api.Endpoints.WebsiteUrl}/teams/{team.Id}"); + } + } +} diff --git a/osu.Game/Users/Drawables/DrawableTeamFlag.cs b/osu.Game/Users/Drawables/DrawableTeamFlag.cs new file mode 100644 index 0000000000..27b5f447a5 --- /dev/null +++ b/osu.Game/Users/Drawables/DrawableTeamFlag.cs @@ -0,0 +1,53 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; +using osu.Game.Online.API.Requests.Responses; + +namespace osu.Game.Users.Drawables +{ + [LongRunningLoad] + public partial class DrawableTeamFlag : CompositeDrawable + { + private readonly APITeam? team; + + private readonly Sprite sprite; + + /// + /// A simple, non-interactable flag sprite for the specified user. + /// + /// The team. A null value will show a placeholder background. + public DrawableTeamFlag(APITeam? team) + { + this.team = team; + + InternalChildren = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Colour4.FromHex("333"), + }, + sprite = new Sprite + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + FillMode = FillMode.Fit, + } + }; + } + + [BackgroundDependencyLoader] + private void load(LargeTextureStore textures) + { + if (team != null) + sprite.Texture = textures.Get(team.FlagUrl); + } + } +} diff --git a/osu.Game/Users/Drawables/UpdateableTeamFlag.cs b/osu.Game/Users/Drawables/UpdateableTeamFlag.cs index 517eb589b9..adaa192451 100644 --- a/osu.Game/Users/Drawables/UpdateableTeamFlag.cs +++ b/osu.Game/Users/Drawables/UpdateableTeamFlag.cs @@ -1,17 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Cursor; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.Textures; -using osu.Framework.Input.Events; -using osu.Framework.Localisation; -using osu.Game.Graphics.UserInterface; -using osu.Game.Online.API; +using osu.Framework.Graphics.Effects; using osu.Game.Online.API.Requests.Responses; namespace osu.Game.Users.Drawables @@ -31,10 +23,52 @@ namespace osu.Game.Users.Drawables } } + public new bool Masking + { + get => base.Masking; + set => base.Masking = value; + } + + private bool useDefaultRadius = true; + + public new float CornerRadius + { + get => base.CornerRadius; + set + { + useDefaultRadius = false; + base.CornerRadius = value; + } + } + + public new EdgeEffectParameters EdgeEffect + { + get => base.EdgeEffect; + set => base.EdgeEffect = value; + } + protected override double LoadDelay => 200; - public UpdateableTeamFlag(APITeam? team = null) + private readonly bool isInteractive; + private readonly bool hideOnNull; + private readonly bool showTooltipOnHover; + + /// + /// Construct a new UpdateableTeamFlag. + /// + /// The initial team to display. + /// If set to true, hover/click sounds will play and clicking the flag will open the team's profile. + /// + /// If set to true, the team's name is displayed in the tooltip. + /// Only has an effect if is true. + /// + /// Whether to hide the flag when the provided team is null. + public UpdateableTeamFlag(APITeam? team = null, bool isInteractive = true, bool hideOnNull = true, bool showTooltipOnHover = true) { + this.isInteractive = isInteractive; + this.hideOnNull = hideOnNull; + this.showTooltipOnHover = showTooltipOnHover; + Team = team; Masking = true; @@ -42,69 +76,33 @@ namespace osu.Game.Users.Drawables protected override Drawable? CreateDrawable(APITeam? team) { - if (team == null) + if (team == null && hideOnNull) return Empty(); - return new TeamFlag(team) { RelativeSizeAxes = Axes.Both }; - } + if (isInteractive) + { + return new ClickableTeamFlag(team, showTooltipOnHover) + { + RelativeSizeAxes = Axes.Both, + }; + } - // Generally we just want team flags to disappear if the user doesn't have one. - // This also handles fill flow cases and avoids spacing being added for non-displaying flags. - public override bool IsPresent => base.IsPresent && Team != null; + return new DrawableTeamFlag(team) + { + RelativeSizeAxes = Axes.Both, + }; + } protected override void Update() { base.Update(); - CornerRadius = DrawHeight / 8; + if (useDefaultRadius) + base.CornerRadius = DrawHeight / 8; } - [LongRunningLoad] - public partial class TeamFlag : CompositeDrawable, IHasTooltip - { - private readonly APITeam team; - - public LocalisableString TooltipText { get; } - - [Resolved] - private OsuGame? game { get; set; } - - [Resolved] - private IAPIProvider api { get; set; } = null!; - - public TeamFlag(APITeam team) - { - this.team = team; - TooltipText = team.Name; - } - - [BackgroundDependencyLoader] - private void load(LargeTextureStore textures) - { - InternalChildren = new Drawable[] - { - new HoverClickSounds(), - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Colour4.FromHex("333"), - }, - new Sprite - { - RelativeSizeAxes = Axes.Both, - Texture = textures.Get(team.FlagUrl), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - FillMode = FillMode.Fit, - } - }; - } - - protected override bool OnClick(ClickEvent e) - { - game?.OpenUrlExternally($"{api.Endpoints.WebsiteUrl}/teams/{team.Id}"); - return true; - } - } + // Generally we just want team flags to disappear if the user doesn't have one. + // This also handles fill flow cases and avoids spacing being added for non-displaying flags. + public override bool IsPresent => base.IsPresent && (Team != null || !hideOnNull); } } diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 085bbf501e..6647f09f8f 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -35,8 +35,6 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - -