From 63e0768207752f674a44ada84acfc61be63f508c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 22 Mar 2023 14:44:52 +0900 Subject: [PATCH 001/107] Show count of beatmaps in collections in manage dialog --- .../Collections/DrawableCollectionListItem.cs | 65 +++++++++++++++++-- 1 file changed, 59 insertions(+), 6 deletions(-) diff --git a/osu.Game/Collections/DrawableCollectionListItem.cs b/osu.Game/Collections/DrawableCollectionListItem.cs index 23156b1ad5..efeb066869 100644 --- a/osu.Game/Collections/DrawableCollectionListItem.cs +++ b/osu.Game/Collections/DrawableCollectionListItem.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -13,6 +14,7 @@ using osu.Framework.Input.Events; using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; using osuTK; @@ -25,7 +27,7 @@ namespace osu.Game.Collections /// public partial class DrawableCollectionListItem : OsuRearrangeableListItem> { - private const float item_height = 35; + private const float item_height = 45; private const float button_width = item_height * 0.75f; /// @@ -81,12 +83,10 @@ namespace osu.Game.Collections Padding = new MarginPadding { Right = collection.IsManaged ? button_width : 0 }, Children = new Drawable[] { - textBox = new ItemTextBox + textBox = new ItemTextBox(collection) { - RelativeSizeAxes = Axes.Both, - Size = Vector2.One, - CornerRadius = item_height / 2, - PlaceholderText = collection.IsManaged ? string.Empty : "Create a new collection" + RelativeSizeAxes = Axes.X, + Height = item_height }, } }, @@ -117,11 +117,64 @@ namespace osu.Game.Collections { protected override float LeftRightPadding => item_height / 2; + private const float count_text_size = 12; + + [Resolved] + private RealmAccess realm { get; set; } = null!; + + private readonly Live collection; + + private OsuSpriteText countText = null!; + + private IDisposable? itemCountSubscription; + + public ItemTextBox(Live collection) + { + this.collection = collection; + + CornerRadius = item_height / 2; + } + [BackgroundDependencyLoader] private void load(OsuColour colours) { BackgroundUnfocused = colours.GreySeaFoamDarker.Darken(0.5f); BackgroundFocused = colours.GreySeaFoam; + + if (collection.IsManaged) + { + TextContainer.Height *= (Height - count_text_size) / Height; + TextContainer.Margin = new MarginPadding { Bottom = count_text_size }; + + TextContainer.Add(countText = new OsuSpriteText + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.TopLeft, + Depth = float.MinValue, + Font = OsuFont.Default.With(size: count_text_size, weight: FontWeight.SemiBold), + Margin = new MarginPadding { Top = 2, Left = 2 }, + Colour = colours.Yellow + }); + + itemCountSubscription = realm.SubscribeToPropertyChanged>(r => r.Find(collection.ID), c => c.BeatmapMD5Hashes, items => + { + countText.Text = items.Count == 1 + // Intentionally not localised until we have proper support for this (see https://github.com/ppy/osu-framework/pull/4918 + // but also in this case we want support for formatting a number within a string). + ? $"{items.Count:#,0} beatmap" + : $"{items.Count:#,0} beatmaps"; + }); + } + else + { + PlaceholderText = "Create a new collection"; + } + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + itemCountSubscription?.Dispose(); } } From 954be126922a63458dff577917ebf46e3ac72b75 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 22 Mar 2023 14:46:13 +0900 Subject: [PATCH 002/107] Debounce updates to ensure event isn't fired too often after much collection management --- .../Collections/DrawableCollectionListItem.cs | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/osu.Game/Collections/DrawableCollectionListItem.cs b/osu.Game/Collections/DrawableCollectionListItem.cs index efeb066869..87cc14ecb9 100644 --- a/osu.Game/Collections/DrawableCollectionListItem.cs +++ b/osu.Game/Collections/DrawableCollectionListItem.cs @@ -156,14 +156,17 @@ namespace osu.Game.Collections Colour = colours.Yellow }); - itemCountSubscription = realm.SubscribeToPropertyChanged>(r => r.Find(collection.ID), c => c.BeatmapMD5Hashes, items => - { - countText.Text = items.Count == 1 - // Intentionally not localised until we have proper support for this (see https://github.com/ppy/osu-framework/pull/4918 - // but also in this case we want support for formatting a number within a string). - ? $"{items.Count:#,0} beatmap" - : $"{items.Count:#,0} beatmaps"; - }); + itemCountSubscription = realm.SubscribeToPropertyChanged>(r => r.Find(collection.ID), c => c.BeatmapMD5Hashes, _ => + Scheduler.AddOnce(() => + { + int count = collection.PerformRead(c => c.BeatmapMD5Hashes.Count); + + countText.Text = count == 1 + // Intentionally not localised until we have proper support for this (see https://github.com/ppy/osu-framework/pull/4918 + // but also in this case we want support for formatting a number within a string). + ? $"{count:#,0} beatmap" + : $"{count:#,0} beatmaps"; + })); } else { From 256789193f7d99d6e1dd5ca00f23a82830d195a2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 3 Apr 2023 15:28:01 +0900 Subject: [PATCH 003/107] Remove redundant type specification --- osu.Game/Collections/DrawableCollectionListItem.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Collections/DrawableCollectionListItem.cs b/osu.Game/Collections/DrawableCollectionListItem.cs index 87cc14ecb9..31b127ef2a 100644 --- a/osu.Game/Collections/DrawableCollectionListItem.cs +++ b/osu.Game/Collections/DrawableCollectionListItem.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -156,7 +155,7 @@ namespace osu.Game.Collections Colour = colours.Yellow }); - itemCountSubscription = realm.SubscribeToPropertyChanged>(r => r.Find(collection.ID), c => c.BeatmapMD5Hashes, _ => + itemCountSubscription = realm.SubscribeToPropertyChanged(r => r.Find(collection.ID), c => c.BeatmapMD5Hashes, _ => Scheduler.AddOnce(() => { int count = collection.PerformRead(c => c.BeatmapMD5Hashes.Count); From c221a0c9f93c20949f26459405cfcc5047a39e0b Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 1 Jan 2025 01:43:43 -0500 Subject: [PATCH 004/107] Improve UI scale on iOS devices --- osu.Game/Graphics/Containers/ScalingContainer.cs | 6 ++++++ osu.Game/OsuGame.cs | 5 +++++ osu.iOS/OsuGameIOS.cs | 3 +++ 3 files changed, 14 insertions(+) diff --git a/osu.Game/Graphics/Containers/ScalingContainer.cs b/osu.Game/Graphics/Containers/ScalingContainer.cs index c47aba2f0c..ac76c0546b 100644 --- a/osu.Game/Graphics/Containers/ScalingContainer.cs +++ b/osu.Game/Graphics/Containers/ScalingContainer.cs @@ -3,6 +3,7 @@ #nullable disable +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -99,6 +100,10 @@ namespace osu.Game.Graphics.Containers this.applyUIScale = applyUIScale; } + [Resolved(canBeNull: true)] + [CanBeNull] + private OsuGame game { get; set; } + [BackgroundDependencyLoader] private void load(OsuConfigManager osuConfig) { @@ -111,6 +116,7 @@ namespace osu.Game.Graphics.Containers protected override void Update() { + TargetDrawSize = new Vector2(1024, 1024 / (game?.BaseAspectRatio ?? 1f)); Scale = new Vector2(CurrentScale); Size = new Vector2(1 / CurrentScale); diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index c20536a1ec..5227400694 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -831,6 +831,11 @@ namespace osu.Game protected virtual UpdateManager CreateUpdateManager() => new UpdateManager(); + /// + /// The base aspect ratio to use in all s. + /// + protected internal virtual float BaseAspectRatio => 4f / 3f; + protected override Container CreateScalingContainer() => new ScalingContainer(ScalingMode.Everything); #region Beatmap progression diff --git a/osu.iOS/OsuGameIOS.cs b/osu.iOS/OsuGameIOS.cs index a9ca1778a0..b3d9be04a1 100644 --- a/osu.iOS/OsuGameIOS.cs +++ b/osu.iOS/OsuGameIOS.cs @@ -10,6 +10,7 @@ using osu.Framework.Platform; using osu.Game; using osu.Game.Updater; using osu.Game.Utils; +using UIKit; namespace osu.iOS { @@ -19,6 +20,8 @@ namespace osu.iOS public override bool HideUnlicensedContent => true; + protected override float BaseAspectRatio => (float)(UIScreen.MainScreen.Bounds.Width / UIScreen.MainScreen.Bounds.Height); + protected override UpdateManager CreateUpdateManager() => new MobileUpdateNotifier(); protected override BatteryInfo CreateBatteryInfo() => new IOSBatteryInfo(); From 4a164b7b149ff7c8f78d15f904fcb61673ac9ff8 Mon Sep 17 00:00:00 2001 From: Joppe27 Date: Sun, 11 Dec 2022 02:17:50 +0100 Subject: [PATCH 005/107] Add legacy taiko swell --- .../Objects/Drawables/DrawableSwell.cs | 113 ++------------ .../Objects/ISkinnableSwell.cs | 22 +++ .../Argon/TaikoArgonSkinTransformer.cs | 2 +- .../Skinning/Default/DefaultSwell.cs | 142 ++++++++++++++++++ .../Skinning/Legacy/LegacySwell.cs | 136 +++++++++++++++++ .../Skinning/Legacy/LegacySwellCirclePiece.cs | 23 +++ .../Legacy/TaikoLegacySkinTransformer.cs | 10 +- .../TaikoSkinComponents.cs | 1 + 8 files changed, 348 insertions(+), 101 deletions(-) create mode 100644 osu.Game.Rulesets.Taiko/Objects/ISkinnableSwell.cs create mode 100644 osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs create mode 100644 osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs create mode 100644 osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwellCirclePiece.cs diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs index 28617b35f6..cba044959c 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs @@ -6,14 +6,9 @@ using System; using System.Linq; using JetBrains.Annotations; -using osu.Framework.Allocation; -using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Game.Graphics; using osu.Game.Rulesets.Objects.Drawables; -using osuTK.Graphics; -using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Taiko.Skinning.Default; @@ -25,11 +20,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { public partial class DrawableSwell : DrawableTaikoHitObject { - private const float target_ring_thick_border = 1.4f; - private const float target_ring_thin_border = 1f; - private const float target_ring_scale = 5f; - private const float inner_ring_alpha = 0.65f; - /// /// Offset away from the start time of the swell at which the ring starts appearing. /// @@ -37,10 +27,9 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables private Vector2 baseSize; + private readonly SkinnableDrawable spinnerBody; + private readonly Container ticks; - private readonly Container bodyContainer; - private readonly CircularContainer targetRing; - private readonly CircularContainer expandingRing; private double? lastPressHandleTime; @@ -61,82 +50,17 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { FillMode = FillMode.Fit; - Content.Add(bodyContainer = new Container + Content.Add(spinnerBody = new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.Swell), _ => new DefaultSwell()) { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, - Depth = 1, - Children = new Drawable[] - { - expandingRing = new CircularContainer - { - Name = "Expanding ring", - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Alpha = 0, - RelativeSizeAxes = Axes.Both, - Blending = BlendingParameters.Additive, - Masking = true, - Children = new[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Alpha = inner_ring_alpha, - } - } - }, - targetRing = new CircularContainer - { - Name = "Target ring (thick border)", - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Masking = true, - BorderThickness = target_ring_thick_border, - Blending = BlendingParameters.Additive, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Alpha = 0, - AlwaysPresent = true - }, - new CircularContainer - { - Name = "Target ring (thin border)", - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Masking = true, - BorderThickness = target_ring_thin_border, - BorderColour = Color4.White, - Children = new[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Alpha = 0, - AlwaysPresent = true - } - } - } - } - } - } }); AddInternal(ticks = new Container { RelativeSizeAxes = Axes.Both }); } - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - expandingRing.Colour = colours.YellowLight; - targetRing.BorderColour = colours.YellowDark.Opacity(0.25f); - } - - protected override SkinnableDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.Swell), + protected override SkinnableDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.SwellCirclePiece), _ => new SwellCirclePiece { // to allow for rotation transform @@ -208,16 +132,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables int numHits = ticks.Count(r => r.IsHit); - float completion = (float)numHits / HitObject.RequiredHits; - - expandingRing - .FadeTo(expandingRing.Alpha + Math.Clamp(completion / 16, 0.1f, 0.6f), 50) - .Then() - .FadeTo(completion / 8, 2000, Easing.OutQuint); - - MainPiece.Drawable.RotateTo((float)(completion * HitObject.Duration / 8), 4000, Easing.OutQuint); - - expandingRing.ScaleTo(1f + Math.Min(target_ring_scale - 1f, (target_ring_scale - 1f) * completion * 1.3f), 260, Easing.OutQuint); + (spinnerBody.Drawable as ISkinnableSwell)?.OnUserInput(this, numHits, MainPiece); if (numHits == HitObject.RequiredHits) ApplyMaxResult(); @@ -252,24 +167,24 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { base.UpdateStartTimeStateTransforms(); - using (BeginDelayedSequence(-ring_appear_offset)) - targetRing.ScaleTo(target_ring_scale, 400, Easing.OutQuint); + (spinnerBody.Drawable as ISkinnableSwell)?.ApplyPassiveTransforms(this, MainPiece); } protected override void UpdateHitStateTransforms(ArmedState state) { - const double transition_duration = 300; - switch (state) { case ArmedState.Idle: - expandingRing.FadeTo(0); + HandleUserInput = true; break; case ArmedState.Miss: case ArmedState.Hit: - this.FadeOut(transition_duration, Easing.Out); - bodyContainer.ScaleTo(1.4f, transition_duration); + // Postpone drawable hitobject expiration until it has animated/faded out. Inputs on the object are disallowed during this delay. + LifetimeEnd = Time.Current + 1200; + HandleUserInput = false; + + (spinnerBody.Drawable as ISkinnableSwell)?.OnHitObjectEnd(state, MainPiece); break; } } diff --git a/osu.Game.Rulesets.Taiko/Objects/ISkinnableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/ISkinnableSwell.cs new file mode 100644 index 0000000000..18feff5bb9 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Objects/ISkinnableSwell.cs @@ -0,0 +1,22 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Taiko.Objects.Drawables; +using osu.Game.Skinning; + +namespace osu.Game.Rulesets.Taiko.Objects +{ + public interface ISkinnableSwell + { + void OnUserInput(DrawableTaikoHitObject swell, int numHits, SkinnableDrawable mainPiece); + + void OnHitObjectEnd(ArmedState state, SkinnableDrawable mainPiece); + + /// + /// Applies passive transforms on HitObject start. Gets called every time DrawableTaikoHitobject + /// changes state. This happens on creation, and when the object is completed (as in hit or missed). + /// + void ApplyPassiveTransforms(DrawableTaikoHitObject swell, SkinnableDrawable mainPiece); + } +} diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs index bfc9e8648d..cfd30dd628 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs @@ -68,7 +68,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon case TaikoSkinComponents.TaikoExplosionOk: return new ArgonHitExplosion(taikoComponent.Component); - case TaikoSkinComponents.Swell: + case TaikoSkinComponents.SwellCirclePiece: return new ArgonSwellCirclePiece(); } diff --git a/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs new file mode 100644 index 0000000000..e525e9873d --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs @@ -0,0 +1,142 @@ +// Copyright (c) ppy Pty Ltd . 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.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.Objects.Drawables; +using osu.Game.Skinning; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Taiko.Skinning.Default +{ + public partial class DefaultSwell : Container, ISkinnableSwell + { + private const float target_ring_thick_border = 1.4f; + private const float target_ring_thin_border = 1f; + private const float target_ring_scale = 5f; + private const float inner_ring_alpha = 0.65f; + + private readonly Container bodyContainer; + private readonly CircularContainer targetRing; + private readonly CircularContainer expandingRing; + + public DefaultSwell() + { + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + RelativeSizeAxes = Axes.Both; + + Content.Add(bodyContainer = new Container + { + RelativeSizeAxes = Axes.Both, + Depth = 1, + Children = new Drawable[] + { + expandingRing = new CircularContainer + { + Name = "Expanding ring", + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Alpha = 0, + RelativeSizeAxes = Axes.Both, + Blending = BlendingParameters.Additive, + Masking = true, + Children = new[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = inner_ring_alpha, + } + } + }, + targetRing = new CircularContainer + { + Name = "Target ring (thick border)", + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Masking = true, + BorderThickness = target_ring_thick_border, + Blending = BlendingParameters.Additive, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + AlwaysPresent = true + }, + new CircularContainer + { + Name = "Target ring (thin border)", + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Masking = true, + BorderThickness = target_ring_thin_border, + BorderColour = Color4.White, + Children = new[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + AlwaysPresent = true + } + } + } + } + } + } + }); + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + expandingRing.Colour = colours.YellowLight; + targetRing.BorderColour = colours.YellowDark.Opacity(0.25f); + } + + public void OnUserInput(DrawableTaikoHitObject swell, int numHits, SkinnableDrawable mainPiece) + { + float completion = (float)numHits / swell.HitObject.RequiredHits; + + mainPiece.Drawable.RotateTo((float)(completion * swell.HitObject.Duration / 8), 4000, Easing.OutQuint); + + expandingRing.ScaleTo(1f + Math.Min(target_ring_scale - 1f, (target_ring_scale - 1f) * completion * 1.3f), 260, Easing.OutQuint); + + expandingRing + .FadeTo(expandingRing.Alpha + Math.Clamp(completion / 16, 0.1f, 0.6f), 50) + .Then() + .FadeTo(completion / 8, 2000, Easing.OutQuint); + } + + public void OnHitObjectEnd(ArmedState state, SkinnableDrawable mainPiece) + { + const double transition_duration = 300; + + bodyContainer.FadeOut(transition_duration, Easing.OutQuad); + bodyContainer.ScaleTo(1.4f, transition_duration); + mainPiece.FadeOut(transition_duration, Easing.OutQuad); + } + + public void ApplyPassiveTransforms(DrawableTaikoHitObject swell, SkinnableDrawable mainPiece) + { + if (swell.IsHit == false) + expandingRing.FadeTo(0); + + const double ring_appear_offset = 100; + + targetRing.Delay(ring_appear_offset).ScaleTo(target_ring_scale, 400, Easing.OutQuint); + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs new file mode 100644 index 0000000000..240ec71f94 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs @@ -0,0 +1,136 @@ +// 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.Graphics; +using osu.Framework.Allocation; +using osu.Framework.Graphics.Containers; +using osu.Game.Skinning; +using osu.Framework.Graphics.Sprites; +using osu.Game.Rulesets.Taiko.Objects.Drawables; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Framework.Audio.Sample; +using osu.Game.Audio; +using osuTK; +using osu.Game.Rulesets.Objects.Drawables; + +namespace osu.Game.Rulesets.Taiko.Skinning.Legacy +{ + public partial class LegacySwell : Container, ISkinnableSwell + { + private Container bodyContainer = null!; + private Sprite spinnerCircle = null!; + private Sprite shrinkingRing = null!; + private Sprite clearAnimation = null!; + private ISample? clearSample; + private LegacySpriteText remainingHitsCountdown = null!; + + private bool samplePlayed; + + public LegacySwell() + { + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + RelativeSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load(ISkinSource skin, SkinManager skinManager) + { + Child = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Position = new Vector2(200f, 100f), + + Children = new Drawable[] + { + bodyContainer = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Alpha = 0, + + Children = new Drawable[] + { + spinnerCircle = new Sprite + { + Texture = skin.GetTexture("spinner-circle"), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(0.8f), + }, + shrinkingRing = new Sprite + { + Texture = skin.GetTexture("spinner-approachcircle") ?? skinManager.DefaultClassicSkin.GetTexture("spinner-approachcircle"), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = Vector2.One, + }, + remainingHitsCountdown = new LegacySpriteText(LegacyFont.Combo) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Position = new Vector2(0f, 165f), + Scale = Vector2.One, + }, + } + }, + clearAnimation = new Sprite + { + // File extension is included here because of a GetTexture limitation, see #21543 + Texture = skin.GetTexture("spinner-osu.png"), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Position = new Vector2(0f, -165f), + Scale = new Vector2(0.3f), + Alpha = 0, + }, + } + }; + + clearSample = skin.GetSample(new SampleInfo("spinner-osu")); + } + + public void OnUserInput(DrawableTaikoHitObject swell, int numHits, SkinnableDrawable mainPiece) + { + remainingHitsCountdown.Text = $"{swell.HitObject.RequiredHits - numHits}"; + spinnerCircle.RotateTo(180f * numHits, 1000, Easing.OutQuint); + } + + public void OnHitObjectEnd(ArmedState state, SkinnableDrawable mainPiece) + { + const double clear_transition_duration = 300; + + bodyContainer.FadeOut(clear_transition_duration, Easing.OutQuad); + + if (state == ArmedState.Hit) + { + if (!samplePlayed) + { + clearSample?.Play(); + samplePlayed = true; + } + + clearAnimation + .FadeIn(clear_transition_duration, Easing.InQuad) + .ScaleTo(0.8f, clear_transition_duration, Easing.InQuad) + .Delay(700).FadeOut(200, Easing.OutQuad); + } + } + + public void ApplyPassiveTransforms(DrawableTaikoHitObject swell, SkinnableDrawable mainPiece) + { + if (swell.IsHit == false) + { + remainingHitsCountdown.Text = $"{swell.HitObject.RequiredHits}"; + samplePlayed = false; + } + + const double body_transition_duration = 100; + + mainPiece.FadeOut(body_transition_duration); + bodyContainer.FadeIn(body_transition_duration); + shrinkingRing.ResizeTo(0.1f, swell.HitObject.Duration); + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwellCirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwellCirclePiece.cs new file mode 100644 index 0000000000..40501d1d40 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwellCirclePiece.cs @@ -0,0 +1,23 @@ +// 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.Sprites; +using osu.Game.Skinning; +using osuTK; + +namespace osu.Game.Rulesets.Taiko.Skinning.Legacy +{ + internal partial class LegacySwellCirclePiece : Sprite + { + [BackgroundDependencyLoader] + private void load(ISkinSource skin, SkinManager skinManager) + { + Texture = skin.GetTexture("spinner-warning") ?? skinManager.DefaultClassicSkin.GetTexture("spinner-circle"); + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + Scale = skin.GetTexture("spinner-warning") != null ? Vector2.One : new Vector2(0.18f); + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs index 5bdb824f1c..243d975216 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs @@ -66,7 +66,15 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy return this.GetAnimation("sliderscorepoint", false, false); case TaikoSkinComponents.Swell: - // todo: support taiko legacy swell (https://github.com/ppy/osu/issues/13601). + if (GetTexture("spinner-circle") != null) + return new LegacySwell(); + + return null; + + case TaikoSkinComponents.SwellCirclePiece: + if (GetTexture("spinner-circle") != null) + return new LegacySwellCirclePiece(); + return null; case TaikoSkinComponents.HitTarget: diff --git a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs index 28133ffcb2..aa7e4686d8 100644 --- a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs +++ b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs @@ -11,6 +11,7 @@ namespace osu.Game.Rulesets.Taiko DrumRollBody, DrumRollTick, Swell, + SwellCirclePiece, HitTarget, PlayfieldBackgroundLeft, PlayfieldBackgroundRight, From fe84e6e5f53d5a3264b1fcbe68fb698b7c039f48 Mon Sep 17 00:00:00 2001 From: Joppe27 Date: Sun, 11 Dec 2022 02:19:06 +0100 Subject: [PATCH 006/107] Adjust existing test to accommodate swell size --- osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableSwell.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableSwell.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableSwell.cs index c130b5f366..286b16aa34 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableSwell.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableSwell.cs @@ -20,6 +20,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning { Anchor = Anchor.Centre, Origin = Anchor.Centre, + Scale = new osuTK.Vector2(0.5f), })); } From 988450a2c4f8244d1ef1bc572d711ee781eaaa09 Mon Sep 17 00:00:00 2001 From: Joppe27 Date: Sun, 11 Dec 2022 02:23:48 +0100 Subject: [PATCH 007/107] Add test for expire delay Delaying the expiry of the drawable hitobject can potentially be dangerous and gameplay-altering when user inputs are accidentally handled. This is why I found a test necessary. --- .../TestSceneDrawableSwellExpireDelay.cs | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableSwellExpireDelay.cs diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableSwellExpireDelay.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableSwellExpireDelay.cs new file mode 100644 index 0000000000..ad78ed3b20 --- /dev/null +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableSwellExpireDelay.cs @@ -0,0 +1,41 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using NUnit.Framework; +using osu.Game.Rulesets.Replays; +using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.Replays; +using osu.Game.Rulesets.Taiko.Tests.Judgements; + +namespace osu.Game.Rulesets.Taiko.Tests.Skinning +{ + public partial class TestSceneDrawableSwellExpireDelay : JudgementTest + { + [Test] + public void TestExpireDelay() + { + const double swell_start = 1000; + const double swell_duration = 1000; + + Swell swell = new Swell + { + StartTime = swell_start, + Duration = swell_duration, + }; + + Hit hit = new Hit { StartTime = swell_start + swell_duration + 50 }; + + List frames = new List + { + new TaikoReplayFrame(0), + new TaikoReplayFrame(2100, TaikoAction.LeftCentre), + }; + + PerformTest(frames, CreateBeatmap(swell, hit)); + + AssertResult(0, HitResult.Ok); + } + } +} From e2196e8b9b97f447863b61124f7bd3454a505e60 Mon Sep 17 00:00:00 2001 From: Joppe27 Date: Tue, 13 Dec 2022 19:32:05 +0100 Subject: [PATCH 008/107] Rename methods and skin component + add comments --- .../Objects/Drawables/DrawableSwell.cs | 13 ++++++++----- osu.Game.Rulesets.Taiko/Objects/ISkinnableSwell.cs | 10 +++------- .../Skinning/Default/DefaultSwell.cs | 6 +++--- .../Skinning/Legacy/LegacySwell.cs | 6 +++--- .../Skinning/Legacy/TaikoLegacySkinTransformer.cs | 2 +- osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs | 2 +- 6 files changed, 19 insertions(+), 20 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs index cba044959c..54a609f7d3 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs @@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { FillMode = FillMode.Fit; - Content.Add(spinnerBody = new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.Swell), _ => new DefaultSwell()) + Content.Add(spinnerBody = new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.SwellBody), _ => new DefaultSwell()) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -132,7 +132,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables int numHits = ticks.Count(r => r.IsHit); - (spinnerBody.Drawable as ISkinnableSwell)?.OnUserInput(this, numHits, MainPiece); + (spinnerBody.Drawable as ISkinnableSwell)?.AnimateSwellProgress(this, numHits, MainPiece); if (numHits == HitObject.RequiredHits) ApplyMaxResult(); @@ -167,7 +167,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { base.UpdateStartTimeStateTransforms(); - (spinnerBody.Drawable as ISkinnableSwell)?.ApplyPassiveTransforms(this, MainPiece); + (spinnerBody.Drawable as ISkinnableSwell)?.AnimateSwellStart(this, MainPiece); } protected override void UpdateHitStateTransforms(ArmedState state) @@ -175,16 +175,19 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables switch (state) { case ArmedState.Idle: + // Only for rewind support. Reallows user inputs if swell is rewound from being hit/missed to being idle. HandleUserInput = true; break; case ArmedState.Miss: case ArmedState.Hit: + const int clear_animation_duration = 1200; + // Postpone drawable hitobject expiration until it has animated/faded out. Inputs on the object are disallowed during this delay. - LifetimeEnd = Time.Current + 1200; + LifetimeEnd = Time.Current + clear_animation_duration; HandleUserInput = false; - (spinnerBody.Drawable as ISkinnableSwell)?.OnHitObjectEnd(state, MainPiece); + (spinnerBody.Drawable as ISkinnableSwell)?.AnimateSwellCompletion(state, MainPiece); break; } } diff --git a/osu.Game.Rulesets.Taiko/Objects/ISkinnableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/ISkinnableSwell.cs index 18feff5bb9..3cdb3566fb 100644 --- a/osu.Game.Rulesets.Taiko/Objects/ISkinnableSwell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/ISkinnableSwell.cs @@ -9,14 +9,10 @@ namespace osu.Game.Rulesets.Taiko.Objects { public interface ISkinnableSwell { - void OnUserInput(DrawableTaikoHitObject swell, int numHits, SkinnableDrawable mainPiece); + void AnimateSwellProgress(DrawableTaikoHitObject swell, int numHits, SkinnableDrawable mainPiece); - void OnHitObjectEnd(ArmedState state, SkinnableDrawable mainPiece); + void AnimateSwellCompletion(ArmedState state, SkinnableDrawable mainPiece); - /// - /// Applies passive transforms on HitObject start. Gets called every time DrawableTaikoHitobject - /// changes state. This happens on creation, and when the object is completed (as in hit or missed). - /// - void ApplyPassiveTransforms(DrawableTaikoHitObject swell, SkinnableDrawable mainPiece); + void AnimateSwellStart(DrawableTaikoHitObject swell, SkinnableDrawable mainPiece); } } diff --git a/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs index e525e9873d..cec07d8769 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs @@ -106,7 +106,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default targetRing.BorderColour = colours.YellowDark.Opacity(0.25f); } - public void OnUserInput(DrawableTaikoHitObject swell, int numHits, SkinnableDrawable mainPiece) + public void AnimateSwellProgress(DrawableTaikoHitObject swell, int numHits, SkinnableDrawable mainPiece) { float completion = (float)numHits / swell.HitObject.RequiredHits; @@ -120,7 +120,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default .FadeTo(completion / 8, 2000, Easing.OutQuint); } - public void OnHitObjectEnd(ArmedState state, SkinnableDrawable mainPiece) + public void AnimateSwellCompletion(ArmedState state, SkinnableDrawable mainPiece) { const double transition_duration = 300; @@ -129,7 +129,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default mainPiece.FadeOut(transition_duration, Easing.OutQuad); } - public void ApplyPassiveTransforms(DrawableTaikoHitObject swell, SkinnableDrawable mainPiece) + public void AnimateSwellStart(DrawableTaikoHitObject swell, SkinnableDrawable mainPiece) { if (swell.IsHit == false) expandingRing.FadeTo(0); diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs index 240ec71f94..fdddea2df5 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs @@ -91,13 +91,13 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy clearSample = skin.GetSample(new SampleInfo("spinner-osu")); } - public void OnUserInput(DrawableTaikoHitObject swell, int numHits, SkinnableDrawable mainPiece) + public void AnimateSwellProgress(DrawableTaikoHitObject swell, int numHits, SkinnableDrawable mainPiece) { remainingHitsCountdown.Text = $"{swell.HitObject.RequiredHits - numHits}"; spinnerCircle.RotateTo(180f * numHits, 1000, Easing.OutQuint); } - public void OnHitObjectEnd(ArmedState state, SkinnableDrawable mainPiece) + public void AnimateSwellCompletion(ArmedState state, SkinnableDrawable mainPiece) { const double clear_transition_duration = 300; @@ -118,7 +118,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy } } - public void ApplyPassiveTransforms(DrawableTaikoHitObject swell, SkinnableDrawable mainPiece) + public void AnimateSwellStart(DrawableTaikoHitObject swell, SkinnableDrawable mainPiece) { if (swell.IsHit == false) { diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs index 243d975216..b9ebed6b80 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs @@ -65,7 +65,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy case TaikoSkinComponents.DrumRollTick: return this.GetAnimation("sliderscorepoint", false, false); - case TaikoSkinComponents.Swell: + case TaikoSkinComponents.SwellBody: if (GetTexture("spinner-circle") != null) return new LegacySwell(); diff --git a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs index aa7e4686d8..0145fb6482 100644 --- a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs +++ b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs @@ -10,7 +10,7 @@ namespace osu.Game.Rulesets.Taiko RimHit, DrumRollBody, DrumRollTick, - Swell, + SwellBody, SwellCirclePiece, HitTarget, PlayfieldBackgroundLeft, From 4fd8a4dc5a6f0453767175aa706ea331bbfca7c6 Mon Sep 17 00:00:00 2001 From: Nathan Du Date: Fri, 31 Jan 2025 16:55:39 +0800 Subject: [PATCH 009/107] Merge taiko swell components Per , taking a variation of the "Make all swell main pieces implement ISkinnableSwellPart" path. Should clean the interface up enough for further refactors. --- .../Objects/Drawables/DrawableSwell.cs | 25 ++++-------- .../Objects/ISkinnableSwell.cs | 7 ++-- .../Skinning/Argon/ArgonSwell.cs | 20 ++++++++++ .../Argon/TaikoArgonSkinTransformer.cs | 4 +- .../Skinning/Default/DefaultSwell.cs | 26 +++++++++---- ...wellSymbolPiece.cs => SwellCirclePiece.cs} | 0 .../Skinning/Legacy/LegacySwell.cs | 38 +++++++++++-------- .../Legacy/TaikoLegacySkinTransformer.cs | 6 --- .../TaikoSkinComponents.cs | 1 - 9 files changed, 75 insertions(+), 52 deletions(-) create mode 100644 osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonSwell.cs rename osu.Game.Rulesets.Taiko/Skinning/Default/{SwellSymbolPiece.cs => SwellCirclePiece.cs} (100%) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs index 54a609f7d3..18d76d02a1 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs @@ -27,8 +27,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables private Vector2 baseSize; - private readonly SkinnableDrawable spinnerBody; - private readonly Container ticks; private double? lastPressHandleTime; @@ -50,24 +48,17 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { FillMode = FillMode.Fit; - Content.Add(spinnerBody = new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.SwellBody), _ => new DefaultSwell()) + AddInternal(ticks = new Container { RelativeSizeAxes = Axes.Both }); + } + + protected override SkinnableDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.SwellBody), + _ => new DefaultSwell { Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, }); - AddInternal(ticks = new Container { RelativeSizeAxes = Axes.Both }); - } - - protected override SkinnableDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.SwellCirclePiece), - _ => new SwellCirclePiece - { - // to allow for rotation transform - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }); - protected override void RecreatePieces() { base.RecreatePieces(); @@ -132,7 +123,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables int numHits = ticks.Count(r => r.IsHit); - (spinnerBody.Drawable as ISkinnableSwell)?.AnimateSwellProgress(this, numHits, MainPiece); + (MainPiece.Drawable as ISkinnableSwell)?.AnimateSwellProgress(this, numHits); if (numHits == HitObject.RequiredHits) ApplyMaxResult(); @@ -167,7 +158,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { base.UpdateStartTimeStateTransforms(); - (spinnerBody.Drawable as ISkinnableSwell)?.AnimateSwellStart(this, MainPiece); + (MainPiece.Drawable as ISkinnableSwell)?.AnimateSwellStart(this); } protected override void UpdateHitStateTransforms(ArmedState state) @@ -187,7 +178,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables LifetimeEnd = Time.Current + clear_animation_duration; HandleUserInput = false; - (spinnerBody.Drawable as ISkinnableSwell)?.AnimateSwellCompletion(state, MainPiece); + (MainPiece.Drawable as ISkinnableSwell)?.AnimateSwellCompletion(state); break; } } diff --git a/osu.Game.Rulesets.Taiko/Objects/ISkinnableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/ISkinnableSwell.cs index 3cdb3566fb..9bd169acd7 100644 --- a/osu.Game.Rulesets.Taiko/Objects/ISkinnableSwell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/ISkinnableSwell.cs @@ -3,16 +3,15 @@ using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Taiko.Objects.Drawables; -using osu.Game.Skinning; namespace osu.Game.Rulesets.Taiko.Objects { public interface ISkinnableSwell { - void AnimateSwellProgress(DrawableTaikoHitObject swell, int numHits, SkinnableDrawable mainPiece); + void AnimateSwellProgress(DrawableTaikoHitObject swell, int numHits); - void AnimateSwellCompletion(ArmedState state, SkinnableDrawable mainPiece); + void AnimateSwellCompletion(ArmedState state); - void AnimateSwellStart(DrawableTaikoHitObject swell, SkinnableDrawable mainPiece); + void AnimateSwellStart(DrawableTaikoHitObject swell); } } diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonSwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonSwell.cs new file mode 100644 index 0000000000..65cd936e38 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonSwell.cs @@ -0,0 +1,20 @@ +// 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.Graphics; +using osu.Game.Rulesets.Taiko.Skinning.Default; + +namespace osu.Game.Rulesets.Taiko.Skinning.Argon +{ + public partial class ArgonSwell : DefaultSwell + { + protected override Drawable CreateCentreCircle() + { + return new ArgonSwellCirclePiece() + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }; + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs index cfd30dd628..b588a22d12 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs @@ -68,8 +68,8 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon case TaikoSkinComponents.TaikoExplosionOk: return new ArgonHitExplosion(taikoComponent.Component); - case TaikoSkinComponents.SwellCirclePiece: - return new ArgonSwellCirclePiece(); + case TaikoSkinComponents.SwellBody: + return new ArgonSwell(); } break; diff --git a/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs index cec07d8769..bdb444db90 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs @@ -11,7 +11,6 @@ using osu.Game.Graphics; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Objects.Drawables; -using osu.Game.Skinning; using osuTK.Graphics; namespace osu.Game.Rulesets.Taiko.Skinning.Default @@ -26,6 +25,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default private readonly Container bodyContainer; private readonly CircularContainer targetRing; private readonly CircularContainer expandingRing; + private readonly Drawable centreCircle; public DefaultSwell() { @@ -35,6 +35,8 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default Content.Add(bodyContainer = new Container { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, Depth = 1, Children = new Drawable[] @@ -94,11 +96,21 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default } } } - } + }, + centreCircle = CreateCentreCircle(), } }); } + protected virtual Drawable CreateCentreCircle() + { + return new SwellCirclePiece() + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }; + } + [BackgroundDependencyLoader] private void load(OsuColour colours) { @@ -106,11 +118,11 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default targetRing.BorderColour = colours.YellowDark.Opacity(0.25f); } - public void AnimateSwellProgress(DrawableTaikoHitObject swell, int numHits, SkinnableDrawable mainPiece) + public void AnimateSwellProgress(DrawableTaikoHitObject swell, int numHits) { float completion = (float)numHits / swell.HitObject.RequiredHits; - mainPiece.Drawable.RotateTo((float)(completion * swell.HitObject.Duration / 8), 4000, Easing.OutQuint); + centreCircle.RotateTo((float)(completion * swell.HitObject.Duration / 8), 4000, Easing.OutQuint); expandingRing.ScaleTo(1f + Math.Min(target_ring_scale - 1f, (target_ring_scale - 1f) * completion * 1.3f), 260, Easing.OutQuint); @@ -120,16 +132,16 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default .FadeTo(completion / 8, 2000, Easing.OutQuint); } - public void AnimateSwellCompletion(ArmedState state, SkinnableDrawable mainPiece) + public void AnimateSwellCompletion(ArmedState state) { const double transition_duration = 300; bodyContainer.FadeOut(transition_duration, Easing.OutQuad); bodyContainer.ScaleTo(1.4f, transition_duration); - mainPiece.FadeOut(transition_duration, Easing.OutQuad); + centreCircle.FadeOut(transition_duration, Easing.OutQuad); } - public void AnimateSwellStart(DrawableTaikoHitObject swell, SkinnableDrawable mainPiece) + public void AnimateSwellStart(DrawableTaikoHitObject swell) { if (swell.IsHit == false) expandingRing.FadeTo(0); diff --git a/osu.Game.Rulesets.Taiko/Skinning/Default/SwellSymbolPiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Default/SwellCirclePiece.cs similarity index 100% rename from osu.Game.Rulesets.Taiko/Skinning/Default/SwellSymbolPiece.cs rename to osu.Game.Rulesets.Taiko/Skinning/Default/SwellCirclePiece.cs diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs index fdddea2df5..e487c5e051 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs @@ -18,6 +18,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy public partial class LegacySwell : Container, ISkinnableSwell { private Container bodyContainer = null!; + private Sprite warning = null!; private Sprite spinnerCircle = null!; private Sprite shrinkingRing = null!; private Sprite clearAnimation = null!; @@ -40,14 +41,21 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Position = new Vector2(200f, 100f), Children = new Drawable[] { + warning = new Sprite + { + Texture = skin.GetTexture("spinner-warning") ?? skinManager.DefaultClassicSkin.GetTexture("spinner-circle"), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = skin.GetTexture("spinner-warning") != null ? Vector2.One : new Vector2(0.18f), + }, bodyContainer = new Container { Anchor = Anchor.Centre, Origin = Anchor.Centre, + Position = new Vector2(200f, 100f), Alpha = 0, Children = new Drawable[] @@ -73,31 +81,31 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy Position = new Vector2(0f, 165f), Scale = Vector2.One, }, + clearAnimation = new Sprite + { + // File extension is included here because of a GetTexture limitation, see #21543 + Texture = skin.GetTexture("spinner-osu.png"), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Position = new Vector2(0f, -165f), + Scale = new Vector2(0.3f), + Alpha = 0, + }, } }, - clearAnimation = new Sprite - { - // File extension is included here because of a GetTexture limitation, see #21543 - Texture = skin.GetTexture("spinner-osu.png"), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Position = new Vector2(0f, -165f), - Scale = new Vector2(0.3f), - Alpha = 0, - }, } }; clearSample = skin.GetSample(new SampleInfo("spinner-osu")); } - public void AnimateSwellProgress(DrawableTaikoHitObject swell, int numHits, SkinnableDrawable mainPiece) + public void AnimateSwellProgress(DrawableTaikoHitObject swell, int numHits) { remainingHitsCountdown.Text = $"{swell.HitObject.RequiredHits - numHits}"; spinnerCircle.RotateTo(180f * numHits, 1000, Easing.OutQuint); } - public void AnimateSwellCompletion(ArmedState state, SkinnableDrawable mainPiece) + public void AnimateSwellCompletion(ArmedState state) { const double clear_transition_duration = 300; @@ -118,7 +126,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy } } - public void AnimateSwellStart(DrawableTaikoHitObject swell, SkinnableDrawable mainPiece) + public void AnimateSwellStart(DrawableTaikoHitObject swell) { if (swell.IsHit == false) { @@ -128,7 +136,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy const double body_transition_duration = 100; - mainPiece.FadeOut(body_transition_duration); + warning.FadeOut(body_transition_duration); bodyContainer.FadeIn(body_transition_duration); shrinkingRing.ResizeTo(0.1f, swell.HitObject.Duration); } diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs index b9ebed6b80..8fa4551fd4 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs @@ -71,12 +71,6 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy return null; - case TaikoSkinComponents.SwellCirclePiece: - if (GetTexture("spinner-circle") != null) - return new LegacySwellCirclePiece(); - - return null; - case TaikoSkinComponents.HitTarget: if (GetTexture("taikobigcircle") != null) return new TaikoLegacyHitTarget(); diff --git a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs index 0145fb6482..05c6316a05 100644 --- a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs +++ b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs @@ -11,7 +11,6 @@ namespace osu.Game.Rulesets.Taiko DrumRollBody, DrumRollTick, SwellBody, - SwellCirclePiece, HitTarget, PlayfieldBackgroundLeft, PlayfieldBackgroundRight, From 2a5540b39251c19f46a2965f0226f45d7a085f3e Mon Sep 17 00:00:00 2001 From: Nathan Du Date: Fri, 31 Jan 2025 17:51:35 +0800 Subject: [PATCH 010/107] remove ISkinnableSwell This commit removes ISkinnableSwell for taiko swell animations. In place of it, an event named UpdateHitProgress is added to DrawableSwell, and the skin swells are converted to listen to said event and ApplyCustomUpdateState, like how spinner skinning is implemented for std. --- .../Objects/Drawables/DrawableSwell.cs | 6 +- .../Objects/ISkinnableSwell.cs | 17 ----- .../Skinning/Default/DefaultSwell.cs | 70 +++++++++++------ .../Skinning/Legacy/LegacySwell.cs | 75 ++++++++++++------- 4 files changed, 101 insertions(+), 67 deletions(-) delete mode 100644 osu.Game.Rulesets.Taiko/Objects/ISkinnableSwell.cs diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs index 18d76d02a1..e0276db911 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs @@ -38,6 +38,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables /// public bool MustAlternate { get; internal set; } = true; + public event Action UpdateHitProgress; + public DrawableSwell() : this(null) { @@ -123,7 +125,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables int numHits = ticks.Count(r => r.IsHit); - (MainPiece.Drawable as ISkinnableSwell)?.AnimateSwellProgress(this, numHits); + UpdateHitProgress?.Invoke(numHits, HitObject.RequiredHits); if (numHits == HitObject.RequiredHits) ApplyMaxResult(); @@ -158,7 +160,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { base.UpdateStartTimeStateTransforms(); - (MainPiece.Drawable as ISkinnableSwell)?.AnimateSwellStart(this); } protected override void UpdateHitStateTransforms(ArmedState state) @@ -178,7 +179,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables LifetimeEnd = Time.Current + clear_animation_duration; HandleUserInput = false; - (MainPiece.Drawable as ISkinnableSwell)?.AnimateSwellCompletion(state); break; } } diff --git a/osu.Game.Rulesets.Taiko/Objects/ISkinnableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/ISkinnableSwell.cs deleted file mode 100644 index 9bd169acd7..0000000000 --- a/osu.Game.Rulesets.Taiko/Objects/ISkinnableSwell.cs +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Game.Rulesets.Objects.Drawables; -using osu.Game.Rulesets.Taiko.Objects.Drawables; - -namespace osu.Game.Rulesets.Taiko.Objects -{ - public interface ISkinnableSwell - { - void AnimateSwellProgress(DrawableTaikoHitObject swell, int numHits); - - void AnimateSwellCompletion(ArmedState state); - - void AnimateSwellStart(DrawableTaikoHitObject swell); - } -} diff --git a/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs index bdb444db90..852116cbfe 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs @@ -4,6 +4,7 @@ using System; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -15,13 +16,15 @@ using osuTK.Graphics; namespace osu.Game.Rulesets.Taiko.Skinning.Default { - public partial class DefaultSwell : Container, ISkinnableSwell + public partial class DefaultSwell : Container { private const float target_ring_thick_border = 1.4f; private const float target_ring_thin_border = 1f; private const float target_ring_scale = 5f; private const float inner_ring_alpha = 0.65f; + private DrawableSwell drawableSwell = null!; + private readonly Container bodyContainer; private readonly CircularContainer targetRing; private readonly CircularContainer expandingRing; @@ -102,6 +105,17 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default }); } + [BackgroundDependencyLoader] + private void load(DrawableHitObject hitObject, OsuColour colours) + { + drawableSwell = (DrawableSwell)hitObject; + drawableSwell.UpdateHitProgress += animateSwellProgress; + drawableSwell.ApplyCustomUpdateState += updateStateTransforms; + + expandingRing.Colour = colours.YellowLight; + targetRing.BorderColour = colours.YellowDark.Opacity(0.25f); + } + protected virtual Drawable CreateCentreCircle() { return new SwellCirclePiece() @@ -111,18 +125,11 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default }; } - [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void animateSwellProgress(int numHits, int requiredHits) { - expandingRing.Colour = colours.YellowLight; - targetRing.BorderColour = colours.YellowDark.Opacity(0.25f); - } + float completion = (float)numHits / requiredHits; - public void AnimateSwellProgress(DrawableTaikoHitObject swell, int numHits) - { - float completion = (float)numHits / swell.HitObject.RequiredHits; - - centreCircle.RotateTo((float)(completion * swell.HitObject.Duration / 8), 4000, Easing.OutQuint); + centreCircle.RotateTo((float)(completion * drawableSwell.HitObject.Duration / 8), 4000, Easing.OutQuint); expandingRing.ScaleTo(1f + Math.Min(target_ring_scale - 1f, (target_ring_scale - 1f) * completion * 1.3f), 260, Easing.OutQuint); @@ -132,23 +139,42 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default .FadeTo(completion / 8, 2000, Easing.OutQuint); } - public void AnimateSwellCompletion(ArmedState state) + private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state) { - const double transition_duration = 300; + if (!(drawableHitObject is DrawableSwell drawableSwell)) + return; - bodyContainer.FadeOut(transition_duration, Easing.OutQuad); - bodyContainer.ScaleTo(1.4f, transition_duration); - centreCircle.FadeOut(transition_duration, Easing.OutQuad); + Swell swell = drawableSwell.HitObject; + + using (BeginAbsoluteSequence(swell.StartTime)) + { + if (state == ArmedState.Idle) + expandingRing.FadeTo(0); + + const double ring_appear_offset = 100; + + targetRing.Delay(ring_appear_offset).ScaleTo(target_ring_scale, 400, Easing.OutQuint); + } + + using (BeginAbsoluteSequence(drawableSwell.HitStateUpdateTime)) + { + const double transition_duration = 300; + + bodyContainer.FadeOut(transition_duration, Easing.OutQuad); + bodyContainer.ScaleTo(1.4f, transition_duration); + centreCircle.FadeOut(transition_duration, Easing.OutQuad); + } } - public void AnimateSwellStart(DrawableTaikoHitObject swell) + protected override void Dispose(bool isDisposing) { - if (swell.IsHit == false) - expandingRing.FadeTo(0); + base.Dispose(isDisposing); - const double ring_appear_offset = 100; - - targetRing.Delay(ring_appear_offset).ScaleTo(target_ring_scale, 400, Easing.OutQuint); + if (drawableSwell.IsNotNull()) + { + drawableSwell.UpdateHitProgress -= animateSwellProgress; + drawableSwell.ApplyCustomUpdateState -= updateStateTransforms; + } } } } diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs index e487c5e051..60a0b1d951 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs @@ -12,11 +12,14 @@ using osu.Framework.Audio.Sample; using osu.Game.Audio; using osuTK; using osu.Game.Rulesets.Objects.Drawables; +using osu.Framework.Extensions.ObjectExtensions; namespace osu.Game.Rulesets.Taiko.Skinning.Legacy { - public partial class LegacySwell : Container, ISkinnableSwell + public partial class LegacySwell : Container { + private DrawableSwell drawableSwell = null!; + private Container bodyContainer = null!; private Sprite warning = null!; private Sprite spinnerCircle = null!; @@ -35,7 +38,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy } [BackgroundDependencyLoader] - private void load(ISkinSource skin, SkinManager skinManager) + private void load(DrawableHitObject hitObject, ISkinSource skin, SkinManager skinManager) { Child = new Container { @@ -96,49 +99,71 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy } }; + drawableSwell = (DrawableSwell)hitObject; + drawableSwell.UpdateHitProgress += animateSwellProgress; + drawableSwell.ApplyCustomUpdateState += updateStateTransforms; clearSample = skin.GetSample(new SampleInfo("spinner-osu")); } - public void AnimateSwellProgress(DrawableTaikoHitObject swell, int numHits) + private void animateSwellProgress(int numHits, int requiredHits) { - remainingHitsCountdown.Text = $"{swell.HitObject.RequiredHits - numHits}"; + remainingHitsCountdown.Text = $"{requiredHits - numHits}"; spinnerCircle.RotateTo(180f * numHits, 1000, Easing.OutQuint); } - public void AnimateSwellCompletion(ArmedState state) + private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state) { - const double clear_transition_duration = 300; + if (!(drawableHitObject is DrawableSwell drawableSwell)) + return; - bodyContainer.FadeOut(clear_transition_duration, Easing.OutQuad); + Swell swell = drawableSwell.HitObject; - if (state == ArmedState.Hit) + using (BeginAbsoluteSequence(swell.StartTime)) { - if (!samplePlayed) + if (state == ArmedState.Idle) { - clearSample?.Play(); - samplePlayed = true; + remainingHitsCountdown.Text = $"{swell.RequiredHits}"; + samplePlayed = false; } - clearAnimation - .FadeIn(clear_transition_duration, Easing.InQuad) - .ScaleTo(0.8f, clear_transition_duration, Easing.InQuad) - .Delay(700).FadeOut(200, Easing.OutQuad); + const double body_transition_duration = 100; + + warning.FadeOut(body_transition_duration); + bodyContainer.FadeIn(body_transition_duration); + shrinkingRing.ResizeTo(0.1f, swell.Duration); + } + + using (BeginAbsoluteSequence(drawableSwell.HitStateUpdateTime)) + { + const double clear_transition_duration = 300; + + bodyContainer.FadeOut(clear_transition_duration, Easing.OutQuad); + + if (state == ArmedState.Hit) + { + if (!samplePlayed) + { + clearSample?.Play(); + samplePlayed = true; + } + + clearAnimation + .FadeIn(clear_transition_duration, Easing.InQuad) + .ScaleTo(0.8f, clear_transition_duration, Easing.InQuad) + .Delay(700).FadeOut(200, Easing.OutQuad); + } } } - public void AnimateSwellStart(DrawableTaikoHitObject swell) + protected override void Dispose(bool isDisposing) { - if (swell.IsHit == false) + base.Dispose(isDisposing); + + if (drawableSwell.IsNotNull()) { - remainingHitsCountdown.Text = $"{swell.HitObject.RequiredHits}"; - samplePlayed = false; + drawableSwell.UpdateHitProgress -= animateSwellProgress; + drawableSwell.ApplyCustomUpdateState -= updateStateTransforms; } - - const double body_transition_duration = 100; - - warning.FadeOut(body_transition_duration); - bodyContainer.FadeIn(body_transition_duration); - shrinkingRing.ResizeTo(0.1f, swell.HitObject.Duration); } } } From ad2b469b143d74da7843a42563fe3e170a53d35c Mon Sep 17 00:00:00 2001 From: Nathan Du Date: Fri, 31 Jan 2025 18:52:19 +0800 Subject: [PATCH 011/107] remove spinner-osu.png workaround https://github.com/ppy/osu/issues/22084 --- osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs index 60a0b1d951..405b0b7692 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs @@ -86,8 +86,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy }, clearAnimation = new Sprite { - // File extension is included here because of a GetTexture limitation, see #21543 - Texture = skin.GetTexture("spinner-osu.png"), + Texture = skin.GetTexture("spinner-osu"), Anchor = Anchor.Centre, Origin = Anchor.Centre, Position = new Vector2(0f, -165f), From ac17b4065f06571cc3bf30cc7536e4746a78e9d3 Mon Sep 17 00:00:00 2001 From: Nathan Du Date: Fri, 31 Jan 2025 19:55:29 +0800 Subject: [PATCH 012/107] change legacy spinner animations to match stable Also removed a few fallbacks pointed out in code review that I don't understand. --- .../Skinning/Legacy/LegacySwell.cs | 46 +++++++++++++------ 1 file changed, 31 insertions(+), 15 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs index 405b0b7692..9ed21b1bb0 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs @@ -13,6 +13,7 @@ using osu.Game.Audio; using osuTK; using osu.Game.Rulesets.Objects.Drawables; using osu.Framework.Extensions.ObjectExtensions; +using System; namespace osu.Game.Rulesets.Taiko.Skinning.Legacy { @@ -23,10 +24,10 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy private Container bodyContainer = null!; private Sprite warning = null!; private Sprite spinnerCircle = null!; - private Sprite shrinkingRing = null!; + private Sprite approachCircle = null!; private Sprite clearAnimation = null!; private ISample? clearSample; - private LegacySpriteText remainingHitsCountdown = null!; + private LegacySpriteText remainingHitsText = null!; private bool samplePlayed; @@ -40,6 +41,8 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy [BackgroundDependencyLoader] private void load(DrawableHitObject hitObject, ISkinSource skin, SkinManager skinManager) { + var spinnerCircleProvider = skin.FindProvider(s => s.GetTexture("spinner-circle") != null); + Child = new Container { Anchor = Anchor.Centre, @@ -49,7 +52,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy { warning = new Sprite { - Texture = skin.GetTexture("spinner-warning") ?? skinManager.DefaultClassicSkin.GetTexture("spinner-circle"), + Texture = skin.GetTexture("spinner-warning"), Anchor = Anchor.Centre, Origin = Anchor.Centre, Scale = skin.GetTexture("spinner-warning") != null ? Vector2.One : new Vector2(0.18f), @@ -70,14 +73,14 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy Origin = Anchor.Centre, Scale = new Vector2(0.8f), }, - shrinkingRing = new Sprite + approachCircle = new Sprite { - Texture = skin.GetTexture("spinner-approachcircle") ?? skinManager.DefaultClassicSkin.GetTexture("spinner-approachcircle"), + Texture = skin.GetTexture("spinner-approachcircle"), Anchor = Anchor.Centre, Origin = Anchor.Centre, - Scale = Vector2.One, + Scale = new Vector2(1.86f * 0.8f), }, - remainingHitsCountdown = new LegacySpriteText(LegacyFont.Combo) + remainingHitsText = new LegacySpriteText(LegacyFont.Combo) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -106,8 +109,14 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy private void animateSwellProgress(int numHits, int requiredHits) { - remainingHitsCountdown.Text = $"{requiredHits - numHits}"; - spinnerCircle.RotateTo(180f * numHits, 1000, Easing.OutQuint); + remainingHitsText.Text = $"{requiredHits - numHits}"; + remainingHitsText.ScaleTo(1.6f - 0.6f * ((float)numHits / requiredHits), 60, Easing.OutQuad); + + spinnerCircle.ClearTransforms(); + spinnerCircle + .RotateTo(180f * numHits, 1000, Easing.OutQuint) + .ScaleTo(Math.Min(0.94f, spinnerCircle.Scale.X + 0.02f)) + .ScaleTo(0.8f, 400, Easing.OutQuad); } private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state) @@ -121,7 +130,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy { if (state == ArmedState.Idle) { - remainingHitsCountdown.Text = $"{swell.RequiredHits}"; + remainingHitsText.Text = $"{swell.RequiredHits}"; samplePlayed = false; } @@ -129,14 +138,17 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy warning.FadeOut(body_transition_duration); bodyContainer.FadeIn(body_transition_duration); - shrinkingRing.ResizeTo(0.1f, swell.Duration); + approachCircle.ResizeTo(0.1f * 0.8f, swell.Duration); } using (BeginAbsoluteSequence(drawableSwell.HitStateUpdateTime)) { const double clear_transition_duration = 300; + const double clear_fade_in = 120; - bodyContainer.FadeOut(clear_transition_duration, Easing.OutQuad); + bodyContainer + .FadeOut(clear_transition_duration, Easing.OutQuad) + .ScaleTo(1.05f, clear_transition_duration, Easing.OutQuad); if (state == ArmedState.Hit) { @@ -147,9 +159,13 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy } clearAnimation - .FadeIn(clear_transition_duration, Easing.InQuad) - .ScaleTo(0.8f, clear_transition_duration, Easing.InQuad) - .Delay(700).FadeOut(200, Easing.OutQuad); + .FadeIn(clear_fade_in) + .MoveTo(new Vector2(320, 240)) + .ScaleTo(0.4f) + .MoveTo(new Vector2(320, 150), clear_fade_in * 2, Easing.OutQuad) + .ScaleTo(1f, clear_fade_in * 2, Easing.Out) + .Delay(clear_fade_in * 3) + .FadeOut(clear_fade_in * 2.5); } } } From a62a84a30f7e92b9a855dfba7ddeb5c42a2bb442 Mon Sep 17 00:00:00 2001 From: Nathan Du Date: Fri, 31 Jan 2025 20:48:29 +0800 Subject: [PATCH 013/107] fix code style --- osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs | 6 ------ osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonSwell.cs | 2 +- osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs | 6 +++--- osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs | 2 +- 4 files changed, 5 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs index e0276db911..363a6bf8e1 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs @@ -156,12 +156,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables } } - protected override void UpdateStartTimeStateTransforms() - { - base.UpdateStartTimeStateTransforms(); - - } - protected override void UpdateHitStateTransforms(ArmedState state) { switch (state) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonSwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonSwell.cs index 65cd936e38..3b3684d219 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonSwell.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonSwell.cs @@ -10,7 +10,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon { protected override Drawable CreateCentreCircle() { - return new ArgonSwellCirclePiece() + return new ArgonSwellCirclePiece { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs index 852116cbfe..a588f866c6 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs @@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, Depth = 1, - Children = new Drawable[] + Children = new[] { expandingRing = new CircularContainer { @@ -118,7 +118,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default protected virtual Drawable CreateCentreCircle() { - return new SwellCirclePiece() + return new SwellCirclePiece { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -141,7 +141,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state) { - if (!(drawableHitObject is DrawableSwell drawableSwell)) + if (!(drawableHitObject is DrawableSwell)) return; Swell swell = drawableSwell.HitObject; diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs index 9ed21b1bb0..43b2d5c435 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs @@ -121,7 +121,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state) { - if (!(drawableHitObject is DrawableSwell drawableSwell)) + if (!(drawableHitObject is DrawableSwell)) return; Swell swell = drawableSwell.HitObject; From e794389fe83644323a563a343338e282783b53b1 Mon Sep 17 00:00:00 2001 From: Nathan Du Date: Sat, 1 Feb 2025 13:34:52 +0800 Subject: [PATCH 014/107] further adjust swell behavior The outstanding visual issues of the clear animation is fixed. The HandleUserInput state management is removed as it no longer seems necessary. --- .../Objects/Drawables/DrawableSwell.cs | 14 +-- .../Skinning/Legacy/LegacySwell.cs | 109 +++++++++--------- 2 files changed, 60 insertions(+), 63 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs index 363a6bf8e1..d75fdbc40a 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs @@ -158,21 +158,19 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables protected override void UpdateHitStateTransforms(ArmedState state) { + base.UpdateHitStateTransforms(state); + switch (state) { case ArmedState.Idle: - // Only for rewind support. Reallows user inputs if swell is rewound from being hit/missed to being idle. - HandleUserInput = true; break; case ArmedState.Miss: + this.Delay(300).FadeOut(); + break; + case ArmedState.Hit: - const int clear_animation_duration = 1200; - - // Postpone drawable hitobject expiration until it has animated/faded out. Inputs on the object are disallowed during this delay. - LifetimeEnd = Time.Current + clear_animation_duration; - HandleUserInput = false; - + this.Delay(660).FadeOut(); break; } } diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs index 43b2d5c435..0eb80d333f 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs @@ -41,64 +41,63 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy [BackgroundDependencyLoader] private void load(DrawableHitObject hitObject, ISkinSource skin, SkinManager skinManager) { - var spinnerCircleProvider = skin.FindProvider(s => s.GetTexture("spinner-circle") != null); - - Child = new Container + Children = new Drawable[] { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - - Children = new Drawable[] + warning = new Sprite { - warning = new Sprite - { - Texture = skin.GetTexture("spinner-warning"), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Scale = skin.GetTexture("spinner-warning") != null ? Vector2.One : new Vector2(0.18f), - }, - bodyContainer = new Container - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Position = new Vector2(200f, 100f), - Alpha = 0, + Texture = skin.GetTexture("spinner-warning"), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = skin.GetTexture("spinner-warning") != null ? Vector2.One : new Vector2(0.18f), + }, + new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Position = new Vector2(200f, 100f), - Children = new Drawable[] + Children = new Drawable[] + { + bodyContainer = new Container { - spinnerCircle = new Sprite + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Alpha = 0, + + Children = new Drawable[] { - Texture = skin.GetTexture("spinner-circle"), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Scale = new Vector2(0.8f), - }, - approachCircle = new Sprite - { - Texture = skin.GetTexture("spinner-approachcircle"), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Scale = new Vector2(1.86f * 0.8f), - }, - remainingHitsText = new LegacySpriteText(LegacyFont.Combo) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Position = new Vector2(0f, 165f), - Scale = Vector2.One, - }, - clearAnimation = new Sprite - { - Texture = skin.GetTexture("spinner-osu"), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Position = new Vector2(0f, -165f), - Scale = new Vector2(0.3f), - Alpha = 0, - }, - } + spinnerCircle = new Sprite + { + Texture = skin.GetTexture("spinner-circle"), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(0.8f), + }, + approachCircle = new Sprite + { + Texture = skin.GetTexture("spinner-approachcircle"), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(1.86f * 0.8f), + }, + remainingHitsText = new LegacySpriteText(LegacyFont.Combo) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Position = new Vector2(0f, 165f), + Scale = Vector2.One, + }, + } + }, + clearAnimation = new Sprite + { + Texture = skin.GetTexture("spinner-osu"), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Alpha = 0, + }, }, - } + }, }; drawableSwell = (DrawableSwell)hitObject; @@ -110,7 +109,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy private void animateSwellProgress(int numHits, int requiredHits) { remainingHitsText.Text = $"{requiredHits - numHits}"; - remainingHitsText.ScaleTo(1.6f - 0.6f * ((float)numHits / requiredHits), 60, Easing.OutQuad); + remainingHitsText.ScaleTo(1.6f - (0.6f * ((float)numHits / requiredHits)), 60, Easing.OutQuad); spinnerCircle.ClearTransforms(); spinnerCircle @@ -160,9 +159,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy clearAnimation .FadeIn(clear_fade_in) - .MoveTo(new Vector2(320, 240)) + .MoveTo(new Vector2(0, 0)) .ScaleTo(0.4f) - .MoveTo(new Vector2(320, 150), clear_fade_in * 2, Easing.OutQuad) + .MoveTo(new Vector2(0, -90), clear_fade_in * 2, Easing.OutQuad) .ScaleTo(1f, clear_fade_in * 2, Easing.Out) .Delay(clear_fade_in * 3) .FadeOut(clear_fade_in * 2.5); From 48e30f4ee80af5fd9c0e6e39bfd28d48a5df6ccf Mon Sep 17 00:00:00 2001 From: Nathan Du Date: Mon, 3 Feb 2025 09:49:37 +0800 Subject: [PATCH 015/107] remove skinning section swell delay test Replaced by TestHitSwellThenHitHit in TestSceneSwellJudgements. --- .../TestSceneDrawableSwellExpireDelay.cs | 41 ------------------- 1 file changed, 41 deletions(-) delete mode 100644 osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableSwellExpireDelay.cs diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableSwellExpireDelay.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableSwellExpireDelay.cs deleted file mode 100644 index ad78ed3b20..0000000000 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableSwellExpireDelay.cs +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System.Collections.Generic; -using NUnit.Framework; -using osu.Game.Rulesets.Replays; -using osu.Game.Rulesets.Scoring; -using osu.Game.Rulesets.Taiko.Objects; -using osu.Game.Rulesets.Taiko.Replays; -using osu.Game.Rulesets.Taiko.Tests.Judgements; - -namespace osu.Game.Rulesets.Taiko.Tests.Skinning -{ - public partial class TestSceneDrawableSwellExpireDelay : JudgementTest - { - [Test] - public void TestExpireDelay() - { - const double swell_start = 1000; - const double swell_duration = 1000; - - Swell swell = new Swell - { - StartTime = swell_start, - Duration = swell_duration, - }; - - Hit hit = new Hit { StartTime = swell_start + swell_duration + 50 }; - - List frames = new List - { - new TaikoReplayFrame(0), - new TaikoReplayFrame(2100, TaikoAction.LeftCentre), - }; - - PerformTest(frames, CreateBeatmap(swell, hit)); - - AssertResult(0, HitResult.Ok); - } - } -} From ccc446a8ca8d004ff74cba2b11bb0d438861f3ed Mon Sep 17 00:00:00 2001 From: Nathan Du Date: Tue, 4 Feb 2025 17:48:44 +0800 Subject: [PATCH 016/107] code cleanup --- .../Objects/Drawables/DrawableSwell.cs | 2 +- .../Argon/TaikoArgonSkinTransformer.cs | 2 +- .../Skinning/Legacy/LegacySwellCirclePiece.cs | 23 ------------------- .../Legacy/TaikoLegacySkinTransformer.cs | 2 +- .../TaikoSkinComponents.cs | 2 +- 5 files changed, 4 insertions(+), 27 deletions(-) delete mode 100644 osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwellCirclePiece.cs diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs index d75fdbc40a..1dde4b6f9c 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs @@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables AddInternal(ticks = new Container { RelativeSizeAxes = Axes.Both }); } - protected override SkinnableDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.SwellBody), + protected override SkinnableDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.Swell), _ => new DefaultSwell { Anchor = Anchor.Centre, diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs index b588a22d12..26bb1900b9 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs @@ -68,7 +68,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon case TaikoSkinComponents.TaikoExplosionOk: return new ArgonHitExplosion(taikoComponent.Component); - case TaikoSkinComponents.SwellBody: + case TaikoSkinComponents.Swell: return new ArgonSwell(); } diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwellCirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwellCirclePiece.cs deleted file mode 100644 index 40501d1d40..0000000000 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwellCirclePiece.cs +++ /dev/null @@ -1,23 +0,0 @@ -// 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.Sprites; -using osu.Game.Skinning; -using osuTK; - -namespace osu.Game.Rulesets.Taiko.Skinning.Legacy -{ - internal partial class LegacySwellCirclePiece : Sprite - { - [BackgroundDependencyLoader] - private void load(ISkinSource skin, SkinManager skinManager) - { - Texture = skin.GetTexture("spinner-warning") ?? skinManager.DefaultClassicSkin.GetTexture("spinner-circle"); - Anchor = Anchor.Centre; - Origin = Anchor.Centre; - Scale = skin.GetTexture("spinner-warning") != null ? Vector2.One : new Vector2(0.18f); - } - } -} diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs index 8fa4551fd4..c6221e0589 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs @@ -65,7 +65,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy case TaikoSkinComponents.DrumRollTick: return this.GetAnimation("sliderscorepoint", false, false); - case TaikoSkinComponents.SwellBody: + case TaikoSkinComponents.Swell: if (GetTexture("spinner-circle") != null) return new LegacySwell(); diff --git a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs index 05c6316a05..28133ffcb2 100644 --- a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs +++ b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs @@ -10,7 +10,7 @@ namespace osu.Game.Rulesets.Taiko RimHit, DrumRollBody, DrumRollTick, - SwellBody, + Swell, HitTarget, PlayfieldBackgroundLeft, PlayfieldBackgroundRight, From 7f8f528ae20da7ac8e0a0cb9a91e64e633b80c87 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 5 Feb 2025 16:26:21 +0900 Subject: [PATCH 017/107] Add helper for testing mod/freemod validity --- osu.Game.Tests/Mods/ModUtilsTest.cs | 35 ++++++++++++++++ .../Multiplayer/MultiplayerMatchSongSelect.cs | 5 --- .../OnlinePlay/OnlinePlaySongSelect.cs | 20 ++++----- osu.Game/Utils/ModUtils.cs | 41 +++++++++++++++++++ 4 files changed, 86 insertions(+), 15 deletions(-) diff --git a/osu.Game.Tests/Mods/ModUtilsTest.cs b/osu.Game.Tests/Mods/ModUtilsTest.cs index decb0a31ac..2964ca9396 100644 --- a/osu.Game.Tests/Mods/ModUtilsTest.cs +++ b/osu.Game.Tests/Mods/ModUtilsTest.cs @@ -6,6 +6,7 @@ using System.Linq; using Moq; using NUnit.Framework; using osu.Framework.Localisation; +using osu.Game.Online.Rooms; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; @@ -342,6 +343,40 @@ namespace osu.Game.Tests.Mods Assert.AreEqual(ModUtils.FormatScoreMultiplier(1.055).ToString(), "1.06x"); } + [Test] + public void TestRoomModValidity() + { + Assert.IsTrue(ModUtils.IsValidModForMatchType(new OsuModHardRock(), MatchType.Playlists)); + Assert.IsTrue(ModUtils.IsValidModForMatchType(new OsuModDoubleTime(), MatchType.Playlists)); + Assert.IsTrue(ModUtils.IsValidModForMatchType(new ModAdaptiveSpeed(), MatchType.Playlists)); + Assert.IsFalse(ModUtils.IsValidModForMatchType(new OsuModAutoplay(), MatchType.Playlists)); + Assert.IsFalse(ModUtils.IsValidModForMatchType(new OsuModTouchDevice(), MatchType.Playlists)); + + Assert.IsTrue(ModUtils.IsValidModForMatchType(new OsuModHardRock(), MatchType.HeadToHead)); + Assert.IsTrue(ModUtils.IsValidModForMatchType(new OsuModDoubleTime(), MatchType.HeadToHead)); + // For now, adaptive speed isn't allowed in multiplayer because it's a per-user rate adjustment. + Assert.IsFalse(ModUtils.IsValidModForMatchType(new ModAdaptiveSpeed(), MatchType.HeadToHead)); + Assert.IsFalse(ModUtils.IsValidModForMatchType(new OsuModAutoplay(), MatchType.HeadToHead)); + Assert.IsFalse(ModUtils.IsValidModForMatchType(new OsuModTouchDevice(), MatchType.HeadToHead)); + } + + [Test] + public void TestRoomFreeModValidity() + { + Assert.IsTrue(ModUtils.IsValidFreeModForMatchType(new OsuModHardRock(), MatchType.Playlists)); + Assert.IsTrue(ModUtils.IsValidFreeModForMatchType(new OsuModDoubleTime(), MatchType.Playlists)); + Assert.IsTrue(ModUtils.IsValidFreeModForMatchType(new ModAdaptiveSpeed(), MatchType.Playlists)); + Assert.IsFalse(ModUtils.IsValidFreeModForMatchType(new OsuModAutoplay(), MatchType.Playlists)); + Assert.IsFalse(ModUtils.IsValidFreeModForMatchType(new OsuModTouchDevice(), MatchType.Playlists)); + + Assert.IsTrue(ModUtils.IsValidFreeModForMatchType(new OsuModHardRock(), MatchType.HeadToHead)); + // For now, all rate adjustment mods aren't allowed as free mods in multiplayer. + Assert.IsFalse(ModUtils.IsValidFreeModForMatchType(new OsuModDoubleTime(), MatchType.HeadToHead)); + Assert.IsFalse(ModUtils.IsValidFreeModForMatchType(new ModAdaptiveSpeed(), MatchType.HeadToHead)); + Assert.IsFalse(ModUtils.IsValidFreeModForMatchType(new OsuModAutoplay(), MatchType.HeadToHead)); + Assert.IsFalse(ModUtils.IsValidFreeModForMatchType(new OsuModTouchDevice(), MatchType.HeadToHead)); + } + public abstract class CustomMod1 : Mod, IModCompatibilitySpecification { } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs index b42a58787d..7328e01026 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs @@ -11,7 +11,6 @@ using osu.Framework.Screens; using osu.Game.Graphics.UserInterface; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; -using osu.Game.Rulesets.Mods; using osu.Game.Screens.Select; namespace osu.Game.Screens.OnlinePlay.Multiplayer @@ -122,9 +121,5 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer } protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea(); - - protected override bool IsValidMod(Mod mod) => base.IsValidMod(mod) && mod.ValidForMultiplayer; - - protected override bool IsValidFreeMod(Mod mod) => base.IsValidFreeMod(mod) && mod.ValidForMultiplayerAsFreeMod; } } diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs index 4ca6abbf7d..1164c4c0fc 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs @@ -67,7 +67,7 @@ namespace osu.Game.Screens.OnlinePlay freeModSelect = new FreeModSelectOverlay { SelectedMods = { BindTarget = FreeMods }, - IsValidMod = IsValidFreeMod, + IsValidMod = isValidFreeMod, }; } @@ -144,10 +144,10 @@ namespace osu.Game.Screens.OnlinePlay private void onModsChanged(ValueChangedEvent> mods) { - FreeMods.Value = FreeMods.Value.Where(checkCompatibleFreeMod).ToList(); + FreeMods.Value = FreeMods.Value.Where(isValidFreeMod).ToList(); // Reset the validity delegate to update the overlay's display. - freeModSelect.IsValidMod = IsValidFreeMod; + freeModSelect.IsValidMod = isValidFreeMod; } private void onRulesetChanged(ValueChangedEvent ruleset) @@ -194,7 +194,7 @@ namespace osu.Game.Screens.OnlinePlay protected override ModSelectOverlay CreateModSelectOverlay() => new UserModSelectOverlay(OverlayColourScheme.Plum) { - IsValidMod = IsValidMod + IsValidMod = isValidMod }; protected override IEnumerable<(FooterButton button, OverlayContainer? overlay)> CreateSongSelectFooterButtons() @@ -217,18 +217,18 @@ namespace osu.Game.Screens.OnlinePlay /// /// The to check. /// Whether is a valid mod for online play. - protected virtual bool IsValidMod(Mod mod) => mod.HasImplementation && ModUtils.FlattenMod(mod).All(m => m.UserPlayable); + private bool isValidMod(Mod mod) => ModUtils.IsValidModForMatchType(mod, room.Type); /// /// Checks whether a given is valid for per-player free-mod selection. /// /// The to check. /// Whether is a selectable free-mod. - protected virtual bool IsValidFreeMod(Mod mod) => IsValidMod(mod) && checkCompatibleFreeMod(mod); - - private bool checkCompatibleFreeMod(Mod mod) - => Mods.Value.All(m => m.Acronym != mod.Acronym) // Mod must not be contained in the required mods. - && ModUtils.CheckCompatibleSet(Mods.Value.Append(mod).ToArray()); // Mod must be compatible with all the required mods. + private bool isValidFreeMod(Mod mod) => ModUtils.IsValidFreeModForMatchType(mod, room.Type) + // Mod must not be contained in the required mods. + && Mods.Value.All(m => m.Acronym != mod.Acronym) + // Mod must be compatible with all the required mods. + && ModUtils.CheckCompatibleSet(Mods.Value.Append(mod).ToArray()); protected override void Dispose(bool isDisposing) { diff --git a/osu.Game/Utils/ModUtils.cs b/osu.Game/Utils/ModUtils.cs index 15fc34b468..ac24bf2130 100644 --- a/osu.Game/Utils/ModUtils.cs +++ b/osu.Game/Utils/ModUtils.cs @@ -8,6 +8,7 @@ using System.Linq; using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Localisation; using osu.Game.Online.API; +using osu.Game.Online.Rooms; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; @@ -292,5 +293,45 @@ namespace osu.Game.Utils return rate; } + + /// + /// Determines whether a mod can be applied to playlist items in the given match type. + /// + /// The mod to test. + /// The match type. + public static bool IsValidModForMatchType(Mod mod, MatchType type) + { + if (mod.Type == ModType.System || !mod.UserPlayable || !mod.HasImplementation) + return false; + + switch (type) + { + case MatchType.Playlists: + return true; + + default: + return mod.ValidForMultiplayer; + } + } + + /// + /// Determines whether a mod can be applied as a free mod to playlist items in the given match type. + /// + /// The mod to test. + /// The match type. + public static bool IsValidFreeModForMatchType(Mod mod, MatchType type) + { + if (mod.Type == ModType.System || !mod.UserPlayable || !mod.HasImplementation) + return false; + + switch (type) + { + case MatchType.Playlists: + return true; + + default: + return mod.ValidForMultiplayerAsFreeMod; + } + } } } From b6731ff7738ede0985297fd69d5b32a82c66bdfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 5 Feb 2025 11:34:13 +0100 Subject: [PATCH 018/107] Add completion flag to `WizardOverlay` --- osu.Game/Overlays/WizardOverlay.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Overlays/WizardOverlay.cs b/osu.Game/Overlays/WizardOverlay.cs index 34ffa7bd77..2a881045fd 100644 --- a/osu.Game/Overlays/WizardOverlay.cs +++ b/osu.Game/Overlays/WizardOverlay.cs @@ -45,6 +45,8 @@ namespace osu.Game.Overlays private LoadingSpinner loading = null!; private ScheduledDelegate? loadingShowDelegate; + public bool Completed { get; private set; } + protected WizardOverlay(OverlayColourScheme scheme) : base(scheme) { @@ -221,6 +223,7 @@ namespace osu.Game.Overlays else { CurrentStepIndex = null; + Completed = true; Hide(); } From fff99a8b4008800ce5a870ac600618e84d8ffdc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 5 Feb 2025 11:54:26 +0100 Subject: [PATCH 019/107] Implement special exporter intended specifically for submission flows --- osu.Game/Database/LegacyBeatmapExporter.cs | 23 +++++--- .../Submission/SubmissionBeatmapExporter.cs | 58 +++++++++++++++++++ 2 files changed, 72 insertions(+), 9 deletions(-) create mode 100644 osu.Game/Screens/Edit/Submission/SubmissionBeatmapExporter.cs diff --git a/osu.Game/Database/LegacyBeatmapExporter.cs b/osu.Game/Database/LegacyBeatmapExporter.cs index 8f94fc9e63..e7e5ddb4d2 100644 --- a/osu.Game/Database/LegacyBeatmapExporter.cs +++ b/osu.Game/Database/LegacyBeatmapExporter.cs @@ -61,6 +61,20 @@ namespace osu.Game.Database Configuration = new LegacySkinDecoder().Decode(skinStreamReader) }; + MutateBeatmap(model, playableBeatmap); + + // Encode to legacy format + var stream = new MemoryStream(); + using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true)) + new LegacyBeatmapEncoder(playableBeatmap, beatmapSkin).Encode(sw); + + stream.Seek(0, SeekOrigin.Begin); + + return stream; + } + + protected virtual void MutateBeatmap(BeatmapSetInfo beatmapSet, IBeatmap playableBeatmap) + { // Convert beatmap elements to be compatible with legacy format // So we truncate time and position values to integers, and convert paths with multiple segments to Bézier curves @@ -145,15 +159,6 @@ namespace osu.Game.Database hasPath.Path.ControlPoints.Add(new PathControlPoint(position)); } } - - // Encode to legacy format - var stream = new MemoryStream(); - using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true)) - new LegacyBeatmapEncoder(playableBeatmap, beatmapSkin).Encode(sw); - - stream.Seek(0, SeekOrigin.Begin); - - return stream; } protected override string FileExtension => @".osz"; diff --git a/osu.Game/Screens/Edit/Submission/SubmissionBeatmapExporter.cs b/osu.Game/Screens/Edit/Submission/SubmissionBeatmapExporter.cs new file mode 100644 index 0000000000..3c50a1bf80 --- /dev/null +++ b/osu.Game/Screens/Edit/Submission/SubmissionBeatmapExporter.cs @@ -0,0 +1,58 @@ +// Copyright (c) ppy Pty Ltd . 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.Platform; +using osu.Game.Beatmaps; +using osu.Game.Database; +using osu.Game.Online.API.Requests.Responses; + +namespace osu.Game.Screens.Edit.Submission +{ + public class SubmissionBeatmapExporter : LegacyBeatmapExporter + { + private readonly uint? beatmapSetId; + private readonly HashSet? beatmapIds; + + public SubmissionBeatmapExporter(Storage storage) + : base(storage) + { + } + + public SubmissionBeatmapExporter(Storage storage, PutBeatmapSetResponse putBeatmapSetResponse) + : base(storage) + { + beatmapSetId = putBeatmapSetResponse.BeatmapSetId; + beatmapIds = putBeatmapSetResponse.BeatmapIds.Select(id => (int)id).ToHashSet(); + } + + protected override void MutateBeatmap(BeatmapSetInfo beatmapSet, IBeatmap playableBeatmap) + { + base.MutateBeatmap(beatmapSet, playableBeatmap); + + if (beatmapSetId != null && beatmapIds != null) + { + playableBeatmap.BeatmapInfo.BeatmapSet = beatmapSet; + playableBeatmap.BeatmapInfo.BeatmapSet!.OnlineID = (int)beatmapSetId; + + if (beatmapIds.Contains(playableBeatmap.BeatmapInfo.OnlineID)) + { + beatmapIds.Remove(playableBeatmap.BeatmapInfo.OnlineID); + return; + } + + if (playableBeatmap.BeatmapInfo.OnlineID > 0) + throw new InvalidOperationException(@"Encountered beatmap with ID that has not been assigned to it by the server!"); + + if (beatmapIds.Count == 0) + throw new InvalidOperationException(@"Ran out of new beatmap IDs to assign to unsubmitted beatmaps!"); + + int newId = beatmapIds.First(); + beatmapIds.Remove(newId); + playableBeatmap.BeatmapInfo.OnlineID = newId; + } + } + } +} From 78e85dc2c7f773ac8cbde2b226ec6ba9b8791672 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 5 Feb 2025 12:22:33 +0100 Subject: [PATCH 020/107] Add beatmap submission support --- .../Localisation/BeatmapSubmissionStrings.cs | 40 ++ osu.Game/Localisation/EditorStrings.cs | 10 + osu.Game/Screens/Edit/Editor.cs | 55 ++- .../Submission/BeatmapSubmissionScreen.cs | 422 ++++++++++++++++++ .../Submission/BeatmapSubmissionSettings.cs | 13 + .../Submission/ScreenSubmissionSettings.cs | 15 +- 6 files changed, 544 insertions(+), 11 deletions(-) create mode 100644 osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs create mode 100644 osu.Game/Screens/Edit/Submission/BeatmapSubmissionSettings.cs diff --git a/osu.Game/Localisation/BeatmapSubmissionStrings.cs b/osu.Game/Localisation/BeatmapSubmissionStrings.cs index a4c2b36894..50b65ab572 100644 --- a/osu.Game/Localisation/BeatmapSubmissionStrings.cs +++ b/osu.Game/Localisation/BeatmapSubmissionStrings.cs @@ -39,6 +39,31 @@ namespace osu.Game.Localisation /// public static LocalisableString SubmissionSettings => new TranslatableString(getKey(@"submission_settings"), @"Submission settings"); + /// + /// "Submit beatmap!" + /// + public static LocalisableString ConfirmSubmission => new TranslatableString(getKey(@"confirm_submission"), @"Submit beatmap!"); + + /// + /// "Exporting beatmap set in compatibility mode..." + /// + public static LocalisableString ExportingBeatmapSet => new TranslatableString(getKey(@"exporting_beatmap_set"), @"Exporting beatmap set in compatibility mode..."); + + /// + /// "Preparing beatmap set online..." + /// + public static LocalisableString PreparingBeatmapSet => new TranslatableString(getKey(@"preparing_beatmap_set"), @"Preparing beatmap set online..."); + + /// + /// "Uploading beatmap set contents..." + /// + public static LocalisableString UploadingBeatmapSetContents => new TranslatableString(getKey(@"uploading_beatmap_set_contents"), @"Uploading beatmap set contents..."); + + /// + /// "Updating local beatmap with relevant changes..." + /// + public static LocalisableString UpdatingLocalBeatmap => new TranslatableString(getKey(@"updating_local_beatmap"), @"Updating local beatmap with relevant changes..."); + /// /// "Before you continue, we ask you to check whether the content you are uploading has been cleared for upload. Please understand that you are responsible for the content you upload to the platform and if in doubt, should ask permission from the creators before uploading!" /// @@ -119,6 +144,21 @@ namespace osu.Game.Localisation /// public static LocalisableString LegacyExportDisclaimer => new TranslatableString(getKey(@"legacy_export_disclaimer"), @"Note: In order to make it possible for users of all osu! versions to enjoy your beatmap, it will be exported in a backwards-compatible format. While we have made efforts to ensure that that process keeps the beatmap playable in its intended form, some data related to features that previous versions of osu! do not support may be lost."); + /// + /// "Empty beatmaps cannot be submitted." + /// + public static LocalisableString EmptyBeatmapsCannotBeSubmitted => new TranslatableString(getKey(@"empty_beatmaps_cannot_be_submitted"), @"Empty beatmaps cannot be submitted."); + + /// + /// "Update beatmap!" + /// + public static LocalisableString UpdateBeatmap => new TranslatableString(getKey(@"update_beatmap"), @"Update beatmap!"); + + /// + /// "Upload NEW beatmap!" + /// + public static LocalisableString UploadNewBeatmap => new TranslatableString(getKey(@"upload_new_beatmap"), @"Upload NEW beatmap!"); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Localisation/EditorStrings.cs b/osu.Game/Localisation/EditorStrings.cs index 3b4026be11..2c834c38bb 100644 --- a/osu.Game/Localisation/EditorStrings.cs +++ b/osu.Game/Localisation/EditorStrings.cs @@ -69,6 +69,16 @@ namespace osu.Game.Localisation /// public static LocalisableString DeleteDifficulty => new TranslatableString(getKey(@"delete_difficulty"), @"Delete difficulty"); + /// + /// "Edit externally" + /// + public static LocalisableString EditExternally => new TranslatableString(getKey(@"edit_externally"), @"Edit externally"); + + /// + /// "Submit beatmap" + /// + public static LocalisableString SubmitBeatmap => new TranslatableString(getKey(@"submit_beatmap"), @"Submit beatmap"); + /// /// "setup" /// diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 3302fafbb8..c2a7264243 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -32,6 +32,7 @@ using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Configuration; using osu.Game.Database; +using osu.Game.Extensions; using osu.Game.Graphics.Cursor; using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; @@ -52,6 +53,7 @@ using osu.Game.Screens.Edit.Compose.Components.Timeline; using osu.Game.Screens.Edit.Design; using osu.Game.Screens.Edit.GameplayTest; using osu.Game.Screens.Edit.Setup; +using osu.Game.Screens.Edit.Submission; using osu.Game.Screens.Edit.Timing; using osu.Game.Screens.Edit.Verify; using osu.Game.Screens.OnlinePlay; @@ -111,6 +113,10 @@ namespace osu.Game.Screens.Edit [Resolved(canBeNull: true)] private INotificationOverlay notifications { get; set; } + [Resolved(canBeNull: true)] + [CanBeNull] + private LoginOverlay loginOverlay { get; set; } + [Resolved] private RealmAccess realm { get; set; } @@ -1309,11 +1315,22 @@ namespace osu.Game.Screens.Edit if (RuntimeInfo.IsDesktop) { - var externalEdit = new EditorMenuItem("Edit externally", MenuItemType.Standard, editExternally); + var externalEdit = new EditorMenuItem(EditorStrings.EditExternally, MenuItemType.Standard, editExternally); saveRelatedMenuItems.Add(externalEdit); yield return externalEdit; } + bool isSetMadeOfLegacyRulesetBeatmaps = (isNewBeatmap && Ruleset.Value.IsLegacyRuleset()) + || (!isNewBeatmap && Beatmap.Value.BeatmapSetInfo.Beatmaps.All(b => b.Ruleset.IsLegacyRuleset())); + bool submissionAvailable = api.EndpointConfiguration.BeatmapSubmissionServiceUrl != null; + + if (isSetMadeOfLegacyRulesetBeatmaps && submissionAvailable) + { + var upload = new EditorMenuItem(EditorStrings.SubmitBeatmap, MenuItemType.Standard, submitBeatmap); + saveRelatedMenuItems.Add(upload); + yield return upload; + } + yield return new OsuMenuItemSpacer(); yield return new EditorMenuItem(CommonStrings.Exit, MenuItemType.Standard, this.Exit); } @@ -1353,6 +1370,42 @@ namespace osu.Game.Screens.Edit } } + private void submitBeatmap() + { + if (api.State.Value != APIState.Online) + { + loginOverlay?.Show(); + return; + } + + if (!editorBeatmap.HitObjects.Any()) + { + notifications?.Post(new SimpleNotification + { + Text = BeatmapSubmissionStrings.EmptyBeatmapsCannotBeSubmitted, + }); + return; + } + + if (HasUnsavedChanges) + { + dialogOverlay.Push(new SaveRequiredPopupDialog(() => attemptMutationOperation(() => + { + if (!Save()) + return false; + + startSubmission(); + return true; + }))); + } + else + { + startSubmission(); + } + + void startSubmission() => this.Push(new BeatmapSubmissionScreen()); + } + private void exportBeatmap(bool legacy) { if (HasUnsavedChanges) diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs new file mode 100644 index 0000000000..796d975e4f --- /dev/null +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs @@ -0,0 +1,422 @@ +// Copyright (c) ppy Pty Ltd . 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.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using osu.Framework.Allocation; +using osu.Framework.Development; +using osu.Framework.Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Logging; +using osu.Framework.Platform; +using osu.Framework.Screens; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Drawables.Cards; +using osu.Game.Configuration; +using osu.Game.Database; +using osu.Game.Extensions; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.IO.Archives; +using osu.Game.Localisation; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Overlays; +using osu.Game.Overlays.Notifications; +using osu.Game.Screens.Menu; +using osuTK; + +namespace osu.Game.Screens.Edit.Submission +{ + public partial class BeatmapSubmissionScreen : OsuScreen + { + private BeatmapSubmissionOverlay overlay = null!; + + public override bool AllowUserExit => false; + + [Cached] + private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine); + + [Resolved] + private RealmAccess realmAccess { get; set; } = null!; + + [Resolved] + private Storage storage { get; set; } = null!; + + [Resolved] + private IAPIProvider api { get; set; } = null!; + + [Resolved] + private OsuConfigManager configManager { get; set; } = null!; + + [Resolved] + private OsuGame? game { get; set; } + + [Resolved] + private BeatmapManager beatmaps { get; set; } = null!; + + [Cached] + private BeatmapSubmissionSettings settings { get; } = new BeatmapSubmissionSettings(); + + private Container submissionProgress = null!; + private SubmissionStageProgress exportStep = null!; + private SubmissionStageProgress createSetStep = null!; + private SubmissionStageProgress uploadStep = null!; + private SubmissionStageProgress updateStep = null!; + private Container successContainer = null!; + private Container flashLayer = null!; + private RoundedButton backButton = null!; + + private uint? beatmapSetId; + + private SubmissionBeatmapExporter legacyBeatmapExporter = null!; + private ProgressNotification? exportProgressNotification; + private MemoryStream beatmapPackageStream = null!; + private ProgressNotification? updateProgressNotification; + + [BackgroundDependencyLoader] + private void load() + { + AddRangeInternal(new Drawable[] + { + overlay = new BeatmapSubmissionOverlay(), + submissionProgress = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Alpha = 0, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 0.6f, + Masking = true, + CornerRadius = 10, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background5, + }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Padding = new MarginPadding(20), + Spacing = new Vector2(5), + Children = new Drawable[] + { + createSetStep = new SubmissionStageProgress + { + StageDescription = BeatmapSubmissionStrings.PreparingBeatmapSet, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + }, + exportStep = new SubmissionStageProgress + { + StageDescription = BeatmapSubmissionStrings.ExportingBeatmapSet, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + }, + uploadStep = new SubmissionStageProgress + { + StageDescription = BeatmapSubmissionStrings.UploadingBeatmapSetContents, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + }, + updateStep = new SubmissionStageProgress + { + StageDescription = BeatmapSubmissionStrings.UpdatingLocalBeatmap, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + }, + successContainer = new Container + { + Padding = new MarginPadding(20), + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + AutoSizeAxes = Axes.Both, + AutoSizeDuration = 500, + AutoSizeEasing = Easing.OutQuint, + Masking = true, + CornerRadius = BeatmapCard.CORNER_RADIUS, + Child = flashLayer = new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + CornerRadius = BeatmapCard.CORNER_RADIUS, + Depth = float.MinValue, + Alpha = 0, + Child = new Box + { + RelativeSizeAxes = Axes.Both, + } + } + }, + backButton = new RoundedButton + { + Text = CommonStrings.Back, + Width = 150, + Action = this.Exit, + Enabled = { Value = false }, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + } + } + } + } + } + }); + + overlay.State.BindValueChanged(_ => + { + if (overlay.State.Value == Visibility.Hidden) + { + if (!overlay.Completed) + this.Exit(); + else + { + submissionProgress.FadeIn(200, Easing.OutQuint); + createBeatmapSet(); + } + } + }); + beatmapPackageStream = new MemoryStream(); + } + + private void createBeatmapSet() + { + bool beatmapHasOnlineId = Beatmap.Value.BeatmapSetInfo.OnlineID > 0; + + var createRequest = beatmapHasOnlineId + ? PutBeatmapSetRequest.UpdateExisting( + (uint)Beatmap.Value.BeatmapSetInfo.OnlineID, + Beatmap.Value.BeatmapSetInfo.Beatmaps.Where(b => b.OnlineID > 0).Select(b => (uint)b.OnlineID).ToArray(), + (uint)Beatmap.Value.BeatmapSetInfo.Beatmaps.Count(b => b.OnlineID <= 0), + settings.Target.Value) + : PutBeatmapSetRequest.CreateNew((uint)Beatmap.Value.BeatmapSetInfo.Beatmaps.Count, settings.Target.Value); + + createRequest.Success += async response => + { + createSetStep.SetCompleted(); + beatmapSetId = response.BeatmapSetId; + + // at this point the set has an assigned online ID. + // it's important to proactively store it to the realm database, + // so that in the event in further failures in the process, the online ID is not lost. + // losing it can incur creation of redundant new sets server-side, or even cause online ID confusion. + if (!beatmapHasOnlineId) + { + await realmAccess.WriteAsync(r => + { + var refetchedSet = r.Find(Beatmap.Value.BeatmapSetInfo.ID); + refetchedSet!.OnlineID = (int)beatmapSetId.Value; + }).ConfigureAwait(true); + } + + legacyBeatmapExporter = new SubmissionBeatmapExporter(storage, response); + await createBeatmapPackage(response.Files).ConfigureAwait(true); + }; + createRequest.Failure += ex => + { + createSetStep.SetFailed(ex.Message); + backButton.Enabled.Value = true; + Logger.Log($"Beatmap set submission failed on creation: {ex}"); + }; + + createSetStep.SetInProgress(); + api.Queue(createRequest); + } + + private async Task createBeatmapPackage(ICollection onlineFiles) + { + Debug.Assert(ThreadSafety.IsUpdateThread); + exportStep.SetInProgress(); + + try + { + await legacyBeatmapExporter.ExportToStreamAsync(Beatmap.Value.BeatmapSetInfo.ToLive(realmAccess), beatmapPackageStream, exportProgressNotification = new ProgressNotification()) + .ConfigureAwait(true); + } + catch (Exception ex) + { + exportStep.SetFailed(ex.Message); + Logger.Log($"Beatmap set submission failed on export: {ex}"); + backButton.Enabled.Value = true; + exportProgressNotification = null; + } + + exportStep.SetCompleted(); + exportProgressNotification = null; + + if (onlineFiles.Count > 0) + await patchBeatmapSet(onlineFiles).ConfigureAwait(true); + else + replaceBeatmapSet(); + } + + private async Task patchBeatmapSet(ICollection onlineFiles) + { + Debug.Assert(beatmapSetId != null); + + var onlineFilesByFilename = onlineFiles.ToDictionary(f => f.Filename, f => f.SHA2Hash); + + // disposing the `ArchiveReader` makes the underlying stream no longer readable which we don't want. + // make a local copy to defend against it. + using var archiveReader = new ZipArchiveReader(new MemoryStream(beatmapPackageStream.ToArray())); + var filesToUpdate = new HashSet(); + + foreach (string filename in archiveReader.Filenames) + { + string localHash = archiveReader.GetStream(filename).ComputeSHA2Hash(); + + if (!onlineFilesByFilename.Remove(filename, out string? onlineHash)) + { + filesToUpdate.Add(filename); + continue; + } + + if (localHash != onlineHash) + filesToUpdate.Add(filename); + } + + var changedFiles = new Dictionary(); + + foreach (string file in filesToUpdate) + changedFiles.Add(file, await archiveReader.GetStream(file).ReadAllBytesToArrayAsync().ConfigureAwait(true)); + + var patchRequest = new PatchBeatmapPackageRequest(beatmapSetId.Value); + patchRequest.FilesChanged.AddRange(changedFiles); + patchRequest.FilesDeleted.AddRange(onlineFilesByFilename.Keys); + patchRequest.Success += async () => + { + uploadStep.SetCompleted(); + + if (configManager.Get(OsuSetting.EditorSubmissionLoadInBrowserAfterSubmission)) + game?.OpenUrlExternally($"{api.EndpointConfiguration.WebsiteRootUrl}/beatmapsets/{beatmapSetId}"); + + await updateLocalBeatmap().ConfigureAwait(true); + }; + patchRequest.Failure += ex => + { + uploadStep.SetFailed(ex.Message); + Logger.Log($"Beatmap submission failed on upload: {ex}"); + backButton.Enabled.Value = true; + }; + patchRequest.Progressed += (current, total) => uploadStep.SetInProgress((float)current / total); + + api.Queue(patchRequest); + uploadStep.SetInProgress(); + } + + private void replaceBeatmapSet() + { + Debug.Assert(beatmapSetId != null); + + var uploadRequest = new ReplaceBeatmapPackageRequest(beatmapSetId.Value, beatmapPackageStream.ToArray()); + + uploadRequest.Success += async () => + { + uploadStep.SetCompleted(); + + if (configManager.Get(OsuSetting.EditorSubmissionLoadInBrowserAfterSubmission)) + game?.OpenUrlExternally($"{api.EndpointConfiguration.WebsiteRootUrl}/beatmapsets/{beatmapSetId}"); + + await updateLocalBeatmap().ConfigureAwait(true); + }; + uploadRequest.Failure += ex => + { + uploadStep.SetFailed(ex.Message); + Logger.Log($"Beatmap submission failed on upload: {ex}"); + backButton.Enabled.Value = true; + }; + uploadRequest.Progressed += (current, total) => uploadStep.SetInProgress((float)current / Math.Max(total, 1)); + + api.Queue(uploadRequest); + uploadStep.SetInProgress(); + } + + private async Task updateLocalBeatmap() + { + Debug.Assert(beatmapSetId != null); + updateStep.SetInProgress(); + + Live? importedSet; + + try + { + importedSet = await beatmaps.ImportAsUpdate( + updateProgressNotification = new ProgressNotification(), + new ImportTask(beatmapPackageStream, $"{beatmapSetId}.osz"), + Beatmap.Value.BeatmapSetInfo).ConfigureAwait(true); + } + catch (Exception ex) + { + updateStep.SetFailed(ex.Message); + Logger.Log($"Beatmap submission failed on local update: {ex}"); + Schedule(() => backButton.Enabled.Value = true); + return; + } + + updateStep.SetCompleted(); + backButton.Enabled.Value = true; + backButton.Action = () => + { + game?.PerformFromScreen(s => + { + if (s is OsuScreen osuScreen) + { + Debug.Assert(importedSet != null); + var targetBeatmap = importedSet.Value.Beatmaps.FirstOrDefault(b => b.DifficultyName == Beatmap.Value.BeatmapInfo.DifficultyName) + ?? importedSet.Value.Beatmaps.First(); + osuScreen.Beatmap.Value = beatmaps.GetWorkingBeatmap(targetBeatmap); + } + + s.Push(new EditorLoader()); + }, [typeof(MainMenu)]); + }; + showBeatmapCard(); + } + + private void showBeatmapCard() + { + Debug.Assert(beatmapSetId != null); + + var getBeatmapSetRequest = new GetBeatmapSetRequest((int)beatmapSetId.Value); + getBeatmapSetRequest.Success += beatmapSet => + { + LoadComponentAsync(new BeatmapCardExtra(beatmapSet, false), loaded => + { + successContainer.Add(loaded); + flashLayer.FadeOutFromOne(2000, Easing.OutQuint); + }); + }; + + api.Queue(getBeatmapSetRequest); + } + + protected override void Update() + { + base.Update(); + + if (exportProgressNotification != null && exportProgressNotification.Ongoing) + exportStep.SetInProgress(exportProgressNotification.Progress); + + if (updateProgressNotification != null && updateProgressNotification.Ongoing) + updateStep.SetInProgress(updateProgressNotification.Progress); + } + + public override void OnEntering(ScreenTransitionEvent e) + { + base.OnEntering(e); + + overlay.Show(); + } + } +} diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionSettings.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionSettings.cs new file mode 100644 index 0000000000..359dc11f39 --- /dev/null +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionSettings.cs @@ -0,0 +1,13 @@ +// 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.Bindables; +using osu.Game.Online.API.Requests; + +namespace osu.Game.Screens.Edit.Submission +{ + public class BeatmapSubmissionSettings + { + public Bindable Target { get; } = new Bindable(); + } +} diff --git a/osu.Game/Screens/Edit/Submission/ScreenSubmissionSettings.cs b/osu.Game/Screens/Edit/Submission/ScreenSubmissionSettings.cs index 72da94afa1..08b4d9f712 100644 --- a/osu.Game/Screens/Edit/Submission/ScreenSubmissionSettings.cs +++ b/osu.Game/Screens/Edit/Submission/ScreenSubmissionSettings.cs @@ -11,6 +11,7 @@ using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Localisation; +using osu.Game.Online.API.Requests; using osu.Game.Overlays; using osuTK; @@ -22,8 +23,10 @@ namespace osu.Game.Screens.Edit.Submission private readonly BindableBool notifyOnDiscussionReplies = new BindableBool(); private readonly BindableBool loadInBrowserAfterSubmission = new BindableBool(); + public override LocalisableString? NextStepText => BeatmapSubmissionStrings.ConfirmSubmission; + [BackgroundDependencyLoader] - private void load(OsuConfigManager configManager, OsuColour colours) + private void load(OsuConfigManager configManager, OsuColour colours, BeatmapSubmissionSettings settings) { configManager.BindWith(OsuSetting.EditorSubmissionNotifyOnDiscussionReplies, notifyOnDiscussionReplies); configManager.BindWith(OsuSetting.EditorSubmissionLoadInBrowserAfterSubmission, loadInBrowserAfterSubmission); @@ -39,6 +42,7 @@ namespace osu.Game.Screens.Edit.Submission { RelativeSizeAxes = Axes.X, Caption = BeatmapSubmissionStrings.BeatmapSubmissionTargetCaption, + Current = settings.Target, }, new FormCheckBox { @@ -60,14 +64,5 @@ namespace osu.Game.Screens.Edit.Submission } }); } - - private enum BeatmapSubmissionTarget - { - [LocalisableDescription(typeof(BeatmapSubmissionStrings), nameof(BeatmapSubmissionStrings.BeatmapSubmissionTargetWIP))] - WIP, - - [LocalisableDescription(typeof(BeatmapSubmissionStrings), nameof(BeatmapSubmissionStrings.BeatmapSubmissionTargetPending))] - Pending, - } } } From 84206e9ad8253ae0acc5169787fb6d6b516e16ff Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 6 Feb 2025 13:29:16 +0900 Subject: [PATCH 021/107] Initial support for freemod+freestyle --- .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 11 +-- .../Multiplayer/MultiplayerMatchSubScreen.cs | 89 ++++++++---------- .../Playlists/PlaylistsRoomSubScreen.cs | 93 +++++++++---------- 3 files changed, 86 insertions(+), 107 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index ce51bb3c21..312253774f 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.ComponentModel; -using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Audio; @@ -441,7 +440,9 @@ namespace osu.Game.Screens.OnlinePlay.Match var rulesetInstance = GetGameplayRuleset().CreateInstance(); // Remove any user mods that are no longer allowed. - Mod[] allowedMods = item.AllowedMods.Select(m => m.ToMod(rulesetInstance)).ToArray(); + Mod[] allowedMods = item.Freestyle + ? rulesetInstance.CreateAllMods().Where(m => ModUtils.IsValidFreeModForMatchType(m, Room.Type)).ToArray() + : item.AllowedMods.Select(m => m.ToMod(rulesetInstance)).ToArray(); Mod[] newUserMods = UserMods.Value.Where(m => allowedMods.Any(a => m.GetType() == a.GetType())).ToArray(); if (!newUserMods.SequenceEqual(UserMods.Value)) UserMods.Value = UserMods.Value.Where(m => allowedMods.Any(a => m.GetType() == a.GetType())).ToList(); @@ -455,12 +456,8 @@ namespace osu.Game.Screens.OnlinePlay.Match Mods.Value = GetGameplayMods().Select(m => m.ToMod(rulesetInstance)).ToArray(); Ruleset.Value = GetGameplayRuleset(); - bool freeMod = item.AllowedMods.Any(); bool freestyle = item.Freestyle; - - // For now, the game can never be in a state where freemod and freestyle are on at the same time. - // This will change, but due to the current implementation if this was to occur drawables will overlap so let's assert. - Debug.Assert(!freeMod || !freestyle); + bool freeMod = freestyle || item.AllowedMods.Any(); if (freeMod) { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index b803c5f28b..a16c5c9442 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -98,7 +98,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { new Drawable?[] { - // Participants column new GridContainer { RelativeSizeAxes = Axes.Both, @@ -118,9 +117,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer } } }, - // Spacer null, - // Beatmap column new GridContainer { RelativeSizeAxes = Axes.Both, @@ -147,67 +144,63 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer SelectedItem = SelectedItem } }, - new Drawable[] + new[] { - new Container + UserModsSection = new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Margin = new MarginPadding { Top = 10 }, - Children = new[] + Alpha = 0, + Children = new Drawable[] { - UserModsSection = new FillFlowContainer + new OverlinedHeader("Extra mods"), + new FillFlowContainer { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Alpha = 0, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(10, 0), Children = new Drawable[] { - new OverlinedHeader("Extra mods"), - new FillFlowContainer + new UserModSelectButton { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(10, 0), - Children = new Drawable[] - { - new UserModSelectButton - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Width = 90, - Text = "Select", - Action = ShowUserModSelect, - }, - new ModDisplay - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Current = UserMods, - Scale = new Vector2(0.8f), - }, - } + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Width = 90, + Text = "Select", + Action = ShowUserModSelect, }, - } - }, - UserStyleSection = new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Alpha = 0, - Children = new Drawable[] - { - new OverlinedHeader("Difficulty"), - UserStyleDisplayContainer = new Container + new ModDisplay { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y - } + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Current = UserMods, + Scale = new Vector2(0.8f), + }, } }, } } }, + new[] + { + UserStyleSection = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Margin = new MarginPadding { Top = 10 }, + Alpha = 0, + Children = new Drawable[] + { + new OverlinedHeader("Difficulty"), + UserStyleDisplayContainer = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y + } + } + }, + }, }, RowDimensions = new[] { @@ -218,9 +211,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer new Dimension(GridSizeMode.AutoSize), } }, - // Spacer null, - // Main right column new GridContainer { RelativeSizeAxes = Axes.Both, diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index 2195ed4722..957a51c467 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -146,7 +146,6 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { new Drawable?[] { - // Playlist items column new GridContainer { RelativeSizeAxes = Axes.Both, @@ -176,73 +175,66 @@ namespace osu.Game.Screens.OnlinePlay.Playlists new Dimension(), } }, - // Spacer null, - // Middle column (mods and leaderboard) new GridContainer { RelativeSizeAxes = Axes.Both, Content = new[] { - new Drawable[] + new[] { - new Container + UserModsSection = new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Margin = new MarginPadding { Bottom = 10 }, - Children = new[] + Alpha = 0, + Children = new Drawable[] { - UserModsSection = new FillFlowContainer + new OverlinedHeader("Extra mods"), + new FillFlowContainer { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Alpha = 0, - Margin = new MarginPadding { Bottom = 10 }, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(10, 0), Children = new Drawable[] { - new OverlinedHeader("Extra mods"), - new FillFlowContainer + new UserModSelectButton { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(10, 0), - Children = new Drawable[] - { - new UserModSelectButton - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Width = 90, - Text = "Select", - Action = ShowUserModSelect, - }, - new ModDisplay - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Current = UserMods, - Scale = new Vector2(0.8f), - }, - } - } + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Width = 90, + Text = "Select", + Action = ShowUserModSelect, + }, + new ModDisplay + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Current = UserMods, + Scale = new Vector2(0.8f), + }, } - }, - UserStyleSection = new FillFlowContainer + } + } + }, + }, + new[] + { + UserStyleSection = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Margin = new MarginPadding { Bottom = 10 }, + Alpha = 0, + Children = new Drawable[] + { + new OverlinedHeader("Difficulty"), + UserStyleDisplayContainer = new Container { RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Alpha = 0, - Children = new Drawable[] - { - new OverlinedHeader("Difficulty"), - UserStyleDisplayContainer = new Container - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y - } - } - }, + AutoSizeAxes = Axes.Y + } } }, }, @@ -273,12 +265,11 @@ namespace osu.Game.Screens.OnlinePlay.Playlists new Dimension(GridSizeMode.AutoSize), new Dimension(GridSizeMode.AutoSize), new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.AutoSize), new Dimension(), } }, - // Spacer null, - // Main right column new GridContainer { RelativeSizeAxes = Axes.Both, From d93f7509b6545489f405faf8e9a60f4800b7e040 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 6 Feb 2025 14:12:15 +0900 Subject: [PATCH 022/107] Fix participant panels not displaying mods from other rulesets correctly --- .../TestSceneMultiplayerParticipantsList.cs | 37 +++++++++++++++++++ .../Participants/ParticipantPanel.cs | 22 ++++++----- 2 files changed, 49 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs index 238a716f91..d3c967a8d5 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs @@ -12,11 +12,14 @@ using osu.Framework.Utils; using osu.Game.Graphics.Cursor; using osu.Game.Graphics.UserInterface; using osu.Game.Online; +using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; +using osu.Game.Rulesets.Catch.Mods; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Rulesets.Taiko.Mods; using osu.Game.Screens.OnlinePlay.Multiplayer.Participants; using osu.Game.Users; using osuTK; @@ -393,6 +396,40 @@ namespace osu.Game.Tests.Visual.Multiplayer }); } + [Test] + public void TestModsAndRuleset() + { + AddStep("add another user", () => + { + MultiplayerClient.AddUser(new APIUser + { + Id = 0, + Username = "User 0", + RulesetsStatistics = new Dictionary + { + { + Ruleset.Value.ShortName, + new UserStatistics { GlobalRank = RNG.Next(1, 100000), } + } + }, + CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + }); + + MultiplayerClient.ChangeUserBeatmapAvailability(0, BeatmapAvailability.LocallyAvailable()); + }); + + AddStep("set user styles", () => + { + MultiplayerClient.ChangeUserStyle(API.LocalUser.Value.OnlineID, 259, 1); + MultiplayerClient.ChangeUserMods(API.LocalUser.Value.OnlineID, + [new APIMod(new TaikoModConstantSpeed()), new APIMod(new TaikoModHidden()), new APIMod(new TaikoModFlashlight()), new APIMod(new TaikoModHardRock())]); + + MultiplayerClient.ChangeUserStyle(0, 259, 2); + MultiplayerClient.ChangeUserMods(0, + [new APIMod(new CatchModFloatingFruits()), new APIMod(new CatchModHidden()), new APIMod(new CatchModMirror())]); + }); + } + private void createNewParticipantsList() { ParticipantsList? participantsList = null; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs index a2657019a3..d6666de2b6 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -27,7 +28,6 @@ using osu.Game.Online.API; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osu.Game.Rulesets; -using osu.Game.Rulesets.Mods; using osu.Game.Screens.Play.HUD; using osu.Game.Users; using osu.Game.Users.Drawables; @@ -210,13 +210,18 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants const double fade_time = 50; MultiplayerPlaylistItem? currentItem = client.Room.GetCurrentItem(); - Ruleset? ruleset = currentItem != null ? rulesets.GetRuleset(currentItem.RulesetID)?.CreateInstance() : null; + Debug.Assert(currentItem != null); - int? currentModeRank = ruleset != null ? User.User?.RulesetsStatistics?.GetValueOrDefault(ruleset.ShortName)?.GlobalRank : null; - userRankText.Text = currentModeRank != null ? $"#{currentModeRank.Value:N0}" : string.Empty; + int userBeatmapId = User.BeatmapId ?? currentItem.BeatmapID; + int userRulesetId = User.RulesetId ?? currentItem.RulesetID; + Ruleset? userRuleset = rulesets.GetRuleset(userRulesetId)?.CreateInstance(); + Debug.Assert(userRuleset != null); userStateDisplay.UpdateStatus(User.State, User.BeatmapAvailability); + int? currentModeRank = User.User?.RulesetsStatistics?.GetValueOrDefault(userRuleset.ShortName)?.GlobalRank; + userRankText.Text = currentModeRank != null ? $"#{currentModeRank.Value:N0}" : string.Empty; + if ((User.BeatmapAvailability.State == DownloadState.LocallyAvailable) && (User.State != MultiplayerUserState.Spectating)) { userModsDisplay.FadeIn(fade_time); @@ -228,20 +233,17 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants userStyleDisplay.FadeOut(fade_time); } - if ((User.BeatmapId == null && User.RulesetId == null) || (User.BeatmapId == currentItem?.BeatmapID && User.RulesetId == currentItem?.RulesetID)) + if (userBeatmapId == currentItem.BeatmapID && userRulesetId == currentItem.RulesetID) userStyleDisplay.Style = null; else - userStyleDisplay.Style = (User.BeatmapId ?? currentItem?.BeatmapID ?? 0, User.RulesetId ?? currentItem?.RulesetID ?? 0); + userStyleDisplay.Style = (userBeatmapId, userRulesetId); kickButton.Alpha = client.IsHost && !User.Equals(client.LocalUser) ? 1 : 0; crown.Alpha = client.Room.Host?.Equals(User) == true ? 1 : 0; // If the mods are updated at the end of the frame, the flow container will skip a reflow cycle: https://github.com/ppy/osu-framework/issues/4187 // This looks particularly jarring here, so re-schedule the update to that start of our frame as a fix. - Schedule(() => - { - userModsDisplay.Current.Value = ruleset != null ? User.Mods.Select(m => m.ToMod(ruleset)).ToList() : Array.Empty(); - }); + Schedule(() => userModsDisplay.Current.Value = User.Mods.Select(m => m.ToMod(userRuleset)).ToList()); } public MenuItem[]? ContextMenuItems From 885ae7c735a82740710fce395a456d8e1280abf9 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 6 Feb 2025 14:25:08 +0900 Subject: [PATCH 023/107] Adjust styling --- .../Multiplayer/Participants/ParticipantPanel.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs index d6666de2b6..51ff52c63e 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs @@ -161,11 +161,18 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants Origin = Anchor.CentreRight, AutoSizeAxes = Axes.Both, Margin = new MarginPadding { Right = 70 }, + Spacing = new Vector2(2), Children = new Drawable[] { - userStyleDisplay = new StyleDisplayIcon(), + userStyleDisplay = new StyleDisplayIcon + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, userModsDisplay = new ModDisplay { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, Scale = new Vector2(0.5f), ExpansionMode = ExpansionMode.AlwaysContracted, } From e1a146d487300feb616adcf100563945aa3d17e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 6 Feb 2025 08:38:28 +0100 Subject: [PATCH 024/107] Remove unnecessary suppressions --- osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs b/osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs index bb9d32f77b..a59a708079 100644 --- a/osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs +++ b/osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs @@ -26,10 +26,8 @@ namespace osu.Game.Online.API.Requests public uint BeatmapSetID { get; } - // ReSharper disable once CollectionNeverUpdated.Global public Dictionary FilesChanged { get; } = new Dictionary(); - // ReSharper disable once CollectionNeverUpdated.Global public HashSet FilesDeleted { get; } = new HashSet(); public PatchBeatmapPackageRequest(uint beatmapSetId) From b7483b9442596fa367105f62effe81addb8bd8ec Mon Sep 17 00:00:00 2001 From: Layendan Date: Thu, 6 Feb 2025 07:25:45 -0700 Subject: [PATCH 025/107] Add playlist collection button w/ tests --- .../TestSceneAddPlaylistToCollectionButton.cs | 94 +++++++++++++++++++ .../AddPlaylistToCollectionButton.cs | 78 +++++++++++++++ .../Playlists/PlaylistsRoomSubScreen.cs | 10 ++ 3 files changed, 182 insertions(+) create mode 100644 osu.Game.Tests/Visual/Playlists/TestSceneAddPlaylistToCollectionButton.cs create mode 100644 osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs diff --git a/osu.Game.Tests/Visual/Playlists/TestSceneAddPlaylistToCollectionButton.cs b/osu.Game.Tests/Visual/Playlists/TestSceneAddPlaylistToCollectionButton.cs new file mode 100644 index 0000000000..acf2c4b3f9 --- /dev/null +++ b/osu.Game.Tests/Visual/Playlists/TestSceneAddPlaylistToCollectionButton.cs @@ -0,0 +1,94 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Diagnostics; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Graphics; +using osu.Framework.Platform; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Database; +using osu.Game.Online.Rooms; +using osu.Game.Overlays; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Osu; +using osu.Game.Screens.OnlinePlay.Playlists; +using osuTK; + +namespace osu.Game.Tests.Visual.Playlists +{ + public partial class TestSceneAddPlaylistToCollectionButton : OsuTestScene + { + private BeatmapManager manager = null!; + private BeatmapSetInfo importedBeatmap = null!; + private Room room = null!; + + [BackgroundDependencyLoader] + private void load(GameHost host, AudioManager audio) + { + Dependencies.Cache(new RealmRulesetStore(Realm)); + Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, API, audio, Resources, host, Beatmap.Default)); + Dependencies.Cache(Realm); + } + + [Cached(typeof(INotificationOverlay))] + private NotificationOverlay notificationOverlay = new NotificationOverlay + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + }; + + [SetUpSteps] + public void SetUpSteps() + { + importBeatmap(); + + setupRoom(); + + AddStep("create button", () => + { + AddRange(new Drawable[] + { + notificationOverlay, + new AddPlaylistToCollectionButton(room) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(300, 40), + } + }); + }); + } + + private void importBeatmap() => AddStep("import beatmap", () => + { + var beatmap = CreateBeatmap(new OsuRuleset().RulesetInfo); + + Debug.Assert(beatmap.BeatmapInfo.BeatmapSet != null); + + importedBeatmap = manager.Import(beatmap.BeatmapInfo.BeatmapSet)!.Value.Detach(); + }); + + private void setupRoom() => AddStep("setup room", () => + { + room = new Room + { + Name = "my awesome room", + MaxAttempts = 5, + Host = API.LocalUser.Value + }; + room.RecentParticipants = [room.Host]; + room.EndDate = DateTimeOffset.Now.AddMinutes(5); + room.Playlist = + [ + new PlaylistItem(importedBeatmap.Beatmaps.First()) + { + RulesetID = new OsuRuleset().RulesetInfo.OnlineID + } + ]; + }); + } +} diff --git a/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs new file mode 100644 index 0000000000..643e274335 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs @@ -0,0 +1,78 @@ +// 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 System.Threading.Tasks; +using osu.Framework.Allocation; +using osu.Framework.Extensions; +using osu.Game.Collections; +using osu.Game.Database; +using osu.Game.Graphics; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Online.Rooms; +using osu.Game.Overlays; +using osu.Game.Overlays.Notifications; + +namespace osu.Game.Screens.OnlinePlay.Playlists +{ + public partial class AddPlaylistToCollectionButton : RoundedButton + { + private readonly Room room; + + [Resolved] + private RealmAccess realmAccess { get; set; } = null!; + + [Resolved] + private BeatmapLookupCache beatmapLookupCache { get; set; } = null!; + + [Resolved(canBeNull: true)] + private INotificationOverlay? notifications { get; set; } + + public AddPlaylistToCollectionButton(Room room) + { + this.room = room; + Text = "Add Maps to Collection"; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + BackgroundColour = colours.Gray5; + + Action = () => + { + int[] ids = room.Playlist.Select(item => item.Beatmap.OnlineID).Where(onlineId => onlineId > 0).ToArray(); + + if (ids.Length == 0) + { + notifications?.Post(new SimpleErrorNotification { Text = "Cannot add local beatmaps" }); + return; + } + + beatmapLookupCache.GetBeatmapsAsync(ids).ContinueWith(task => Schedule(() => + { + var beatmaps = task.GetResultSafely().Where(item => item?.BeatmapSet != null).ToList(); + + var collection = realmAccess.Realm.All().FirstOrDefault(c => c.Name == room.Name); + + if (collection == null) + { + collection = new BeatmapCollection(room.Name, beatmaps.Select(i => i!.MD5Hash).Distinct().ToList()); + realmAccess.Realm.Write(() => realmAccess.Realm.Add(collection)); + notifications?.Post(new SimpleNotification { Text = $"Created new playlist: {room.Name}" }); + } + else + { + collection.ToLive(realmAccess).PerformWrite(c => + { + beatmaps = beatmaps.Where(i => !c.BeatmapMD5Hashes.Contains(i!.MD5Hash)).ToList(); + foreach (var item in beatmaps) + c.BeatmapMD5Hashes.Add(item!.MD5Hash); + notifications?.Post(new SimpleNotification { Text = $"Updated playlist: {room.Name}" }); + }); + } + }), TaskContinuationOptions.OnlyOnRanToCompletion); + }; + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index 9b4630ac0b..afab8a9721 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -153,11 +153,21 @@ namespace osu.Game.Screens.OnlinePlay.Playlists } } }, + new Drawable[] + { + new AddPlaylistToCollectionButton(Room) + { + Margin = new MarginPadding { Top = 5 }, + RelativeSizeAxes = Axes.X, + Size = new Vector2(1, 40) + } + } }, RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize), new Dimension(), + new Dimension(GridSizeMode.AutoSize), } }, // Spacer From 6769a74c92937eead5628a4a3b0080059c2d2e85 Mon Sep 17 00:00:00 2001 From: Layendan Date: Thu, 6 Feb 2025 17:23:06 -0700 Subject: [PATCH 026/107] Add loading in case cache lookup takes longer than expected --- .../Playlists/AddPlaylistToCollectionButton.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs index 643e274335..d28776cac2 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs @@ -8,6 +8,7 @@ using osu.Framework.Extensions; using osu.Game.Collections; using osu.Game.Database; using osu.Game.Graphics; +using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Online.Rooms; using osu.Game.Overlays; @@ -19,6 +20,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { private readonly Room room; + private LoadingLayer loading = null!; + [Resolved] private RealmAccess realmAccess { get; set; } = null!; @@ -39,6 +42,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { BackgroundColour = colours.Gray5; + Add(loading = new LoadingLayer(true, false)); + Action = () => { int[] ids = room.Playlist.Select(item => item.Beatmap.OnlineID).Where(onlineId => onlineId > 0).ToArray(); @@ -49,6 +54,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists return; } + Enabled.Value = false; + loading.Show(); beatmapLookupCache.GetBeatmapsAsync(ids).ContinueWith(task => Schedule(() => { var beatmaps = task.GetResultSafely().Where(item => item?.BeatmapSet != null).ToList(); @@ -71,6 +78,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists notifications?.Post(new SimpleNotification { Text = $"Updated playlist: {room.Name}" }); }); } + + loading.Hide(); + Enabled.Value = true; }), TaskContinuationOptions.OnlyOnRanToCompletion); }; } From 2aa930a36c87d579c1cde09a11a56342f8ca960f Mon Sep 17 00:00:00 2001 From: Layendan Date: Thu, 6 Feb 2025 17:46:49 -0700 Subject: [PATCH 027/107] Corrected notification strings --- .../OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs index d28776cac2..ab3e481f9f 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs @@ -66,7 +66,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { collection = new BeatmapCollection(room.Name, beatmaps.Select(i => i!.MD5Hash).Distinct().ToList()); realmAccess.Realm.Write(() => realmAccess.Realm.Add(collection)); - notifications?.Post(new SimpleNotification { Text = $"Created new playlist: {room.Name}" }); + notifications?.Post(new SimpleNotification { Text = $"Created new collection: {room.Name}" }); } else { @@ -75,7 +75,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists beatmaps = beatmaps.Where(i => !c.BeatmapMD5Hashes.Contains(i!.MD5Hash)).ToList(); foreach (var item in beatmaps) c.BeatmapMD5Hashes.Add(item!.MD5Hash); - notifications?.Post(new SimpleNotification { Text = $"Updated playlist: {room.Name}" }); + notifications?.Post(new SimpleNotification { Text = $"Updated collection: {room.Name}" }); }); } From bf57fef4125bba86595850a6ec13f5f1fcb3f980 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Feb 2025 17:50:32 +0900 Subject: [PATCH 028/107] Fix missing cached settings in `BetamapSubmissionOverlay` test --- .../Visual/Editing/TestSceneBeatmapSubmissionOverlay.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneBeatmapSubmissionOverlay.cs b/osu.Game.Tests/Visual/Editing/TestSceneBeatmapSubmissionOverlay.cs index e3e8c0de39..f83d424d56 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneBeatmapSubmissionOverlay.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneBeatmapSubmissionOverlay.cs @@ -24,7 +24,11 @@ namespace osu.Game.Tests.Visual.Editing Child = new DependencyProvidingContainer { RelativeSizeAxes = Axes.Both, - CachedDependencies = new[] { (typeof(ScreenFooter), (object)footer) }, + CachedDependencies = new[] + { + (typeof(ScreenFooter), (object)footer), + (typeof(BeatmapSubmissionSettings), new BeatmapSubmissionSettings()), + }, Children = new Drawable[] { receptor, From 46290ae76b81d953253b670c752968906ced6e5f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Feb 2025 18:05:47 +0900 Subject: [PATCH 029/107] Disallow changing beatmap / ruleset while submitting beatmap --- osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs index 9794402061..4c7ea39c35 100644 --- a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs @@ -40,6 +40,8 @@ namespace osu.Game.Screens.Edit.Submission public override bool AllowUserExit => false; + public override bool DisallowExternalBeatmapRulesetChanges => true; + [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine); From 12881f3f366625ecdd861c66e24120541c428995 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Feb 2025 18:06:31 +0900 Subject: [PATCH 030/107] Don't show informational screens for subsequent submissions These are historically only presented to the user when uploading a new beatmap for the first time. --- .../Edit/Submission/BeatmapSubmissionOverlay.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionOverlay.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionOverlay.cs index da2abd8c23..cf2fef25d5 100644 --- a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionOverlay.cs +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionOverlay.cs @@ -2,6 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Game.Beatmaps; using osu.Game.Overlays; using osu.Game.Localisation; @@ -15,10 +17,14 @@ namespace osu.Game.Screens.Edit.Submission } [BackgroundDependencyLoader] - private void load() + private void load(IBindable beatmap) { - AddStep(); - AddStep(); + if (beatmap.Value.BeatmapSetInfo.OnlineID <= 0) + { + AddStep(); + AddStep(); + } + AddStep(); Header.Title = BeatmapSubmissionStrings.BeatmapSubmissionTitle; From 95967a2fde5ae2015c206d35f3edc86eff318388 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Feb 2025 18:17:49 +0900 Subject: [PATCH 031/107] Adjust beatmap stream creation to make a bit more sense --- .../Edit/Submission/BeatmapSubmissionScreen.cs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs index 4c7ea39c35..44b2778869 100644 --- a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs @@ -76,10 +76,10 @@ namespace osu.Game.Screens.Edit.Submission private RoundedButton backButton = null!; private uint? beatmapSetId; + private MemoryStream? beatmapPackageStream; private SubmissionBeatmapExporter legacyBeatmapExporter = null!; private ProgressNotification? exportProgressNotification; - private MemoryStream beatmapPackageStream = null!; private ProgressNotification? updateProgressNotification; [BackgroundDependencyLoader] @@ -189,7 +189,6 @@ namespace osu.Game.Screens.Edit.Submission } } }); - beatmapPackageStream = new MemoryStream(); } private void createBeatmapSet() @@ -239,10 +238,12 @@ namespace osu.Game.Screens.Edit.Submission private async Task createBeatmapPackage(ICollection onlineFiles) { Debug.Assert(ThreadSafety.IsUpdateThread); + exportStep.SetInProgress(); try { + beatmapPackageStream = new MemoryStream(); await legacyBeatmapExporter.ExportToStreamAsync(Beatmap.Value.BeatmapSetInfo.ToLive(realmAccess), beatmapPackageStream, exportProgressNotification = new ProgressNotification()) .ConfigureAwait(true); } @@ -266,6 +267,7 @@ namespace osu.Game.Screens.Edit.Submission private async Task patchBeatmapSet(ICollection onlineFiles) { Debug.Assert(beatmapSetId != null); + Debug.Assert(beatmapPackageStream != null); var onlineFilesByFilename = onlineFiles.ToDictionary(f => f.Filename, f => f.SHA2Hash); @@ -320,6 +322,7 @@ namespace osu.Game.Screens.Edit.Submission private void replaceBeatmapSet() { Debug.Assert(beatmapSetId != null); + Debug.Assert(beatmapPackageStream != null); var uploadRequest = new ReplaceBeatmapPackageRequest(beatmapSetId.Value, beatmapPackageStream.ToArray()); @@ -347,6 +350,8 @@ namespace osu.Game.Screens.Edit.Submission private async Task updateLocalBeatmap() { Debug.Assert(beatmapSetId != null); + Debug.Assert(beatmapPackageStream != null); + updateStep.SetInProgress(); Live? importedSet; @@ -420,5 +425,12 @@ namespace osu.Game.Screens.Edit.Submission overlay.Show(); } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + beatmapPackageStream?.Dispose(); + } } } From 783ef0078533c7bf90f13675861a88c03c4242e5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Feb 2025 18:34:48 +0900 Subject: [PATCH 032/107] Change `BeatmapSubmissionScreen` to use global back button instead of custom implementation --- .../Submission/BeatmapSubmissionScreen.cs | 81 ++++++++++--------- 1 file changed, 44 insertions(+), 37 deletions(-) diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs index 44b2778869..8536ba5f02 100644 --- a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs @@ -21,7 +21,6 @@ using osu.Game.Beatmaps.Drawables.Cards; using osu.Game.Configuration; using osu.Game.Database; using osu.Game.Extensions; -using osu.Game.Graphics.UserInterfaceV2; using osu.Game.IO.Archives; using osu.Game.Localisation; using osu.Game.Online.API; @@ -38,8 +37,6 @@ namespace osu.Game.Screens.Edit.Submission { private BeatmapSubmissionOverlay overlay = null!; - public override bool AllowUserExit => false; - public override bool DisallowExternalBeatmapRulesetChanges => true; [Cached] @@ -73,7 +70,6 @@ namespace osu.Game.Screens.Edit.Submission private SubmissionStageProgress updateStep = null!; private Container successContainer = null!; private Container flashLayer = null!; - private RoundedButton backButton = null!; private uint? beatmapSetId; private MemoryStream? beatmapPackageStream; @@ -82,6 +78,8 @@ namespace osu.Game.Screens.Edit.Submission private ProgressNotification? exportProgressNotification; private ProgressNotification? updateProgressNotification; + private Live? importedSet; + [BackgroundDependencyLoader] private void load() { @@ -161,15 +159,6 @@ namespace osu.Game.Screens.Edit.Submission } } }, - backButton = new RoundedButton - { - Text = CommonStrings.Back, - Width = 150, - Action = this.Exit, - Enabled = { Value = false }, - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - } } } } @@ -181,7 +170,10 @@ namespace osu.Game.Screens.Edit.Submission if (overlay.State.Value == Visibility.Hidden) { if (!overlay.Completed) + { + allowExit(); this.Exit(); + } else { submissionProgress.FadeIn(200, Easing.OutQuint); @@ -227,8 +219,8 @@ namespace osu.Game.Screens.Edit.Submission createRequest.Failure += ex => { createSetStep.SetFailed(ex.Message); - backButton.Enabled.Value = true; Logger.Log($"Beatmap set submission failed on creation: {ex}"); + allowExit(); }; createSetStep.SetInProgress(); @@ -250,9 +242,9 @@ namespace osu.Game.Screens.Edit.Submission catch (Exception ex) { exportStep.SetFailed(ex.Message); - Logger.Log($"Beatmap set submission failed on export: {ex}"); - backButton.Enabled.Value = true; exportProgressNotification = null; + Logger.Log($"Beatmap set submission failed on export: {ex}"); + allowExit(); } exportStep.SetCompleted(); @@ -311,7 +303,7 @@ namespace osu.Game.Screens.Edit.Submission { uploadStep.SetFailed(ex.Message); Logger.Log($"Beatmap submission failed on upload: {ex}"); - backButton.Enabled.Value = true; + allowExit(); }; patchRequest.Progressed += (current, total) => uploadStep.SetInProgress((float)current / total); @@ -339,7 +331,7 @@ namespace osu.Game.Screens.Edit.Submission { uploadStep.SetFailed(ex.Message); Logger.Log($"Beatmap submission failed on upload: {ex}"); - backButton.Enabled.Value = true; + allowExit(); }; uploadRequest.Progressed += (current, total) => uploadStep.SetInProgress((float)current / Math.Max(total, 1)); @@ -354,8 +346,6 @@ namespace osu.Game.Screens.Edit.Submission updateStep.SetInProgress(); - Live? importedSet; - try { importedSet = await beatmaps.ImportAsUpdate( @@ -367,28 +357,13 @@ namespace osu.Game.Screens.Edit.Submission { updateStep.SetFailed(ex.Message); Logger.Log($"Beatmap submission failed on local update: {ex}"); - Schedule(() => backButton.Enabled.Value = true); + allowExit(); return; } updateStep.SetCompleted(); - backButton.Enabled.Value = true; - backButton.Action = () => - { - game?.PerformFromScreen(s => - { - if (s is OsuScreen osuScreen) - { - Debug.Assert(importedSet != null); - var targetBeatmap = importedSet.Value.Beatmaps.FirstOrDefault(b => b.DifficultyName == Beatmap.Value.BeatmapInfo.DifficultyName) - ?? importedSet.Value.Beatmaps.First(); - osuScreen.Beatmap.Value = beatmaps.GetWorkingBeatmap(targetBeatmap); - } - - s.Push(new EditorLoader()); - }, [typeof(MainMenu)]); - }; showBeatmapCard(); + allowExit(); } private void showBeatmapCard() @@ -408,6 +383,11 @@ namespace osu.Game.Screens.Edit.Submission api.Queue(getBeatmapSetRequest); } + private void allowExit() + { + BackButtonVisibility.Value = true; + } + protected override void Update() { base.Update(); @@ -419,6 +399,33 @@ namespace osu.Game.Screens.Edit.Submission updateStep.SetInProgress(updateProgressNotification.Progress); } + public override bool OnExiting(ScreenExitEvent e) + { + // We probably want a method of cancelling in the future… + if (!BackButtonVisibility.Value) + return true; + + if (importedSet != null) + { + game?.PerformFromScreen(s => + { + if (s is OsuScreen osuScreen) + { + Debug.Assert(importedSet != null); + var targetBeatmap = importedSet.Value.Beatmaps.FirstOrDefault(b => b.DifficultyName == Beatmap.Value.BeatmapInfo.DifficultyName) + ?? importedSet.Value.Beatmaps.First(); + osuScreen.Beatmap.Value = beatmaps.GetWorkingBeatmap(targetBeatmap); + } + + s.Push(new EditorLoader()); + }, [typeof(MainMenu)]); + + return true; + } + + return base.OnExiting(e); + } + public override void OnEntering(ScreenTransitionEvent e) { base.OnEntering(e); From ce88ecfb3cbfb2df90663b6f7ac1d3b8021da22e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Feb 2025 18:39:01 +0900 Subject: [PATCH 033/107] Adjust timeouts to be much higher for upload requests It seems that right now these timeouts do not check for actual data movement, which is to say if a user with a very slow connection is uploading and it takes more than `Timeout`, their upload will fail. For now let's set these values high enough that most users will not be affected. --- osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs | 2 +- osu.Game/Online/API/Requests/ReplaceBeatmapPackageRequest.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs b/osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs index 5728dbe3fa..df3c9d071c 100644 --- a/osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs +++ b/osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs @@ -46,7 +46,7 @@ namespace osu.Game.Online.API.Requests foreach (string filename in FilesDeleted) request.AddParameter(@"filesDeleted", filename, RequestParameterType.Form); - request.Timeout = 60_000; + request.Timeout = 600_000; return request; } } diff --git a/osu.Game/Online/API/Requests/ReplaceBeatmapPackageRequest.cs b/osu.Game/Online/API/Requests/ReplaceBeatmapPackageRequest.cs index 2e224ce602..de8af6a623 100644 --- a/osu.Game/Online/API/Requests/ReplaceBeatmapPackageRequest.cs +++ b/osu.Game/Online/API/Requests/ReplaceBeatmapPackageRequest.cs @@ -38,7 +38,7 @@ namespace osu.Game.Online.API.Requests var request = base.CreateWebRequest(); request.AddFile(@"beatmapArchive", oszPackage); request.Method = HttpMethod.Put; - request.Timeout = 60_000; + request.Timeout = 600_000; return request; } } From 753eae426d7c33978621025424b8dd43081a31fc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Feb 2025 18:42:36 +0900 Subject: [PATCH 034/107] Update strings --- .../Localisation/BeatmapSubmissionStrings.cs | 20 +++++++++---------- .../Submission/BeatmapSubmissionScreen.cs | 8 ++++---- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/osu.Game/Localisation/BeatmapSubmissionStrings.cs b/osu.Game/Localisation/BeatmapSubmissionStrings.cs index 50b65ab572..3abe8cc515 100644 --- a/osu.Game/Localisation/BeatmapSubmissionStrings.cs +++ b/osu.Game/Localisation/BeatmapSubmissionStrings.cs @@ -45,24 +45,24 @@ namespace osu.Game.Localisation public static LocalisableString ConfirmSubmission => new TranslatableString(getKey(@"confirm_submission"), @"Submit beatmap!"); /// - /// "Exporting beatmap set in compatibility mode..." + /// "Exporting beatmap for compatibility..." /// - public static LocalisableString ExportingBeatmapSet => new TranslatableString(getKey(@"exporting_beatmap_set"), @"Exporting beatmap set in compatibility mode..."); + public static LocalisableString Exporting => new TranslatableString(getKey(@"exporting"), @"Exporting beatmap for compatibility..."); /// - /// "Preparing beatmap set online..." + /// "Preparing for upload..." /// - public static LocalisableString PreparingBeatmapSet => new TranslatableString(getKey(@"preparing_beatmap_set"), @"Preparing beatmap set online..."); + public static LocalisableString Preparing => new TranslatableString(getKey(@"preparing"), @"Preparing for upload..."); /// - /// "Uploading beatmap set contents..." + /// "Uploading beatmap contents..." /// - public static LocalisableString UploadingBeatmapSetContents => new TranslatableString(getKey(@"uploading_beatmap_set_contents"), @"Uploading beatmap set contents..."); + public static LocalisableString Uploading => new TranslatableString(getKey(@"uploading"), @"Uploading beatmap contents..."); /// - /// "Updating local beatmap with relevant changes..." + /// "Finishing up..." /// - public static LocalisableString UpdatingLocalBeatmap => new TranslatableString(getKey(@"updating_local_beatmap"), @"Updating local beatmap with relevant changes..."); + public static LocalisableString Finishing => new TranslatableString(getKey(@"finishing"), @"Finishing up..."); /// /// "Before you continue, we ask you to check whether the content you are uploading has been cleared for upload. Please understand that you are responsible for the content you upload to the platform and if in doubt, should ask permission from the creators before uploading!" @@ -140,9 +140,9 @@ namespace osu.Game.Localisation public static LocalisableString LoadInBrowserAfterSubmission => new TranslatableString(getKey(@"load_in_browser_after_submission"), @"Load in browser after submission"); /// - /// "Note: In order to make it possible for users of all osu! versions to enjoy your beatmap, it will be exported in a backwards-compatible format. While we have made efforts to ensure that that process keeps the beatmap playable in its intended form, some data related to features that previous versions of osu! do not support may be lost." + /// "Note: In order to make it possible for users of all osu! versions to enjoy your beatmap, it will be exported in a backwards-compatible format. While we have made efforts to ensure that process keeps the beatmap playable in its intended form, some data related to features that previous versions of osu! do not support may be lost." /// - public static LocalisableString LegacyExportDisclaimer => new TranslatableString(getKey(@"legacy_export_disclaimer"), @"Note: In order to make it possible for users of all osu! versions to enjoy your beatmap, it will be exported in a backwards-compatible format. While we have made efforts to ensure that that process keeps the beatmap playable in its intended form, some data related to features that previous versions of osu! do not support may be lost."); + public static LocalisableString LegacyExportDisclaimer => new TranslatableString(getKey(@"legacy_export_disclaimer"), @"Note: In order to make it possible for users of all osu! versions to enjoy your beatmap, it will be exported in a backwards-compatible format. While we have made efforts to ensure that process keeps the beatmap playable in its intended form, some data related to features that previous versions of osu! do not support may be lost."); /// /// "Empty beatmaps cannot be submitted." diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs index 8536ba5f02..41c875ac1f 100644 --- a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs @@ -114,25 +114,25 @@ namespace osu.Game.Screens.Edit.Submission { createSetStep = new SubmissionStageProgress { - StageDescription = BeatmapSubmissionStrings.PreparingBeatmapSet, + StageDescription = BeatmapSubmissionStrings.Preparing, Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, }, exportStep = new SubmissionStageProgress { - StageDescription = BeatmapSubmissionStrings.ExportingBeatmapSet, + StageDescription = BeatmapSubmissionStrings.Exporting, Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, }, uploadStep = new SubmissionStageProgress { - StageDescription = BeatmapSubmissionStrings.UploadingBeatmapSetContents, + StageDescription = BeatmapSubmissionStrings.Uploading, Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, }, updateStep = new SubmissionStageProgress { - StageDescription = BeatmapSubmissionStrings.UpdatingLocalBeatmap, + StageDescription = BeatmapSubmissionStrings.Finishing, Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, }, From aad12024b0db512c32b77cd2b48dd50a64cb7d05 Mon Sep 17 00:00:00 2001 From: Layendan Date: Fri, 7 Feb 2025 03:13:51 -0700 Subject: [PATCH 035/107] remove using cache, improve tests, and revert loading --- .../TestSceneAddPlaylistToCollectionButton.cs | 37 ++++++++--- .../AddPlaylistToCollectionButton.cs | 62 +++++++------------ 2 files changed, 50 insertions(+), 49 deletions(-) diff --git a/osu.Game.Tests/Visual/Playlists/TestSceneAddPlaylistToCollectionButton.cs b/osu.Game.Tests/Visual/Playlists/TestSceneAddPlaylistToCollectionButton.cs index acf2c4b3f9..f18488170d 100644 --- a/osu.Game.Tests/Visual/Playlists/TestSceneAddPlaylistToCollectionButton.cs +++ b/osu.Game.Tests/Visual/Playlists/TestSceneAddPlaylistToCollectionButton.cs @@ -4,12 +4,14 @@ using System; using System.Diagnostics; using System.Linq; +using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Graphics; using osu.Framework.Platform; using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Collections; using osu.Game.Database; using osu.Game.Online.Rooms; using osu.Game.Overlays; @@ -17,14 +19,17 @@ using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; using osu.Game.Screens.OnlinePlay.Playlists; using osuTK; +using osuTK.Input; +using SharpCompress; namespace osu.Game.Tests.Visual.Playlists { - public partial class TestSceneAddPlaylistToCollectionButton : OsuTestScene + public partial class TestSceneAddPlaylistToCollectionButton : OsuManualInputManagerTestScene { private BeatmapManager manager = null!; private BeatmapSetInfo importedBeatmap = null!; private Room room = null!; + private AddPlaylistToCollectionButton button = null!; [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) @@ -32,6 +37,8 @@ namespace osu.Game.Tests.Visual.Playlists Dependencies.Cache(new RealmRulesetStore(Realm)); Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, API, audio, Resources, host, Beatmap.Default)); Dependencies.Cache(Realm); + + Add(notificationOverlay); } [Cached(typeof(INotificationOverlay))] @@ -44,25 +51,37 @@ namespace osu.Game.Tests.Visual.Playlists [SetUpSteps] public void SetUpSteps() { + AddStep("clear realm", () => Realm.Realm.Write(() => Realm.Realm.RemoveAll())); + + AddStep("clear notifications", () => notificationOverlay.AllNotifications.Empty()); + importBeatmap(); setupRoom(); AddStep("create button", () => { - AddRange(new Drawable[] + Add(button = new AddPlaylistToCollectionButton(room) { - notificationOverlay, - new AddPlaylistToCollectionButton(room) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(300, 40), - } + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(300, 40), }); }); } + [Test] + public void TestButtonFlow() + { + AddStep("move mouse to button", () => InputManager.MoveMouseTo(button)); + + AddStep("click button", () => InputManager.Click(MouseButton.Left)); + + AddAssert("notification shown", () => notificationOverlay.AllNotifications.FirstOrDefault(n => n.Text.ToString().StartsWith("Created", StringComparison.Ordinal)) != null); + + AddAssert("realm is updated", () => Realm.Realm.All().FirstOrDefault(c => c.Name == room.Name) != null); + } + private void importBeatmap() => AddStep("import beatmap", () => { var beatmap = CreateBeatmap(new OsuRuleset().RulesetInfo); diff --git a/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs index ab3e481f9f..8801d73e9e 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs @@ -2,17 +2,15 @@ // See the LICENCE file in the repository root for full licence text. using System.Linq; -using System.Threading.Tasks; using osu.Framework.Allocation; -using osu.Framework.Extensions; +using osu.Game.Beatmaps; using osu.Game.Collections; using osu.Game.Database; -using osu.Game.Graphics; -using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; +using Realms; namespace osu.Game.Screens.OnlinePlay.Playlists { @@ -20,14 +18,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { private readonly Room room; - private LoadingLayer loading = null!; - [Resolved] private RealmAccess realmAccess { get; set; } = null!; - [Resolved] - private BeatmapLookupCache beatmapLookupCache { get; set; } = null!; - [Resolved(canBeNull: true)] private INotificationOverlay? notifications { get; set; } @@ -38,12 +31,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists } [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load() { - BackgroundColour = colours.Gray5; - - Add(loading = new LoadingLayer(true, false)); - Action = () => { int[] ids = room.Playlist.Select(item => item.Beatmap.OnlineID).Where(onlineId => onlineId > 0).ToArray(); @@ -54,34 +43,27 @@ namespace osu.Game.Screens.OnlinePlay.Playlists return; } - Enabled.Value = false; - loading.Show(); - beatmapLookupCache.GetBeatmapsAsync(ids).ContinueWith(task => Schedule(() => + string filter = string.Join(" OR ", ids.Select(id => $"(OnlineID == {id})")); + var beatmaps = realmAccess.Realm.All().Filter(filter).ToList(); + + var collection = realmAccess.Realm.All().FirstOrDefault(c => c.Name == room.Name); + + if (collection == null) { - var beatmaps = task.GetResultSafely().Where(item => item?.BeatmapSet != null).ToList(); - - var collection = realmAccess.Realm.All().FirstOrDefault(c => c.Name == room.Name); - - if (collection == null) + collection = new BeatmapCollection(room.Name, beatmaps.Select(i => i.MD5Hash).Distinct().ToList()); + realmAccess.Realm.Write(() => realmAccess.Realm.Add(collection)); + notifications?.Post(new SimpleNotification { Text = $"Created new collection: {room.Name}" }); + } + else + { + collection.ToLive(realmAccess).PerformWrite(c => { - collection = new BeatmapCollection(room.Name, beatmaps.Select(i => i!.MD5Hash).Distinct().ToList()); - realmAccess.Realm.Write(() => realmAccess.Realm.Add(collection)); - notifications?.Post(new SimpleNotification { Text = $"Created new collection: {room.Name}" }); - } - else - { - collection.ToLive(realmAccess).PerformWrite(c => - { - beatmaps = beatmaps.Where(i => !c.BeatmapMD5Hashes.Contains(i!.MD5Hash)).ToList(); - foreach (var item in beatmaps) - c.BeatmapMD5Hashes.Add(item!.MD5Hash); - notifications?.Post(new SimpleNotification { Text = $"Updated collection: {room.Name}" }); - }); - } - - loading.Hide(); - Enabled.Value = true; - }), TaskContinuationOptions.OnlyOnRanToCompletion); + beatmaps = beatmaps.Where(i => !c.BeatmapMD5Hashes.Contains(i.MD5Hash)).ToList(); + foreach (var item in beatmaps) + c.BeatmapMD5Hashes.Add(item.MD5Hash); + notifications?.Post(new SimpleNotification { Text = $"Updated collection: {room.Name}" }); + }); + } }; } } From d4c69f0c9063c7c4d56f75ecc37a1819b616e4dc Mon Sep 17 00:00:00 2001 From: Layendan Date: Fri, 7 Feb 2025 04:04:29 -0700 Subject: [PATCH 036/107] Assume room is setup correctly and remove duplicate maps before querying realm --- .../OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs index 8801d73e9e..c24c7d834d 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs @@ -35,15 +35,13 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { Action = () => { - int[] ids = room.Playlist.Select(item => item.Beatmap.OnlineID).Where(onlineId => onlineId > 0).ToArray(); - - if (ids.Length == 0) + if (room.Playlist.Count == 0) { notifications?.Post(new SimpleErrorNotification { Text = "Cannot add local beatmaps" }); return; } - string filter = string.Join(" OR ", ids.Select(id => $"(OnlineID == {id})")); + string filter = string.Join(" OR ", room.Playlist.Select(item => $"(OnlineID == {item.Beatmap.OnlineID})").Distinct()); var beatmaps = realmAccess.Realm.All().Filter(filter).ToList(); var collection = realmAccess.Realm.All().FirstOrDefault(c => c.Name == room.Name); From de0aabbfc59963923637bc08edcc3c205a3e1f8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 7 Feb 2025 15:34:52 +0100 Subject: [PATCH 037/107] Add staging submission service URL to development endpoint config --- osu.Game/Online/DevelopmentEndpointConfiguration.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Online/DevelopmentEndpointConfiguration.cs b/osu.Game/Online/DevelopmentEndpointConfiguration.cs index f4e1b257ee..e36e36ee9f 100644 --- a/osu.Game/Online/DevelopmentEndpointConfiguration.cs +++ b/osu.Game/Online/DevelopmentEndpointConfiguration.cs @@ -13,6 +13,7 @@ namespace osu.Game.Online SpectatorUrl = $@"{APIUrl}/signalr/spectator"; MultiplayerUrl = $@"{APIUrl}/signalr/multiplayer"; MetadataUrl = $@"{APIUrl}/signalr/metadata"; + BeatmapSubmissionServiceUrl = $@"{APIUrl}/beatmap-submission"; } } } From 64f0d234d84222b00363397b43c9cda55c772a04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 7 Feb 2025 15:37:27 +0100 Subject: [PATCH 038/107] Fix exiting being eternally blocked after successful beatmap submission --- osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs index 41c875ac1f..9dfe998138 100644 --- a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs @@ -420,7 +420,7 @@ namespace osu.Game.Screens.Edit.Submission s.Push(new EditorLoader()); }, [typeof(MainMenu)]); - return true; + return false; } return base.OnExiting(e); From f9bda0524ada81a9bbc440b88195af3d8ec9786e Mon Sep 17 00:00:00 2001 From: Layendan Date: Sun, 9 Feb 2025 18:45:13 -0700 Subject: [PATCH 039/107] Update button text to include downloaded beatmaps and collection status --- .../AddPlaylistToCollectionButton.cs | 37 +++++++++++++++++-- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs index c24c7d834d..cc875b707d 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs @@ -1,8 +1,11 @@ // Copyright (c) ppy Pty Ltd . 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.Allocation; +using osu.Framework.Bindables; using osu.Game.Beatmaps; using osu.Game.Collections; using osu.Game.Database; @@ -17,6 +20,10 @@ namespace osu.Game.Screens.OnlinePlay.Playlists public partial class AddPlaylistToCollectionButton : RoundedButton { private readonly Room room; + private readonly Bindable downloadedBeatmapsCount = new Bindable(0); + private readonly Bindable collectionExists = new Bindable(false); + private IDisposable? beatmapSubscription; + private IDisposable? collectionSubscription; [Resolved] private RealmAccess realmAccess { get; set; } = null!; @@ -27,7 +34,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists public AddPlaylistToCollectionButton(Room room) { this.room = room; - Text = "Add Maps to Collection"; + Text = formatButtonText(downloadedBeatmapsCount.Value, collectionExists.Value); } [BackgroundDependencyLoader] @@ -41,8 +48,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists return; } - string filter = string.Join(" OR ", room.Playlist.Select(item => $"(OnlineID == {item.Beatmap.OnlineID})").Distinct()); - var beatmaps = realmAccess.Realm.All().Filter(filter).ToList(); + var beatmaps = realmAccess.Realm.All().Filter(formatFilterQuery(room.Playlist)).ToList(); var collection = realmAccess.Realm.All().FirstOrDefault(c => c.Name == room.Name); @@ -64,5 +70,30 @@ namespace osu.Game.Screens.OnlinePlay.Playlists } }; } + + protected override void LoadComplete() + { + base.LoadComplete(); + + beatmapSubscription = realmAccess.RegisterForNotifications(r => r.All().Filter(formatFilterQuery(room.Playlist)), (sender, _) => downloadedBeatmapsCount.Value = sender.Count); + + collectionSubscription = realmAccess.RegisterForNotifications(r => r.All().Where(c => c.Name == room.Name), (sender, _) => collectionExists.Value = sender.Count > 0); + + downloadedBeatmapsCount.BindValueChanged(_ => Text = formatButtonText(downloadedBeatmapsCount.Value, collectionExists.Value)); + + collectionExists.BindValueChanged(_ => Text = formatButtonText(downloadedBeatmapsCount.Value, collectionExists.Value), true); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + beatmapSubscription?.Dispose(); + collectionSubscription?.Dispose(); + } + + private string formatFilterQuery(IReadOnlyList playlistItems) => string.Join(" OR ", playlistItems.Select(item => $"(OnlineID == {item.Beatmap.OnlineID})").Distinct()); + + private string formatButtonText(int count, bool collectionExists) => $"Add {count} {(count == 1 ? "beatmap" : "beatmaps")} to {(collectionExists ? "collection" : "new collection")}"; } } From 7853456c06abf8c7e46d233580b50cdf070f2efe Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 Feb 2025 15:12:59 +0900 Subject: [PATCH 040/107] Add delay before browser displays beatmap --- .../Submission/BeatmapSubmissionScreen.cs | 32 ++++++++----------- 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs index 9dfe998138..039c919ed6 100644 --- a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs @@ -290,15 +290,7 @@ namespace osu.Game.Screens.Edit.Submission var patchRequest = new PatchBeatmapPackageRequest(beatmapSetId.Value); patchRequest.FilesChanged.AddRange(changedFiles); patchRequest.FilesDeleted.AddRange(onlineFilesByFilename.Keys); - patchRequest.Success += async () => - { - uploadStep.SetCompleted(); - - if (configManager.Get(OsuSetting.EditorSubmissionLoadInBrowserAfterSubmission)) - game?.OpenUrlExternally($"{api.Endpoints.WebsiteUrl}/beatmapsets/{beatmapSetId}"); - - await updateLocalBeatmap().ConfigureAwait(true); - }; + patchRequest.Success += uploadCompleted; patchRequest.Failure += ex => { uploadStep.SetFailed(ex.Message); @@ -318,15 +310,7 @@ namespace osu.Game.Screens.Edit.Submission var uploadRequest = new ReplaceBeatmapPackageRequest(beatmapSetId.Value, beatmapPackageStream.ToArray()); - uploadRequest.Success += async () => - { - uploadStep.SetCompleted(); - - if (configManager.Get(OsuSetting.EditorSubmissionLoadInBrowserAfterSubmission)) - game?.OpenUrlExternally($"{api.Endpoints.WebsiteUrl}/beatmapsets/{beatmapSetId}"); - - await updateLocalBeatmap().ConfigureAwait(true); - }; + uploadRequest.Success += uploadCompleted; uploadRequest.Failure += ex => { uploadStep.SetFailed(ex.Message); @@ -339,6 +323,12 @@ namespace osu.Game.Screens.Edit.Submission uploadStep.SetInProgress(); } + private void uploadCompleted() + { + uploadStep.SetCompleted(); + updateLocalBeatmap().ConfigureAwait(true); + } + private async Task updateLocalBeatmap() { Debug.Assert(beatmapSetId != null); @@ -364,6 +354,12 @@ namespace osu.Game.Screens.Edit.Submission updateStep.SetCompleted(); showBeatmapCard(); allowExit(); + + if (configManager.Get(OsuSetting.EditorSubmissionLoadInBrowserAfterSubmission)) + { + await Task.Delay(1000).ConfigureAwait(true); + game?.OpenUrlExternally($"{api.Endpoints.WebsiteUrl}/beatmapsets/{beatmapSetId}"); + } } private void showBeatmapCard() From 930aaecd7fc39a9455f3e56fe7baffe97b9dc360 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 Feb 2025 15:22:31 +0900 Subject: [PATCH 041/107] Fix back button displaying before it should --- osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs index 039c919ed6..0967bcfc65 100644 --- a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs @@ -39,6 +39,8 @@ namespace osu.Game.Screens.Edit.Submission public override bool DisallowExternalBeatmapRulesetChanges => true; + protected override bool InitialBackButtonVisibility => false; + [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine); From eae1ea7e32484c03cd24b656c68c3138f4197b82 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 Feb 2025 15:23:25 +0900 Subject: [PATCH 042/107] Adjust animations and induce some short delays to make things more graceful --- .../Screens/Edit/Submission/BeatmapSubmissionScreen.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs index 0967bcfc65..121e25d8b7 100644 --- a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs @@ -92,6 +92,8 @@ namespace osu.Game.Screens.Edit.Submission { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, + AutoSizeDuration = 400, + AutoSizeEasing = Easing.OutQuint, Alpha = 0, Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -144,9 +146,6 @@ namespace osu.Game.Screens.Edit.Submission Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, AutoSizeAxes = Axes.Both, - AutoSizeDuration = 500, - AutoSizeEasing = Easing.OutQuint, - Masking = true, CornerRadius = BeatmapCard.CORNER_RADIUS, Child = flashLayer = new Container { @@ -252,6 +251,8 @@ namespace osu.Game.Screens.Edit.Submission exportStep.SetCompleted(); exportProgressNotification = null; + await Task.Delay(200).ConfigureAwait(true); + if (onlineFiles.Count > 0) await patchBeatmapSet(onlineFiles).ConfigureAwait(true); else @@ -337,6 +338,7 @@ namespace osu.Game.Screens.Edit.Submission Debug.Assert(beatmapPackageStream != null); updateStep.SetInProgress(); + await Task.Delay(200).ConfigureAwait(true); try { From 5e9f195117307feb555e663fe8544c9a2527bc51 Mon Sep 17 00:00:00 2001 From: Layendan Date: Sun, 9 Feb 2025 23:27:28 -0700 Subject: [PATCH 043/107] Fix tests failing if playlist was empty --- .../OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs index cc875b707d..8b5d5c752c 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs @@ -75,7 +75,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { base.LoadComplete(); - beatmapSubscription = realmAccess.RegisterForNotifications(r => r.All().Filter(formatFilterQuery(room.Playlist)), (sender, _) => downloadedBeatmapsCount.Value = sender.Count); + if (room.Playlist.Count > 0) + beatmapSubscription = realmAccess.RegisterForNotifications(r => r.All().Filter(formatFilterQuery(room.Playlist)), (sender, _) => downloadedBeatmapsCount.Value = sender.Count); collectionSubscription = realmAccess.RegisterForNotifications(r => r.All().Where(c => c.Name == room.Name), (sender, _) => collectionExists.Value = sender.Count > 0); From a3cd62ec7295b3dd5f16350ae6439a623acab111 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sat, 25 Jan 2025 20:17:21 -0500 Subject: [PATCH 044/107] Flip mania judgement anchor on flipped scroll direction --- .../UI/DrawableManiaJudgement.cs | 65 +++++-------------- osu.Game.Rulesets.Mania/UI/Stage.cs | 8 +-- 2 files changed, 17 insertions(+), 56 deletions(-) diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs index 75f56bffa4..40fef1a56a 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs @@ -3,65 +3,32 @@ #nullable disable +using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; -using osuTK; +using osu.Game.Rulesets.UI.Scrolling; namespace osu.Game.Rulesets.Mania.UI { public partial class DrawableManiaJudgement : DrawableJudgement { - protected override Drawable CreateDefaultJudgement(HitResult result) => new DefaultManiaJudgementPiece(result); + private IBindable direction; - private partial class DefaultManiaJudgementPiece : DefaultJudgementPiece + [BackgroundDependencyLoader] + private void load(IScrollingInfo scrollingInfo) { - private const float judgement_y_position = -180f; - - public DefaultManiaJudgementPiece(HitResult result) - : base(result) - { - Y = judgement_y_position; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - JudgementText.Font = JudgementText.Font.With(size: 25); - } - - public override void PlayAnimation() - { - switch (Result) - { - case HitResult.None: - this.FadeOutFromOne(800); - break; - - case HitResult.Miss: - this.ScaleTo(1.6f); - this.ScaleTo(1, 100, Easing.In); - - this.MoveToY(judgement_y_position); - this.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint); - - this.RotateTo(0); - this.RotateTo(40, 800, Easing.InQuint); - - this.FadeOutFromOne(800); - break; - - default: - this.ScaleTo(0.8f); - this.ScaleTo(1, 250, Easing.OutElastic); - - this.Delay(50) - .ScaleTo(0.75f, 250) - .FadeOut(200); - break; - } - } + direction = scrollingInfo.Direction.GetBoundCopy(); + direction.BindValueChanged(_ => onDirectionChanged(), true); } + + private void onDirectionChanged() + { + Anchor = direction.Value == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre; + Origin = Anchor.Centre; + } + + protected override Drawable CreateDefaultJudgement(HitResult result) => new DefaultManiaJudgementPiece(result); } } diff --git a/osu.Game.Rulesets.Mania/UI/Stage.cs b/osu.Game.Rulesets.Mania/UI/Stage.cs index fb9671c14d..faa9fc318c 100644 --- a/osu.Game.Rulesets.Mania/UI/Stage.cs +++ b/osu.Game.Rulesets.Mania/UI/Stage.cs @@ -216,13 +216,7 @@ namespace osu.Game.Rulesets.Mania.UI return; judgements.Clear(false); - judgements.Add(judgementPooler.Get(result.Type, j => - { - j.Apply(result, judgedObject); - - j.Anchor = Anchor.BottomCentre; - j.Origin = Anchor.Centre; - })!); + judgements.Add(judgementPooler.Get(result.Type, j => j.Apply(result, judgedObject))!); } protected override void Update() From 1e06c5cc4ac823f773101899b0ea55e23e82e291 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sat, 25 Jan 2025 20:17:36 -0500 Subject: [PATCH 045/107] Flip the Y offset of skin judgement pieces on flipped scroll direction --- .../Skinning/Argon/ArgonJudgementPiece.cs | 14 +++- .../Legacy/LegacyManiaJudgementPiece.cs | 29 +++++-- .../UI/DefaultManiaJudgementPiece.cs | 75 +++++++++++++++++++ 3 files changed, 108 insertions(+), 10 deletions(-) create mode 100644 osu.Game.Rulesets.Mania/UI/DefaultManiaJudgementPiece.cs diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonJudgementPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonJudgementPiece.cs index a1c81d3a6a..6098459f6b 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonJudgementPiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonJudgementPiece.cs @@ -3,6 +3,7 @@ using System; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -12,6 +13,7 @@ using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.UI.Scrolling; using osuTK; using osuTK.Graphics; @@ -26,18 +28,22 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon [Resolved] private OsuColour colours { get; set; } = null!; + private IBindable direction = null!; + public ArgonJudgementPiece(HitResult result) : base(result) { AutoSizeAxes = Axes.Both; Origin = Anchor.Centre; - Y = judgement_y_position; } [BackgroundDependencyLoader] - private void load() + private void load(IScrollingInfo scrollingInfo) { + direction = scrollingInfo.Direction.GetBoundCopy(); + direction.BindValueChanged(_ => onDirectionChanged(), true); + if (Result.IsHit()) { AddInternal(ringExplosion = new RingExplosion(Result) @@ -47,6 +53,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon } } + private void onDirectionChanged() => Y = direction.Value == ScrollingDirection.Up ? -judgement_y_position : judgement_y_position; + protected override SpriteText CreateJudgementText() => new OsuSpriteText { @@ -78,7 +86,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon this.ScaleTo(1.6f); this.ScaleTo(1, 100, Easing.In); - this.MoveToY(judgement_y_position); + this.MoveToY(direction.Value == ScrollingDirection.Up ? -judgement_y_position : judgement_y_position); this.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint); this.RotateTo(0); diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaJudgementPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaJudgementPiece.cs index 4b0cc482d9..3752c5f27a 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaJudgementPiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaJudgementPiece.cs @@ -2,12 +2,14 @@ // 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.Animations; using osu.Framework.Graphics.Containers; using osu.Framework.Utils; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Skinning; namespace osu.Game.Rulesets.Mania.Skinning.Legacy @@ -28,14 +30,16 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy AutoSizeAxes = Axes.Both; } - [BackgroundDependencyLoader] - private void load(ISkinSource skin) - { - float hitPosition = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.HitPosition)?.Value ?? 0; - float scorePosition = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.ScorePosition)?.Value ?? 0; + private IBindable direction = null!; - float absoluteHitPosition = 480f * LegacyManiaSkinConfiguration.POSITION_SCALE_FACTOR - hitPosition; - Y = scorePosition - absoluteHitPosition; + [Resolved] + private ISkinSource skin { get; set; } = null!; + + [BackgroundDependencyLoader] + private void load(IScrollingInfo scrollingInfo) + { + direction = scrollingInfo.Direction.GetBoundCopy(); + direction.BindValueChanged(_ => onDirectionChanged(), true); InternalChild = animation.With(d => { @@ -44,6 +48,17 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy }); } + private void onDirectionChanged() + { + float hitPosition = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.HitPosition)?.Value ?? 0; + float scorePosition = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.ScorePosition)?.Value ?? 0; + + float absoluteHitPosition = 480f * LegacyManiaSkinConfiguration.POSITION_SCALE_FACTOR - hitPosition; + float finalPosition = scorePosition - absoluteHitPosition; + + Y = direction.Value == ScrollingDirection.Up ? -finalPosition : finalPosition; + } + public void PlayAnimation() { (animation as IFramedAnimation)?.GotoFrame(0); diff --git a/osu.Game.Rulesets.Mania/UI/DefaultManiaJudgementPiece.cs b/osu.Game.Rulesets.Mania/UI/DefaultManiaJudgementPiece.cs new file mode 100644 index 0000000000..f0af6085d0 --- /dev/null +++ b/osu.Game.Rulesets.Mania/UI/DefaultManiaJudgementPiece.cs @@ -0,0 +1,75 @@ +// 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.Bindables; +using osu.Framework.Graphics; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.UI.Scrolling; +using osuTK; + +namespace osu.Game.Rulesets.Mania.UI +{ + public partial class DefaultManiaJudgementPiece : DefaultJudgementPiece + { + private const float judgement_y_position = -180f; + + private IBindable direction = null!; + + public DefaultManiaJudgementPiece(HitResult result) + : base(result) + { + } + + [BackgroundDependencyLoader] + private void load(IScrollingInfo scrollingInfo) + { + direction = scrollingInfo.Direction.GetBoundCopy(); + direction.BindValueChanged(_ => onDirectionChanged(), true); + } + + private void onDirectionChanged() => Y = direction.Value == ScrollingDirection.Up ? -judgement_y_position : judgement_y_position; + + protected override void LoadComplete() + { + base.LoadComplete(); + + JudgementText.Font = JudgementText.Font.With(size: 25); + } + + public override void PlayAnimation() + { + switch (Result) + { + case HitResult.None: + this.FadeOutFromOne(800); + break; + + case HitResult.Miss: + this.ScaleTo(1.6f); + this.ScaleTo(1, 100, Easing.In); + + this.MoveToY(direction.Value == ScrollingDirection.Up ? -judgement_y_position : judgement_y_position); + this.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint); + + this.RotateTo(0); + this.RotateTo(40, 800, Easing.InQuint); + + this.FadeOutFromOne(800); + break; + + default: + this.ScaleTo(0.8f); + this.ScaleTo(1, 250, Easing.OutElastic); + + this.Delay(50) + .ScaleTo(0.75f, 250) + .FadeOut(200); + + // osu!mania uses a custom fade length, so the base call is intentionally omitted. + break; + } + } + } +} From 895493877cd0f04699099a4228657b05365c7b85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 10 Feb 2025 09:02:47 +0100 Subject: [PATCH 046/107] Allow performing beatmap reload after submission from song select --- osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs index 121e25d8b7..f53d10d23b 100644 --- a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs @@ -29,6 +29,7 @@ using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; using osu.Game.Screens.Menu; +using osu.Game.Screens.Select; using osuTK; namespace osu.Game.Screens.Edit.Submission @@ -418,7 +419,7 @@ namespace osu.Game.Screens.Edit.Submission } s.Push(new EditorLoader()); - }, [typeof(MainMenu)]); + }, [typeof(SongSelect)]); return false; } From 45259b374a2fdd6626e06a7ed9c526cf28cd5fb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 10 Feb 2025 09:09:43 +0100 Subject: [PATCH 047/107] Remove unused using --- osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs index f53d10d23b..9672e4360a 100644 --- a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs @@ -28,7 +28,6 @@ using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; -using osu.Game.Screens.Menu; using osu.Game.Screens.Select; using osuTK; From b8e33a28d25c8590cf4d0b93e59deeaa21daa1d2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 Feb 2025 17:40:00 +0900 Subject: [PATCH 048/107] Minor code refactors --- .../Submission/BeatmapSubmissionScreen.cs | 19 ++++++++++------- .../Submission/SubmissionBeatmapExporter.cs | 21 +++++++------------ 2 files changed, 19 insertions(+), 21 deletions(-) diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs index 9672e4360a..201888e078 100644 --- a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs @@ -76,7 +76,6 @@ namespace osu.Game.Screens.Edit.Submission private uint? beatmapSetId; private MemoryStream? beatmapPackageStream; - private SubmissionBeatmapExporter legacyBeatmapExporter = null!; private ProgressNotification? exportProgressNotification; private ProgressNotification? updateProgressNotification; @@ -214,8 +213,7 @@ namespace osu.Game.Screens.Edit.Submission }).ConfigureAwait(true); } - legacyBeatmapExporter = new SubmissionBeatmapExporter(storage, response); - await createBeatmapPackage(response.Files).ConfigureAwait(true); + await createBeatmapPackage(response).ConfigureAwait(true); }; createRequest.Failure += ex => { @@ -228,7 +226,7 @@ namespace osu.Game.Screens.Edit.Submission api.Queue(createRequest); } - private async Task createBeatmapPackage(ICollection onlineFiles) + private async Task createBeatmapPackage(PutBeatmapSetResponse response) { Debug.Assert(ThreadSafety.IsUpdateThread); @@ -237,8 +235,13 @@ namespace osu.Game.Screens.Edit.Submission try { beatmapPackageStream = new MemoryStream(); - await legacyBeatmapExporter.ExportToStreamAsync(Beatmap.Value.BeatmapSetInfo.ToLive(realmAccess), beatmapPackageStream, exportProgressNotification = new ProgressNotification()) - .ConfigureAwait(true); + exportProgressNotification = new ProgressNotification(); + + var legacyBeatmapExporter = new SubmissionBeatmapExporter(storage, response); + + await legacyBeatmapExporter + .ExportToStreamAsync(Beatmap.Value.BeatmapSetInfo.ToLive(realmAccess), beatmapPackageStream, exportProgressNotification) + .ConfigureAwait(true); } catch (Exception ex) { @@ -253,8 +256,8 @@ namespace osu.Game.Screens.Edit.Submission await Task.Delay(200).ConfigureAwait(true); - if (onlineFiles.Count > 0) - await patchBeatmapSet(onlineFiles).ConfigureAwait(true); + if (response.Files.Count > 0) + await patchBeatmapSet(response.Files).ConfigureAwait(true); else replaceBeatmapSet(); } diff --git a/osu.Game/Screens/Edit/Submission/SubmissionBeatmapExporter.cs b/osu.Game/Screens/Edit/Submission/SubmissionBeatmapExporter.cs index 3c50a1bf80..fab080cdba 100644 --- a/osu.Game/Screens/Edit/Submission/SubmissionBeatmapExporter.cs +++ b/osu.Game/Screens/Edit/Submission/SubmissionBeatmapExporter.cs @@ -14,43 +14,38 @@ namespace osu.Game.Screens.Edit.Submission public class SubmissionBeatmapExporter : LegacyBeatmapExporter { private readonly uint? beatmapSetId; - private readonly HashSet? beatmapIds; - - public SubmissionBeatmapExporter(Storage storage) - : base(storage) - { - } + private readonly HashSet? allocatedBeatmapIds; public SubmissionBeatmapExporter(Storage storage, PutBeatmapSetResponse putBeatmapSetResponse) : base(storage) { beatmapSetId = putBeatmapSetResponse.BeatmapSetId; - beatmapIds = putBeatmapSetResponse.BeatmapIds.Select(id => (int)id).ToHashSet(); + allocatedBeatmapIds = putBeatmapSetResponse.BeatmapIds.Select(id => (int)id).ToHashSet(); } protected override void MutateBeatmap(BeatmapSetInfo beatmapSet, IBeatmap playableBeatmap) { base.MutateBeatmap(beatmapSet, playableBeatmap); - if (beatmapSetId != null && beatmapIds != null) + if (beatmapSetId != null && allocatedBeatmapIds != null) { playableBeatmap.BeatmapInfo.BeatmapSet = beatmapSet; playableBeatmap.BeatmapInfo.BeatmapSet!.OnlineID = (int)beatmapSetId; - if (beatmapIds.Contains(playableBeatmap.BeatmapInfo.OnlineID)) + if (allocatedBeatmapIds.Contains(playableBeatmap.BeatmapInfo.OnlineID)) { - beatmapIds.Remove(playableBeatmap.BeatmapInfo.OnlineID); + allocatedBeatmapIds.Remove(playableBeatmap.BeatmapInfo.OnlineID); return; } if (playableBeatmap.BeatmapInfo.OnlineID > 0) throw new InvalidOperationException(@"Encountered beatmap with ID that has not been assigned to it by the server!"); - if (beatmapIds.Count == 0) + if (allocatedBeatmapIds.Count == 0) throw new InvalidOperationException(@"Ran out of new beatmap IDs to assign to unsubmitted beatmaps!"); - int newId = beatmapIds.First(); - beatmapIds.Remove(newId); + int newId = allocatedBeatmapIds.First(); + allocatedBeatmapIds.Remove(newId); playableBeatmap.BeatmapInfo.OnlineID = newId; } } From 3ba56e009e347942089dbfe8533020ad7a4b63e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 10 Feb 2025 10:41:10 +0100 Subject: [PATCH 049/107] Privatise a few members --- osu.Game/Screens/Play/HUD/SpectatorList.cs | 35 +++++++++++----------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/SpectatorList.cs b/osu.Game/Screens/Play/HUD/SpectatorList.cs index c784fc298a..72e866cb24 100644 --- a/osu.Game/Screens/Play/HUD/SpectatorList.cs +++ b/osu.Game/Screens/Play/HUD/SpectatorList.cs @@ -28,17 +28,16 @@ namespace osu.Game.Screens.Play.HUD { private const int max_spectators_displayed = 10; - public BindableList Spectators { get; } = new BindableList(); - public Bindable UserPlayingState { get; } = new Bindable(); - [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.Font), nameof(SkinnableComponentStrings.FontDescription))] public Bindable Font { get; } = new Bindable(Typeface.Torus); [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.TextColour), nameof(SkinnableComponentStrings.TextColourDescription))] public BindableColour4 HeaderColour { get; } = new BindableColour4(Colour4.White); - protected OsuSpriteText Header { get; private set; } = null!; + private BindableList spectators { get; } = new BindableList(); + private Bindable userPlayingState { get; } = new Bindable(); + private OsuSpriteText header = null!; private FillFlowContainer mainFlow = null!; private FillFlowContainer spectatorsFlow = null!; private DrawablePool pool = null!; @@ -63,7 +62,7 @@ namespace osu.Game.Screens.Play.HUD Direction = FillDirection.Vertical, Children = new Drawable[] { - Header = new OsuSpriteText + header = new OsuSpriteText { Colour = colours.Blue0, Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold), @@ -78,18 +77,18 @@ namespace osu.Game.Screens.Play.HUD pool = new DrawablePool(max_spectators_displayed), }; - HeaderColour.Value = Header.Colour; + HeaderColour.Value = header.Colour; } protected override void LoadComplete() { base.LoadComplete(); - ((IBindableList)Spectators).BindTo(client.WatchingUsers); - ((IBindable)UserPlayingState).BindTo(gameplayState.PlayingState); + ((IBindableList)spectators).BindTo(client.WatchingUsers); + ((IBindable)userPlayingState).BindTo(gameplayState.PlayingState); - Spectators.BindCollectionChanged(onSpectatorsChanged, true); - UserPlayingState.BindValueChanged(_ => updateVisibility()); + spectators.BindCollectionChanged(onSpectatorsChanged, true); + userPlayingState.BindValueChanged(_ => updateVisibility()); Font.BindValueChanged(_ => updateAppearance()); HeaderColour.BindValueChanged(_ => updateAppearance(), true); @@ -125,10 +124,10 @@ namespace osu.Game.Screens.Play.HUD for (int i = 0; i < spectatorsFlow.Count; i++) spectatorsFlow.SetLayoutPosition(spectatorsFlow[i], i); - if (Spectators.Count >= max_spectators_displayed && spectatorsFlow.Count < max_spectators_displayed) + if (spectators.Count >= max_spectators_displayed && spectatorsFlow.Count < max_spectators_displayed) { for (int i = spectatorsFlow.Count; i < max_spectators_displayed; i++) - addNewSpectatorToList(i, Spectators[i]); + addNewSpectatorToList(i, spectators[i]); } break; @@ -144,7 +143,7 @@ namespace osu.Game.Screens.Play.HUD throw new NotSupportedException(); } - Header.Text = SpectatorListStrings.SpectatorCount(Spectators.Count).ToUpper(); + header.Text = SpectatorListStrings.SpectatorCount(spectators.Count).ToUpper(); updateVisibility(); for (int i = 0; i < spectatorsFlow.Count; i++) @@ -160,7 +159,7 @@ namespace osu.Game.Screens.Play.HUD var entry = pool.Get(entry => { entry.Current.Value = spectator; - entry.UserPlayingState = UserPlayingState; + entry.UserPlayingState = userPlayingState; }); spectatorsFlow.Insert(i, entry); @@ -169,15 +168,15 @@ namespace osu.Game.Screens.Play.HUD private void updateVisibility() { // We don't want to show spectators when we are watching a replay. - mainFlow.FadeTo(Spectators.Count > 0 && UserPlayingState.Value != LocalUserPlayingState.NotPlaying ? 1 : 0, 250, Easing.OutQuint); + mainFlow.FadeTo(spectators.Count > 0 && userPlayingState.Value != LocalUserPlayingState.NotPlaying ? 1 : 0, 250, Easing.OutQuint); } private void updateAppearance() { - Header.Font = OsuFont.GetFont(Font.Value, 12, FontWeight.Bold); - Header.Colour = HeaderColour.Value; + header.Font = OsuFont.GetFont(Font.Value, 12, FontWeight.Bold); + header.Colour = HeaderColour.Value; - Width = Header.DrawWidth; + Width = header.DrawWidth; } private partial class SpectatorListEntry : PoolableDrawable From ad642b84258497b0140ae3d45680f52988a1429f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 10 Feb 2025 11:17:17 +0100 Subject: [PATCH 050/107] Fix spectator list showing other users in multiplayer room even if they're not spectating --- .../Visual/Gameplay/TestSceneSpectatorList.cs | 17 ++++++--- osu.Game/Screens/Play/HUD/SpectatorList.cs | 37 +++++++++++++++---- 2 files changed, 42 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorList.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorList.cs index 66a87c0715..66c465cbed 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorList.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorList.cs @@ -8,11 +8,14 @@ using osu.Framework.Graphics; using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Graphics; +using osu.Game.Online.Multiplayer; using osu.Game.Online.Spectator; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Scoring; using osu.Game.Screens.Play; using osu.Game.Screens.Play.HUD; +using osu.Game.Tests.Visual.Multiplayer; +using osu.Game.Tests.Visual.OnlinePlay; using osu.Game.Tests.Visual.Spectator; namespace osu.Game.Tests.Visual.Gameplay @@ -28,20 +31,23 @@ namespace osu.Game.Tests.Visual.Gameplay SpectatorList list = null!; Bindable playingState = new Bindable(); GameplayState gameplayState = new GameplayState(new Beatmap(), new OsuRuleset(), healthProcessor: new OsuHealthProcessor(0), localUserPlayingState: playingState); - TestSpectatorClient client = new TestSpectatorClient(); + TestSpectatorClient spectatorClient = new TestSpectatorClient(); + TestMultiplayerClient multiplayerClient = new TestMultiplayerClient(new TestMultiplayerRoomManager(new TestRoomRequestsHandler())); AddStep("create spectator list", () => { Children = new Drawable[] { - client, + spectatorClient, + multiplayerClient, new DependencyProvidingContainer { RelativeSizeAxes = Axes.Both, CachedDependencies = [ (typeof(GameplayState), gameplayState), - (typeof(SpectatorClient), client) + (typeof(SpectatorClient), spectatorClient), + (typeof(MultiplayerClient), multiplayerClient), ], Child = list = new SpectatorList { @@ -57,7 +63,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddRepeatStep("add a user", () => { int id = Interlocked.Increment(ref counter); - ((ISpectatorClient)client).UserStartedWatching([ + ((ISpectatorClient)spectatorClient).UserStartedWatching([ new SpectatorUser { OnlineID = id, @@ -66,7 +72,8 @@ namespace osu.Game.Tests.Visual.Gameplay ]); }, 10); - AddRepeatStep("remove random user", () => ((ISpectatorClient)client).UserEndedWatching(client.WatchingUsers[RNG.Next(client.WatchingUsers.Count)].OnlineID), 5); + AddRepeatStep("remove random user", () => ((ISpectatorClient)spectatorClient).UserEndedWatching( + spectatorClient.WatchingUsers[RNG.Next(spectatorClient.WatchingUsers.Count)].OnlineID), 5); AddStep("change font to venera", () => list.Font.Value = Typeface.Venera); AddStep("change font to torus", () => list.Font.Value = Typeface.Torus); diff --git a/osu.Game/Screens/Play/HUD/SpectatorList.cs b/osu.Game/Screens/Play/HUD/SpectatorList.cs index 72e866cb24..9f97121a92 100644 --- a/osu.Game/Screens/Play/HUD/SpectatorList.cs +++ b/osu.Game/Screens/Play/HUD/SpectatorList.cs @@ -2,7 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using System.Collections.Specialized; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; @@ -17,6 +19,7 @@ using osu.Game.Graphics.Sprites; using osu.Game.Online.Chat; using osu.Game.Localisation.HUD; using osu.Game.Localisation.SkinComponents; +using osu.Game.Online.Multiplayer; using osu.Game.Online.Spectator; using osu.Game.Skinning; using osuTK; @@ -34,8 +37,9 @@ namespace osu.Game.Screens.Play.HUD [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.TextColour), nameof(SkinnableComponentStrings.TextColourDescription))] public BindableColour4 HeaderColour { get; } = new BindableColour4(Colour4.White); - private BindableList spectators { get; } = new BindableList(); + private BindableList watchingUsers { get; } = new BindableList(); private Bindable userPlayingState { get; } = new Bindable(); + private int displayedSpectatorCount; private OsuSpriteText header = null!; private FillFlowContainer mainFlow = null!; @@ -48,6 +52,9 @@ namespace osu.Game.Screens.Play.HUD [Resolved] private GameplayState gameplayState { get; set; } = null!; + [Resolved] + private MultiplayerClient multiplayerClient { get; set; } = null!; + [BackgroundDependencyLoader] private void load(OsuColour colours) { @@ -84,10 +91,10 @@ namespace osu.Game.Screens.Play.HUD { base.LoadComplete(); - ((IBindableList)spectators).BindTo(client.WatchingUsers); + ((IBindableList)watchingUsers).BindTo(client.WatchingUsers); ((IBindable)userPlayingState).BindTo(gameplayState.PlayingState); - spectators.BindCollectionChanged(onSpectatorsChanged, true); + watchingUsers.BindCollectionChanged(onSpectatorsChanged, true); userPlayingState.BindValueChanged(_ => updateVisibility()); Font.BindValueChanged(_ => updateAppearance()); @@ -99,6 +106,18 @@ namespace osu.Game.Screens.Play.HUD private void onSpectatorsChanged(object? sender, NotifyCollectionChangedEventArgs e) { + // the multiplayer gameplay leaderboard relies on calling `SpectatorClient.WatchUser()` to get updates on users' total scores. + // this has an unfortunate side effect of other players showing up in `SpectatorClient.WatchingUsers`. + // we do not generally wish to display other players in the room as spectators due to that implementation detail, + // therefore this code is intended to filter out those players on the client side. + // note that the way that this is done is rather specific to the multiplayer use case and therefore carries a lot of assumptions + // (e.g. that the `MultiplayerRoomUser`s have the correct `State` at the point wherein they issue the `WatchUser()` calls). + // the more proper way to do this (which is by subscribing to `WatchingUsers` and `RoomUpdated`, and doing a proper diff to a third list on any change of either) + // is a lot more difficult to write correctly, given that we also rely on `BindableList`'s collection changed event arguments to properly animate this component. + var excludedUserIds = new HashSet(); + if (multiplayerClient.Room != null) + excludedUserIds.UnionWith(multiplayerClient.Room.Users.Where(u => u.State != MultiplayerUserState.Spectating).Select(u => u.UserID)); + switch (e.Action) { case NotifyCollectionChangedAction.Add: @@ -108,6 +127,9 @@ namespace osu.Game.Screens.Play.HUD var spectator = (SpectatorUser)e.NewItems![i]!; int index = Math.Max(e.NewStartingIndex, 0) + i; + if (excludedUserIds.Contains(spectator.OnlineID)) + continue; + if (index >= max_spectators_displayed) break; @@ -124,10 +146,10 @@ namespace osu.Game.Screens.Play.HUD for (int i = 0; i < spectatorsFlow.Count; i++) spectatorsFlow.SetLayoutPosition(spectatorsFlow[i], i); - if (spectators.Count >= max_spectators_displayed && spectatorsFlow.Count < max_spectators_displayed) + if (watchingUsers.Count >= max_spectators_displayed && spectatorsFlow.Count < max_spectators_displayed) { for (int i = spectatorsFlow.Count; i < max_spectators_displayed; i++) - addNewSpectatorToList(i, spectators[i]); + addNewSpectatorToList(i, watchingUsers[i]); } break; @@ -143,7 +165,8 @@ namespace osu.Game.Screens.Play.HUD throw new NotSupportedException(); } - header.Text = SpectatorListStrings.SpectatorCount(spectators.Count).ToUpper(); + displayedSpectatorCount = watchingUsers.Count(s => !excludedUserIds.Contains(s.OnlineID)); + header.Text = SpectatorListStrings.SpectatorCount(displayedSpectatorCount).ToUpper(); updateVisibility(); for (int i = 0; i < spectatorsFlow.Count; i++) @@ -168,7 +191,7 @@ namespace osu.Game.Screens.Play.HUD private void updateVisibility() { // We don't want to show spectators when we are watching a replay. - mainFlow.FadeTo(spectators.Count > 0 && userPlayingState.Value != LocalUserPlayingState.NotPlaying ? 1 : 0, 250, Easing.OutQuint); + mainFlow.FadeTo(displayedSpectatorCount > 0 && userPlayingState.Value != LocalUserPlayingState.NotPlaying ? 1 : 0, 250, Easing.OutQuint); } private void updateAppearance() From 4b8890ef0c86e7ccdd415181b89ca132331a7024 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Mon, 10 Feb 2025 05:55:21 -0500 Subject: [PATCH 051/107] Fix incorrect thread access in recent iOS orientation changes --- osu.iOS/OsuGameIOS.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.iOS/OsuGameIOS.cs b/osu.iOS/OsuGameIOS.cs index a5a42c1e66..a0132de966 100644 --- a/osu.iOS/OsuGameIOS.cs +++ b/osu.iOS/OsuGameIOS.cs @@ -41,7 +41,7 @@ namespace osu.iOS updateOrientation(); } - private void updateOrientation() + private void updateOrientation() => UIApplication.SharedApplication.InvokeOnMainThread(() => { bool iPad = UIDevice.CurrentDevice.UserInterfaceIdiom == UIUserInterfaceIdiom.Pad; var orientation = MobileUtils.GetOrientation(this, (IOsuScreen)ScreenStack.CurrentScreen, iPad); @@ -60,7 +60,7 @@ namespace osu.iOS appDelegate.Orientations = null; break; } - } + }); protected override UpdateManager CreateUpdateManager() => new MobileUpdateNotifier(); From 288851c606682165d7bb9f0cc8604eefa2b1604b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 10 Feb 2025 12:13:46 +0100 Subject: [PATCH 052/107] Fix score position not being displayed in solo results screen Closes https://github.com/ppy/osu/issues/31842. To be honest, I recall this working too, but I don't recall when it might have broken, nor do I want to go look for the point of breakage because it might be borderline impossible to find it now. So I'm just fixing as if it was just a straight omission. Opting for a client-side fix because server-side inclusion of the score position for an entire leaderboard has been previously rejected as too expensive: https://github.com/ppy/osu-web/pull/11354#discussion_r1689217450 --- osu.Game/Screens/Ranking/SoloResultsScreen.cs | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Ranking/SoloResultsScreen.cs b/osu.Game/Screens/Ranking/SoloResultsScreen.cs index 33b4bf976b..9f7604aa82 100644 --- a/osu.Game/Screens/Ranking/SoloResultsScreen.cs +++ b/osu.Game/Screens/Ranking/SoloResultsScreen.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Linq; using osu.Framework.Allocation; using osu.Game.Beatmaps; using osu.Game.Extensions; @@ -35,7 +34,32 @@ namespace osu.Game.Screens.Ranking return null; getScoreRequest = new GetScoresRequest(Score.BeatmapInfo, Score.Ruleset); - getScoreRequest.Success += r => scoresCallback.Invoke(r.Scores.Where(s => !s.MatchesOnlineID(Score)).Select(s => s.ToScoreInfo(rulesets, Beatmap.Value.BeatmapInfo))); + getScoreRequest.Success += r => + { + var toDisplay = new List(); + + for (int i = 0; i < r.Scores.Count; ++i) + { + var score = r.Scores[i]; + int position = i + 1; + + if (score.MatchesOnlineID(Score)) + { + // we don't want to add the same score twice, but also setting any properties of `Score` this late will have no visible effect, + // so we have to fish out the actual drawable panel and set the position to it directly. + var panel = ScorePanelList.GetPanelForScore(Score); + Score.Position = panel.ScorePosition.Value = position; + } + else + { + var converted = score.ToScoreInfo(rulesets, Beatmap.Value.BeatmapInfo); + converted.Position = position; + toDisplay.Add(converted); + } + } + + scoresCallback.Invoke(toDisplay); + }; return getScoreRequest; } From 38e2f793cae38148dc5c2eae7af7c20dd46c98b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 10 Feb 2025 12:47:38 +0100 Subject: [PATCH 053/107] Add menu items to open beatmap info & discussion pages in browser from editor --- osu.Game/Localisation/EditorStrings.cs | 12 +++++++++++- osu.Game/Screens/Edit/Editor.cs | 9 +++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/osu.Game/Localisation/EditorStrings.cs b/osu.Game/Localisation/EditorStrings.cs index 3b4026be11..1681e541fc 100644 --- a/osu.Game/Localisation/EditorStrings.cs +++ b/osu.Game/Localisation/EditorStrings.cs @@ -184,6 +184,16 @@ namespace osu.Game.Localisation /// public static LocalisableString ResetBookmarks => new TranslatableString(getKey(@"reset_bookmarks"), @"Reset bookmarks"); + /// + /// "Open beatmap info page in browser" + /// + public static LocalisableString OpenInfoPageInBrowser => new TranslatableString(getKey(@"open_info_page_in_browser"), @"Open beatmap info page in browser"); + + /// + /// "Open beatmap discussion page in browser" + /// + public static LocalisableString OpenDiscussionPageInBrowser => new TranslatableString(getKey(@"open_discussion_page_in_browser"), @"Open beatmap discussion page in browser"); + private static string getKey(string key) => $@"{prefix}:{key}"; } -} \ No newline at end of file +} diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index a5dfda9c95..ecb0731c16 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -1256,6 +1256,15 @@ namespace osu.Game.Screens.Edit yield return externalEdit; } + if (editorBeatmap.BeatmapInfo.OnlineID > 0) + { + yield return new OsuMenuItemSpacer(); + yield return new EditorMenuItem(EditorStrings.OpenInfoPageInBrowser, MenuItemType.Standard, + () => (Game as OsuGame)?.OpenUrlExternally(editorBeatmap.BeatmapInfo.GetOnlineURL(api, editorBeatmap.BeatmapInfo.Ruleset))); + yield return new EditorMenuItem(EditorStrings.OpenDiscussionPageInBrowser, MenuItemType.Standard, + () => (Game as OsuGame)?.OpenUrlExternally($@"{api.Endpoints.WebsiteUrl}/beatmapsets/{editorBeatmap.BeatmapInfo.BeatmapSet!.OnlineID}/discussion/{editorBeatmap.BeatmapInfo.OnlineID}")); + } + yield return new OsuMenuItemSpacer(); yield return new EditorMenuItem(CommonStrings.Exit, MenuItemType.Standard, this.Exit); } From 310700b4e7a4d3570605195babc78826751f0de6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 Feb 2025 21:48:27 +0900 Subject: [PATCH 054/107] Space out comment --- osu.Game/Screens/Play/HUD/SpectatorList.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/Play/HUD/SpectatorList.cs b/osu.Game/Screens/Play/HUD/SpectatorList.cs index 9f97121a92..4297c62712 100644 --- a/osu.Game/Screens/Play/HUD/SpectatorList.cs +++ b/osu.Game/Screens/Play/HUD/SpectatorList.cs @@ -108,8 +108,10 @@ namespace osu.Game.Screens.Play.HUD { // the multiplayer gameplay leaderboard relies on calling `SpectatorClient.WatchUser()` to get updates on users' total scores. // this has an unfortunate side effect of other players showing up in `SpectatorClient.WatchingUsers`. + // // we do not generally wish to display other players in the room as spectators due to that implementation detail, // therefore this code is intended to filter out those players on the client side. + // // note that the way that this is done is rather specific to the multiplayer use case and therefore carries a lot of assumptions // (e.g. that the `MultiplayerRoomUser`s have the correct `State` at the point wherein they issue the `WatchUser()` calls). // the more proper way to do this (which is by subscribing to `WatchingUsers` and `RoomUpdated`, and doing a proper diff to a third list on any change of either) From 78e5e0eddd1e20e480b3e49b59c2f1c3f5319e8e Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 11 Feb 2025 12:17:00 +0900 Subject: [PATCH 055/107] Refactor with a bit more null safety In particular I don't like the non-null assert around `GetCurrentItem()`, because there's no reason why it _couldn't_ be `null`. Consider, for example, if these panels are used in matchmaking where there are no items initially present in the playlist. The ruleset nullability part is debatable, but I've chosen to restore the original code here. --- .../Participants/ParticipantPanel.cs | 39 +++++++++---------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs index 51ff52c63e..230245e926 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -28,6 +27,7 @@ using osu.Game.Online.API; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; using osu.Game.Screens.Play.HUD; using osu.Game.Users; using osu.Game.Users.Drawables; @@ -216,20 +216,28 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants const double fade_time = 50; - MultiplayerPlaylistItem? currentItem = client.Room.GetCurrentItem(); - Debug.Assert(currentItem != null); + if (client.Room.GetCurrentItem() is MultiplayerPlaylistItem currentItem) + { + int userBeatmapId = User.BeatmapId ?? currentItem.BeatmapID; + int userRulesetId = User.RulesetId ?? currentItem.RulesetID; + Ruleset? userRuleset = rulesets.GetRuleset(userRulesetId)?.CreateInstance(); - int userBeatmapId = User.BeatmapId ?? currentItem.BeatmapID; - int userRulesetId = User.RulesetId ?? currentItem.RulesetID; - Ruleset? userRuleset = rulesets.GetRuleset(userRulesetId)?.CreateInstance(); - Debug.Assert(userRuleset != null); + int? currentModeRank = User.User?.RulesetsStatistics?.GetValueOrDefault(userRuleset?.ShortName)?.GlobalRank; + userRankText.Text = currentModeRank != null ? $"#{currentModeRank.Value:N0}" : string.Empty; + + if (userBeatmapId == currentItem.BeatmapID && userRulesetId == currentItem.RulesetID) + userStyleDisplay.Style = null; + else + userStyleDisplay.Style = (userBeatmapId, userRulesetId); + + // If the mods are updated at the end of the frame, the flow container will skip a reflow cycle: https://github.com/ppy/osu-framework/issues/4187 + // This looks particularly jarring here, so re-schedule the update to that start of our frame as a fix. + Schedule(() => userModsDisplay.Current.Value = userRuleset == null ? Array.Empty() : User.Mods.Select(m => m.ToMod(userRuleset)).ToList()); + } userStateDisplay.UpdateStatus(User.State, User.BeatmapAvailability); - int? currentModeRank = User.User?.RulesetsStatistics?.GetValueOrDefault(userRuleset.ShortName)?.GlobalRank; - userRankText.Text = currentModeRank != null ? $"#{currentModeRank.Value:N0}" : string.Empty; - - if ((User.BeatmapAvailability.State == DownloadState.LocallyAvailable) && (User.State != MultiplayerUserState.Spectating)) + if (User.BeatmapAvailability.State == DownloadState.LocallyAvailable && User.State != MultiplayerUserState.Spectating) { userModsDisplay.FadeIn(fade_time); userStyleDisplay.FadeIn(fade_time); @@ -240,17 +248,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants userStyleDisplay.FadeOut(fade_time); } - if (userBeatmapId == currentItem.BeatmapID && userRulesetId == currentItem.RulesetID) - userStyleDisplay.Style = null; - else - userStyleDisplay.Style = (userBeatmapId, userRulesetId); - kickButton.Alpha = client.IsHost && !User.Equals(client.LocalUser) ? 1 : 0; crown.Alpha = client.Room.Host?.Equals(User) == true ? 1 : 0; - - // If the mods are updated at the end of the frame, the flow container will skip a reflow cycle: https://github.com/ppy/osu-framework/issues/4187 - // This looks particularly jarring here, so re-schedule the update to that start of our frame as a fix. - Schedule(() => userModsDisplay.Current.Value = User.Mods.Select(m => m.ToMod(userRuleset)).ToList()); } public MenuItem[]? ContextMenuItems From 748c2eb3904bdd23ab60bd2e1dbb5a2c772aecb8 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 11 Feb 2025 12:43:51 +0900 Subject: [PATCH 056/107] Refactor `RoomSubScreen` update --- .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 312253774f..59acd3c17f 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -439,13 +439,14 @@ namespace osu.Game.Screens.OnlinePlay.Match var rulesetInstance = GetGameplayRuleset().CreateInstance(); - // Remove any user mods that are no longer allowed. Mod[] allowedMods = item.Freestyle - ? rulesetInstance.CreateAllMods().Where(m => ModUtils.IsValidFreeModForMatchType(m, Room.Type)).ToArray() + ? rulesetInstance.AllMods.OfType().Where(m => ModUtils.IsValidFreeModForMatchType(m, Room.Type)).ToArray() : item.AllowedMods.Select(m => m.ToMod(rulesetInstance)).ToArray(); + + // Remove any user mods that are no longer allowed. Mod[] newUserMods = UserMods.Value.Where(m => allowedMods.Any(a => m.GetType() == a.GetType())).ToArray(); if (!newUserMods.SequenceEqual(UserMods.Value)) - UserMods.Value = UserMods.Value.Where(m => allowedMods.Any(a => m.GetType() == a.GetType())).ToList(); + UserMods.Value = newUserMods; // Retrieve the corresponding local beatmap, since we can't directly use the playlist's beatmap info int beatmapId = GetGameplayBeatmap().OnlineID; @@ -456,10 +457,7 @@ namespace osu.Game.Screens.OnlinePlay.Match Mods.Value = GetGameplayMods().Select(m => m.ToMod(rulesetInstance)).ToArray(); Ruleset.Value = GetGameplayRuleset(); - bool freestyle = item.Freestyle; - bool freeMod = freestyle || item.AllowedMods.Any(); - - if (freeMod) + if (allowedMods.Length > 0) { UserModsSection.Show(); UserModsSelectOverlay.IsValidMod = m => allowedMods.Any(a => a.GetType() == m.GetType()); @@ -471,7 +469,7 @@ namespace osu.Game.Screens.OnlinePlay.Match UserModsSelectOverlay.IsValidMod = _ => false; } - if (freestyle) + if (item.Freestyle) { UserStyleSection.Show(); @@ -484,7 +482,7 @@ namespace osu.Game.Screens.OnlinePlay.Match UserStyleDisplayContainer.Child = new DrawableRoomPlaylistItem(gameplayItem, true) { AllowReordering = false, - AllowEditing = freestyle, + AllowEditing = true, RequestEdit = _ => OpenStyleSelection() }; } From e51c09ec3d94823ea6707b3541da6d74a738344a Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 11 Feb 2025 14:23:51 +0900 Subject: [PATCH 057/107] Fix inspection Interestingly, this is not a compiler error nor does R# warn about it. No problem, because this is just restoring the original code anyway. --- .../OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs index 230245e926..0fa2be44f3 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs @@ -222,7 +222,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants int userRulesetId = User.RulesetId ?? currentItem.RulesetID; Ruleset? userRuleset = rulesets.GetRuleset(userRulesetId)?.CreateInstance(); - int? currentModeRank = User.User?.RulesetsStatistics?.GetValueOrDefault(userRuleset?.ShortName)?.GlobalRank; + int? currentModeRank = userRuleset == null ? null : User.User?.RulesetsStatistics?.GetValueOrDefault(userRuleset.ShortName)?.GlobalRank; userRankText.Text = currentModeRank != null ? $"#{currentModeRank.Value:N0}" : string.Empty; if (userBeatmapId == currentItem.BeatmapID && userRulesetId == currentItem.RulesetID) From daf0130b2307e435641a9485fb026f1071aaff6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 11 Feb 2025 08:06:12 +0100 Subject: [PATCH 058/107] Reword copy to be less verbose --- osu.Game/Localisation/EditorStrings.cs | 4 ++-- osu.Game/Screens/Edit/Editor.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Localisation/EditorStrings.cs b/osu.Game/Localisation/EditorStrings.cs index 1681e541fc..0a15752961 100644 --- a/osu.Game/Localisation/EditorStrings.cs +++ b/osu.Game/Localisation/EditorStrings.cs @@ -187,12 +187,12 @@ namespace osu.Game.Localisation /// /// "Open beatmap info page in browser" /// - public static LocalisableString OpenInfoPageInBrowser => new TranslatableString(getKey(@"open_info_page_in_browser"), @"Open beatmap info page in browser"); + public static LocalisableString OpenInfoPage => new TranslatableString(getKey(@"open_info_page"), @"Open beatmap info page"); /// /// "Open beatmap discussion page in browser" /// - public static LocalisableString OpenDiscussionPageInBrowser => new TranslatableString(getKey(@"open_discussion_page_in_browser"), @"Open beatmap discussion page in browser"); + public static LocalisableString OpenDiscussionPage => new TranslatableString(getKey(@"open_discussion_page"), @"Open beatmap discussion page"); private static string getKey(string key) => $@"{prefix}:{key}"; } diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index ecb0731c16..d73384af7f 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -1259,9 +1259,9 @@ namespace osu.Game.Screens.Edit if (editorBeatmap.BeatmapInfo.OnlineID > 0) { yield return new OsuMenuItemSpacer(); - yield return new EditorMenuItem(EditorStrings.OpenInfoPageInBrowser, MenuItemType.Standard, + yield return new EditorMenuItem(EditorStrings.OpenInfoPage, MenuItemType.Standard, () => (Game as OsuGame)?.OpenUrlExternally(editorBeatmap.BeatmapInfo.GetOnlineURL(api, editorBeatmap.BeatmapInfo.Ruleset))); - yield return new EditorMenuItem(EditorStrings.OpenDiscussionPageInBrowser, MenuItemType.Standard, + yield return new EditorMenuItem(EditorStrings.OpenDiscussionPage, MenuItemType.Standard, () => (Game as OsuGame)?.OpenUrlExternally($@"{api.Endpoints.WebsiteUrl}/beatmapsets/{editorBeatmap.BeatmapInfo.BeatmapSet!.OnlineID}/discussion/{editorBeatmap.BeatmapInfo.OnlineID}")); } From 7db0a6f81775248bbcb41a57f363b2d6a73b8875 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 11 Feb 2025 08:06:12 +0100 Subject: [PATCH 059/107] Update xmldoc --- osu.Game/Localisation/EditorStrings.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Localisation/EditorStrings.cs b/osu.Game/Localisation/EditorStrings.cs index 0a15752961..b74a546eca 100644 --- a/osu.Game/Localisation/EditorStrings.cs +++ b/osu.Game/Localisation/EditorStrings.cs @@ -185,12 +185,12 @@ namespace osu.Game.Localisation public static LocalisableString ResetBookmarks => new TranslatableString(getKey(@"reset_bookmarks"), @"Reset bookmarks"); /// - /// "Open beatmap info page in browser" + /// "Open beatmap info page" /// public static LocalisableString OpenInfoPage => new TranslatableString(getKey(@"open_info_page"), @"Open beatmap info page"); /// - /// "Open beatmap discussion page in browser" + /// "Open beatmap discussion page" /// public static LocalisableString OpenDiscussionPage => new TranslatableString(getKey(@"open_discussion_page"), @"Open beatmap discussion page"); From 1fa8d53232931e0edc37ddba22cde7aacb48e799 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 Feb 2025 17:11:20 +0900 Subject: [PATCH 060/107] Disable scale animation when holding editor "test" button --- .../Timelines/Summary/TestGameplayButton.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/TestGameplayButton.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/TestGameplayButton.cs index 169e72fe3f..065f52b929 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/TestGameplayButton.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/TestGameplayButton.cs @@ -4,6 +4,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; +using osu.Framework.Input.Events; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; @@ -33,5 +34,16 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary Text = EditorStrings.TestBeatmap; } + + protected override bool OnMouseDown(MouseDownEvent e) + { + // block scale animation + return false; + } + + protected override void OnMouseUp(MouseUpEvent e) + { + // block scale animation + } } } From 884fa20b286264482f6e965f946369e42d9fe356 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 11 Feb 2025 09:13:08 +0100 Subject: [PATCH 061/107] Remove completely unnecessary subscriptions per collection --- .../Collections/DrawableCollectionList.cs | 1 - .../Collections/DrawableCollectionListItem.cs | 32 +++++++------------ 2 files changed, 11 insertions(+), 22 deletions(-) diff --git a/osu.Game/Collections/DrawableCollectionList.cs b/osu.Game/Collections/DrawableCollectionList.cs index 85af1d383d..c494b830d1 100644 --- a/osu.Game/Collections/DrawableCollectionList.cs +++ b/osu.Game/Collections/DrawableCollectionList.cs @@ -96,7 +96,6 @@ namespace osu.Game.Collections lastCreated = collections[changes.InsertedIndices[0]].ID; foreach (int i in changes.NewModifiedIndices) - { var updatedItem = collections[i]; diff --git a/osu.Game/Collections/DrawableCollectionListItem.cs b/osu.Game/Collections/DrawableCollectionListItem.cs index 0060dacc01..703def9546 100644 --- a/osu.Game/Collections/DrawableCollectionListItem.cs +++ b/osu.Game/Collections/DrawableCollectionListItem.cs @@ -126,15 +126,10 @@ namespace osu.Game.Collections private const float count_text_size = 12; - [Resolved] - private RealmAccess realm { get; set; } = null!; - private readonly Live collection; private OsuSpriteText countText = null!; - private IDisposable? itemCountSubscription; - public ItemTextBox(Live collection) { this.collection = collection; @@ -163,29 +158,24 @@ namespace osu.Game.Collections Colour = colours.Yellow }); - itemCountSubscription = realm.SubscribeToPropertyChanged(r => r.Find(collection.ID), c => c.BeatmapMD5Hashes, _ => - Scheduler.AddOnce(() => - { - int count = collection.PerformRead(c => c.BeatmapMD5Hashes.Count); + // interestingly, it is not required to subscribe to change notifications on this collection at all for this to work correctly. + // the reasoning for this is that `DrawableCollectionList` already takes out a subscription on the set of all `BeatmapCollection`s - + // but that subscription does not only cover *changes to the set of collections* (i.e. addition/removal/rearrangement of collections), + // but also covers *changes to the properties of collections*, which `BeatmapMD5Hashes` is one. + // when a collection item changes due to `BeatmapMD5Hashes` changing, the list item is deleted and re-inserted, thus guaranteeing this to work correctly. + int count = collection.PerformRead(c => c.BeatmapMD5Hashes.Count); - countText.Text = count == 1 - // Intentionally not localised until we have proper support for this (see https://github.com/ppy/osu-framework/pull/4918 - // but also in this case we want support for formatting a number within a string). - ? $"{count:#,0} beatmap" - : $"{count:#,0} beatmaps"; - })); + countText.Text = count == 1 + // Intentionally not localised until we have proper support for this (see https://github.com/ppy/osu-framework/pull/4918 + // but also in this case we want support for formatting a number within a string). + ? $"{count:#,0} beatmap" + : $"{count:#,0} beatmaps"; } else { PlaceholderText = "Create a new collection"; } } - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - itemCountSubscription?.Dispose(); - } } public partial class DeleteButton : OsuClickableContainer From b9ed217308f3ebe7405274d8fbd257835bc259dd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 Feb 2025 17:13:34 +0900 Subject: [PATCH 062/107] Add basic brighten animation instead --- .../Timelines/Summary/TestGameplayButton.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/TestGameplayButton.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/TestGameplayButton.cs index 065f52b929..f5c0ed2382 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/TestGameplayButton.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/TestGameplayButton.cs @@ -15,6 +15,9 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary { public partial class TestGameplayButton : OsuButton { + [Resolved] + private OsuColour colours { get; set; } = null!; + protected override SpriteText CreateText() => new OsuSpriteText { Depth = -1, @@ -25,7 +28,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary }; [BackgroundDependencyLoader] - private void load(OsuColour colours, OverlayColourProvider colourProvider) + private void load(OverlayColourProvider colourProvider) { BackgroundColour = colours.Orange1; SpriteText.Colour = colourProvider.Background6; @@ -37,13 +40,15 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary protected override bool OnMouseDown(MouseDownEvent e) { - // block scale animation + Background.FadeColour(colours.Orange0, 500, Easing.OutQuint); + // don't call base in order to block scale animation return false; } protected override void OnMouseUp(MouseUpEvent e) { - // block scale animation + Background.FadeColour(colours.Orange1, 300, Easing.OutQuint); + // don't call base in order to block scale animation } } } From d8b3c28c2e5cb3c666ae937d4cd13feb7d5475d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 11 Feb 2025 09:17:11 +0100 Subject: [PATCH 063/107] Use more neutral terminology to avoid contentious 'beatmap' term --- osu.Game/Collections/DrawableCollectionListItem.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Collections/DrawableCollectionListItem.cs b/osu.Game/Collections/DrawableCollectionListItem.cs index 703def9546..f2b00004e2 100644 --- a/osu.Game/Collections/DrawableCollectionListItem.cs +++ b/osu.Game/Collections/DrawableCollectionListItem.cs @@ -168,8 +168,8 @@ namespace osu.Game.Collections countText.Text = count == 1 // Intentionally not localised until we have proper support for this (see https://github.com/ppy/osu-framework/pull/4918 // but also in this case we want support for formatting a number within a string). - ? $"{count:#,0} beatmap" - : $"{count:#,0} beatmaps"; + ? $"{count:#,0} item" + : $"{count:#,0} items"; } else { From b9c4e235958796bb4f85b9734b5f685541ea13d3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 Feb 2025 20:05:48 +0900 Subject: [PATCH 064/107] Fix potential bad realm access to collection name --- osu.Game/Collections/DrawableCollectionListItem.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Collections/DrawableCollectionListItem.cs b/osu.Game/Collections/DrawableCollectionListItem.cs index f2b00004e2..b0dd70227c 100644 --- a/osu.Game/Collections/DrawableCollectionListItem.cs +++ b/osu.Game/Collections/DrawableCollectionListItem.cs @@ -255,7 +255,7 @@ namespace osu.Game.Collections private void deleteCollection() => collection.PerformWrite(c => c.Realm!.Remove(c)); } - public IEnumerable FilterTerms => [(LocalisableString)Model.Value.Name]; + public IEnumerable FilterTerms => Model.PerformRead(m => m.IsValid ? new[] { (LocalisableString)m.Name } : []); private bool matchingFilter = true; From be035538c241f29ef609c9f73c670b0056278222 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 11 Feb 2025 14:01:32 +0100 Subject: [PATCH 065/107] Fix remaining hit counter scaling in the incorrect direction --- osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs index 0eb80d333f..c819cb7937 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs @@ -108,8 +108,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy private void animateSwellProgress(int numHits, int requiredHits) { - remainingHitsText.Text = $"{requiredHits - numHits}"; - remainingHitsText.ScaleTo(1.6f - (0.6f * ((float)numHits / requiredHits)), 60, Easing.OutQuad); + int remainingHits = requiredHits - numHits; + remainingHitsText.Text = remainingHits.ToString(); + remainingHitsText.ScaleTo(1.6f - (0.6f * ((float)remainingHits / requiredHits)), 60, Easing.OutQuad); spinnerCircle.ClearTransforms(); spinnerCircle From 231988bc9de21a5e7cdc0fbd838e6cb20c75990a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 11 Feb 2025 15:20:36 +0100 Subject: [PATCH 066/107] Adjust things to be closer to stable (but not close enough yet) --- .../Skinning/Legacy/LegacySwell.cs | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs index c819cb7937..d3b5d54828 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs @@ -14,6 +14,7 @@ using osuTK; using osu.Game.Rulesets.Objects.Drawables; using osu.Framework.Extensions.ObjectExtensions; using System; +using System.Globalization; namespace osu.Game.Rulesets.Taiko.Skinning.Legacy { @@ -39,7 +40,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy } [BackgroundDependencyLoader] - private void load(DrawableHitObject hitObject, ISkinSource skin, SkinManager skinManager) + private void load(DrawableHitObject hitObject, ISkinSource skin) { Children = new Drawable[] { @@ -54,7 +55,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Position = new Vector2(200f, 100f), + Position = new Vector2(250f, 100f), // ballparked to be horizontally centred on 4:3 resolution Children = new Drawable[] { @@ -109,14 +110,14 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy private void animateSwellProgress(int numHits, int requiredHits) { int remainingHits = requiredHits - numHits; - remainingHitsText.Text = remainingHits.ToString(); - remainingHitsText.ScaleTo(1.6f - (0.6f * ((float)remainingHits / requiredHits)), 60, Easing.OutQuad); + remainingHitsText.Text = remainingHits.ToString(CultureInfo.InvariantCulture); + remainingHitsText.ScaleTo(1.6f - (0.6f * ((float)remainingHits / requiredHits)), 60, Easing.Out); spinnerCircle.ClearTransforms(); spinnerCircle .RotateTo(180f * numHits, 1000, Easing.OutQuint) .ScaleTo(Math.Min(0.94f, spinnerCircle.Scale.X + 0.02f)) - .ScaleTo(0.8f, 400, Easing.OutQuad); + .ScaleTo(0.8f, 400, Easing.Out); } private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state) @@ -134,7 +135,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy samplePlayed = false; } - const double body_transition_duration = 100; + const double body_transition_duration = 200; warning.FadeOut(body_transition_duration); bodyContainer.FadeIn(body_transition_duration); @@ -146,9 +147,8 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy const double clear_transition_duration = 300; const double clear_fade_in = 120; - bodyContainer - .FadeOut(clear_transition_duration, Easing.OutQuad) - .ScaleTo(1.05f, clear_transition_duration, Easing.OutQuad); + bodyContainer.FadeOut(clear_transition_duration, Easing.OutQuad); + spinnerCircle.ScaleTo(spinnerCircle.Scale.X + 0.05f, clear_transition_duration, Easing.OutQuad); if (state == ArmedState.Hit) { @@ -159,11 +159,11 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy } clearAnimation - .FadeIn(clear_fade_in) .MoveTo(new Vector2(0, 0)) + .MoveTo(new Vector2(0, -90), clear_fade_in * 2, Easing.Out) .ScaleTo(0.4f) - .MoveTo(new Vector2(0, -90), clear_fade_in * 2, Easing.OutQuad) .ScaleTo(1f, clear_fade_in * 2, Easing.Out) + .FadeIn(clear_fade_in) .Delay(clear_fade_in * 3) .FadeOut(clear_fade_in * 2.5); } From a8f07ae7b1ebce7579cc97a14264b7132b017f1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Tue, 11 Feb 2025 18:04:23 +0100 Subject: [PATCH 067/107] Add comment warning about enum entry order in `GlobalAction` --- osu.Game/Input/Bindings/GlobalActionContainer.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index 599ca6d6c1..e4dc2d503b 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -227,6 +227,10 @@ namespace osu.Game.Input.Bindings }; } + /// + /// IMPORTANT: New entries should always be added at the end of the enum, as key bindings are stored using the enum's numeric value and + /// changes in order would cause key bindings to get associated with the wrong action. + /// public enum GlobalAction { [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ToggleChat))] From ffd8bd7bf4dd4d238986c90e598ad11580667d01 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Feb 2025 16:14:12 +0900 Subject: [PATCH 068/107] Rename `ParentObject` to `DrawableObject` It's not a parent. The follow circle is directly part of the slider itself. --- .../Skinning/FollowCircle.cs | 51 ++++++++++--------- .../Skinning/Legacy/LegacyFollowCircle.cs | 4 +- 2 files changed, 28 insertions(+), 27 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/FollowCircle.cs b/osu.Game.Rulesets.Osu/Skinning/FollowCircle.cs index 4fadb09948..d1836010fb 100644 --- a/osu.Game.Rulesets.Osu/Skinning/FollowCircle.cs +++ b/osu.Game.Rulesets.Osu/Skinning/FollowCircle.cs @@ -13,8 +13,7 @@ namespace osu.Game.Rulesets.Osu.Skinning { public abstract partial class FollowCircle : CompositeDrawable { - [Resolved] - protected DrawableHitObject? ParentObject { get; private set; } + protected DrawableSlider? DrawableObject { get; private set; } protected FollowCircle() { @@ -22,16 +21,18 @@ namespace osu.Game.Rulesets.Osu.Skinning } [BackgroundDependencyLoader] - private void load() + private void load(DrawableHitObject? hitObject) { - ((DrawableSlider?)ParentObject)?.Tracking.BindValueChanged(tracking => - { - Debug.Assert(ParentObject != null); + DrawableObject = hitObject as DrawableSlider; - if (ParentObject.Judged) + DrawableObject?.Tracking.BindValueChanged(tracking => + { + Debug.Assert(DrawableObject != null); + + if (DrawableObject.Judged) return; - using (BeginAbsoluteSequence(Math.Max(Time.Current, ParentObject.HitObject?.StartTime ?? 0))) + using (BeginAbsoluteSequence(Math.Max(Time.Current, DrawableObject.HitObject?.StartTime ?? 0))) { if (tracking.NewValue) OnSliderPress(); @@ -45,13 +46,13 @@ namespace osu.Game.Rulesets.Osu.Skinning { base.LoadComplete(); - if (ParentObject != null) + if (DrawableObject != null) { - ParentObject.HitObjectApplied += onHitObjectApplied; - onHitObjectApplied(ParentObject); + DrawableObject.HitObjectApplied += onHitObjectApplied; + onHitObjectApplied(DrawableObject); - ParentObject.ApplyCustomUpdateState += updateStateTransforms; - updateStateTransforms(ParentObject, ParentObject.State.Value); + DrawableObject.ApplyCustomUpdateState += updateStateTransforms; + updateStateTransforms(DrawableObject, DrawableObject.State.Value); } } @@ -61,26 +62,26 @@ namespace osu.Game.Rulesets.Osu.Skinning .FadeOut(); } - private void updateStateTransforms(DrawableHitObject drawableObject, ArmedState state) + private void updateStateTransforms(DrawableHitObject d, ArmedState state) { - Debug.Assert(ParentObject != null); + Debug.Assert(DrawableObject != null); switch (state) { case ArmedState.Hit: - switch (drawableObject) + switch (d) { case DrawableSliderTail: - // Use ParentObject instead of drawableObject because slider tail's + // Use DrawableObject instead of local object because slider tail's // HitStateUpdateTime is ~36ms before the actual slider end (aka slider // tail leniency) - using (BeginAbsoluteSequence(ParentObject.HitStateUpdateTime)) + using (BeginAbsoluteSequence(DrawableObject.HitStateUpdateTime)) OnSliderEnd(); break; case DrawableSliderTick: case DrawableSliderRepeat: - using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime)) + using (BeginAbsoluteSequence(d.HitStateUpdateTime)) OnSliderTick(); break; } @@ -88,15 +89,15 @@ namespace osu.Game.Rulesets.Osu.Skinning break; case ArmedState.Miss: - switch (drawableObject) + switch (d) { case DrawableSliderTail: case DrawableSliderTick: case DrawableSliderRepeat: - // Despite above comment, ok to use drawableObject.HitStateUpdateTime + // Despite above comment, ok to use d.HitStateUpdateTime // here, since on stable, the break anim plays right when the tail is // missed, not when the slider ends - using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime)) + using (BeginAbsoluteSequence(d.HitStateUpdateTime)) OnSliderBreak(); break; } @@ -109,10 +110,10 @@ namespace osu.Game.Rulesets.Osu.Skinning { base.Dispose(isDisposing); - if (ParentObject != null) + if (DrawableObject != null) { - ParentObject.HitObjectApplied -= onHitObjectApplied; - ParentObject.ApplyCustomUpdateState -= updateStateTransforms; + DrawableObject.HitObjectApplied -= onHitObjectApplied; + DrawableObject.ApplyCustomUpdateState -= updateStateTransforms; } } diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyFollowCircle.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyFollowCircle.cs index 4a8b737206..f60b5cfe12 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyFollowCircle.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyFollowCircle.cs @@ -22,9 +22,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy protected override void OnSliderPress() { - Debug.Assert(ParentObject != null); + Debug.Assert(DrawableObject != null); - double remainingTime = Math.Max(0, ParentObject.HitStateUpdateTime - Time.Current); + double remainingTime = Math.Max(0, DrawableObject.HitStateUpdateTime - Time.Current); // Note that the scale adjust here is 2 instead of DrawableSliderBall.FOLLOW_AREA to match legacy behaviour. // This means the actual tracking area for gameplay purposes is larger than the sprite (but skins may be accounting for this). From f97708e6b3bd4bc516e7837e43599b5f1c88c6f7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Feb 2025 16:28:14 +0900 Subject: [PATCH 069/107] Avoid binding directly to DHO's bindable --- .../Skinning/FollowCircle.cs | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/FollowCircle.cs b/osu.Game.Rulesets.Osu/Skinning/FollowCircle.cs index d1836010fb..903ba08010 100644 --- a/osu.Game.Rulesets.Osu/Skinning/FollowCircle.cs +++ b/osu.Game.Rulesets.Osu/Skinning/FollowCircle.cs @@ -4,6 +4,7 @@ using System; using System.Diagnostics; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Objects.Drawables; @@ -15,6 +16,8 @@ namespace osu.Game.Rulesets.Osu.Skinning { protected DrawableSlider? DrawableObject { get; private set; } + private readonly IBindable tracking = new Bindable(); + protected FollowCircle() { RelativeSizeAxes = Axes.Both; @@ -25,21 +28,23 @@ namespace osu.Game.Rulesets.Osu.Skinning { DrawableObject = hitObject as DrawableSlider; - DrawableObject?.Tracking.BindValueChanged(tracking => + if (DrawableObject != null) { - Debug.Assert(DrawableObject != null); - - if (DrawableObject.Judged) - return; - - using (BeginAbsoluteSequence(Math.Max(Time.Current, DrawableObject.HitObject?.StartTime ?? 0))) + tracking.BindTo(DrawableObject.Tracking); + tracking.BindValueChanged(tracking => { - if (tracking.NewValue) - OnSliderPress(); - else - OnSliderRelease(); - } - }, true); + if (DrawableObject.Judged) + return; + + using (BeginAbsoluteSequence(Math.Max(Time.Current, DrawableObject.HitObject?.StartTime ?? 0))) + { + if (tracking.NewValue) + OnSliderPress(); + else + OnSliderRelease(); + } + }, true); + } } protected override void LoadComplete() From 84b5ea3dbf6ab7b6209820468d3369e477f9d1b1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Feb 2025 16:33:23 +0900 Subject: [PATCH 070/107] Fix weird follow circle display when rewinding through sliders in editor Closes https://github.com/ppy/osu/issues/31812. --- osu.Game.Rulesets.Osu/Skinning/FollowCircle.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Skinning/FollowCircle.cs b/osu.Game.Rulesets.Osu/Skinning/FollowCircle.cs index 903ba08010..db789166c6 100644 --- a/osu.Game.Rulesets.Osu/Skinning/FollowCircle.cs +++ b/osu.Game.Rulesets.Osu/Skinning/FollowCircle.cs @@ -63,8 +63,12 @@ namespace osu.Game.Rulesets.Osu.Skinning private void onHitObjectApplied(DrawableHitObject drawableObject) { + // Sane defaults when a new hitobject is applied to the drawable slider. this.ScaleTo(1f) .FadeOut(); + + // Immediately play out any pending transforms from press/release + FinishTransforms(true); } private void updateStateTransforms(DrawableHitObject d, ArmedState state) From b92e9f515bd291a19546538355aeb48001933829 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Feb 2025 17:31:55 +0900 Subject: [PATCH 071/107] Fix layout of user setting areas when aspect ratio is vertically tall --- .../Multiplayer/MultiplayerMatchSubScreen.cs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index a16c5c9442..ff4c8c2fd9 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -121,9 +121,18 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer new GridContainer { RelativeSizeAxes = Axes.Both, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.Absolute, 5), + new Dimension(), + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.AutoSize), + }, Content = new[] { - new Drawable[] { new OverlinedHeader("Beatmap") }, + new Drawable[] { new OverlinedHeader("Beatmap queue") }, new Drawable[] { addItemButton = new AddItemButton @@ -202,14 +211,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer }, }, }, - RowDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize), - new Dimension(GridSizeMode.AutoSize), - new Dimension(GridSizeMode.Absolute, 5), - new Dimension(), - new Dimension(GridSizeMode.AutoSize), - } }, null, new GridContainer From 9aef95c38127ae72b2538326e561a28db5d3acda Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Feb 2025 17:43:49 +0900 Subject: [PATCH 072/107] Adjust some paddings and text MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mostly trying to give more space to the queue as we add more vertical elements to the middle area of multiplayer / playerlists. This whole UI will likely change – this is just a stop-gap fix. --- osu.Game/Screens/OnlinePlay/Components/OverlinedHeader.cs | 2 -- .../Multiplayer/Match/Playlist/MultiplayerPlaylistTabControl.cs | 2 +- .../Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs | 1 + 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Components/OverlinedHeader.cs b/osu.Game/Screens/OnlinePlay/Components/OverlinedHeader.cs index d9cdcac7d7..6dfde183f0 100644 --- a/osu.Game/Screens/OnlinePlay/Components/OverlinedHeader.cs +++ b/osu.Game/Screens/OnlinePlay/Components/OverlinedHeader.cs @@ -53,13 +53,11 @@ namespace osu.Game.Screens.OnlinePlay.Components { RelativeSizeAxes = Axes.X, Height = 2, - Margin = new MarginPadding { Bottom = 2 } }, new FillFlowContainer { AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, - Margin = new MarginPadding { Top = 5 }, Spacing = new Vector2(10, 0), Children = new Drawable[] { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylistTabControl.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylistTabControl.cs index e5d94c5358..a7f3e17efa 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylistTabControl.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylistTabControl.cs @@ -32,7 +32,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist protected override void LoadComplete() { base.LoadComplete(); - QueueItems.BindCollectionChanged((_, _) => Text.Text = QueueItems.Count > 0 ? $"Queue ({QueueItems.Count})" : "Queue", true); + QueueItems.BindCollectionChanged((_, _) => Text.Text = QueueItems.Count > 0 ? $"Up next ({QueueItems.Count})" : "Up next", true); } } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index ff4c8c2fd9..083c8e070e 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -176,6 +176,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, Width = 90, + Height = 30, Text = "Select", Action = ShowUserModSelect, }, From 9c3e9e7c55b8aad452151c2c1b13a00660b3f52d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Feb 2025 17:56:15 +0900 Subject: [PATCH 073/107] Change free mods button to show "all" when freestyle is enabled --- .../TestSceneFreeModSelectOverlay.cs | 2 +- .../OnlinePlay/FooterButtonFreeMods.cs | 28 ++++++------------- .../OnlinePlay/FooterButtonFreestyle.cs | 15 ++++------ .../OnlinePlay/OnlinePlaySongSelect.cs | 20 +++++++++---- 4 files changed, 29 insertions(+), 36 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs index fb54b89a4b..fd589e928a 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs @@ -166,7 +166,7 @@ namespace osu.Game.Tests.Visual.Multiplayer Anchor = Anchor.BottomRight, Origin = Anchor.BottomRight, Y = -ScreenFooter.HEIGHT, - Current = { BindTarget = freeModSelectOverlay.SelectedMods }, + FreeMods = { BindTarget = freeModSelectOverlay.SelectedMods }, }, footer = new ScreenFooter(), }, diff --git a/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs b/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs index 402f538716..695ed74ab9 100644 --- a/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs +++ b/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs @@ -11,31 +11,20 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; +using osu.Game.Localisation; using osu.Game.Rulesets.Mods; using osu.Game.Screens.Select; using osuTK; -using osu.Game.Localisation; namespace osu.Game.Screens.OnlinePlay { - public partial class FooterButtonFreeMods : FooterButton, IHasCurrentValue> + public partial class FooterButtonFreeMods : FooterButton { - private readonly BindableWithCurrent> current = new BindableWithCurrent>(Array.Empty()); - - public Bindable> Current - { - get => current.Current; - set - { - ArgumentNullException.ThrowIfNull(value); - - current.Current = value; - } - } + public readonly Bindable> FreeMods = new Bindable>(); + public readonly IBindable Freestyle = new Bindable(); public new Action Action { set => throw new NotSupportedException("The click action is handled by the button itself."); } @@ -104,7 +93,8 @@ namespace osu.Game.Screens.OnlinePlay { base.LoadComplete(); - Current.BindValueChanged(_ => updateModDisplay(), true); + Freestyle.BindValueChanged(_ => updateModDisplay()); + FreeMods.BindValueChanged(_ => updateModDisplay(), true); } /// @@ -114,16 +104,16 @@ namespace osu.Game.Screens.OnlinePlay { var availableMods = allAvailableAndValidMods.ToArray(); - Current.Value = Current.Value.Count == availableMods.Length + FreeMods.Value = FreeMods.Value.Count == availableMods.Length ? Array.Empty() : availableMods; } private void updateModDisplay() { - int currentCount = Current.Value.Count; + int currentCount = FreeMods.Value.Count; - if (currentCount == allAvailableAndValidMods.Count()) + if (currentCount == allAvailableAndValidMods.Count() || Freestyle.Value) { count.Text = "all"; count.FadeColour(colours.Gray2, 200, Easing.OutQuint); diff --git a/osu.Game/Screens/OnlinePlay/FooterButtonFreestyle.cs b/osu.Game/Screens/OnlinePlay/FooterButtonFreestyle.cs index 157f90d078..d907fec489 100644 --- a/osu.Game/Screens/OnlinePlay/FooterButtonFreestyle.cs +++ b/osu.Game/Screens/OnlinePlay/FooterButtonFreestyle.cs @@ -16,15 +16,10 @@ using osu.Game.Localisation; namespace osu.Game.Screens.OnlinePlay { - public partial class FooterButtonFreestyle : FooterButton, IHasCurrentValue + public partial class FooterButtonFreestyle : FooterButton { - private readonly BindableWithCurrent current = new BindableWithCurrent(); + public readonly Bindable Freestyle = new Bindable(); - public Bindable Current - { - get => current.Current; - set => current.Current = value; - } public new Action Action { set => throw new NotSupportedException("The click action is handled by the button itself."); } @@ -37,7 +32,7 @@ namespace osu.Game.Screens.OnlinePlay public FooterButtonFreestyle() { // Overwrite any external behaviour as we delegate the main toggle action to a sub-button. - base.Action = () => current.Value = !current.Value; + base.Action = () => Freestyle.Value = !Freestyle.Value; } [BackgroundDependencyLoader] @@ -81,12 +76,12 @@ namespace osu.Game.Screens.OnlinePlay { base.LoadComplete(); - Current.BindValueChanged(_ => updateDisplay(), true); + Freestyle.BindValueChanged(_ => updateDisplay(), true); } private void updateDisplay() { - if (current.Value) + if (Freestyle.Value) { text.Text = "on"; text.FadeColour(colours.Gray2, 200, Easing.OutQuint); diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs index 1164c4c0fc..cf351b31bf 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs @@ -126,6 +126,7 @@ namespace osu.Game.Screens.OnlinePlay { if (enabled.NewValue) { + freeModsFooterButton.Enabled.Value = false; freeModsFooterButton.Enabled.Value = false; ModsFooterButton.Enabled.Value = false; @@ -205,8 +206,15 @@ namespace osu.Game.Screens.OnlinePlay baseButtons.InsertRange(baseButtons.FindIndex(b => b.button is FooterButtonMods) + 1, new (FooterButton, OverlayContainer?)[] { - (freeModsFooterButton = new FooterButtonFreeMods(freeModSelect) { Current = FreeMods }, null), - (new FooterButtonFreestyle { Current = Freestyle }, null) + (freeModsFooterButton = new FooterButtonFreeMods(freeModSelect) + { + FreeMods = { BindTarget = FreeMods }, + Freestyle = { BindTarget = Freestyle } + }, null), + (new FooterButtonFreestyle + { + Freestyle = { BindTarget = Freestyle } + }, null) }); return baseButtons; @@ -225,10 +233,10 @@ namespace osu.Game.Screens.OnlinePlay /// The to check. /// Whether is a selectable free-mod. private bool isValidFreeMod(Mod mod) => ModUtils.IsValidFreeModForMatchType(mod, room.Type) - // Mod must not be contained in the required mods. - && Mods.Value.All(m => m.Acronym != mod.Acronym) - // Mod must be compatible with all the required mods. - && ModUtils.CheckCompatibleSet(Mods.Value.Append(mod).ToArray()); + // Mod must not be contained in the required mods. + && Mods.Value.All(m => m.Acronym != mod.Acronym) + // Mod must be compatible with all the required mods. + && ModUtils.CheckCompatibleSet(Mods.Value.Append(mod).ToArray()); protected override void Dispose(bool isDisposing) { From 218151bb3c7af0fe77b32e55757cc0079b40cce6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Feb 2025 18:27:53 +0900 Subject: [PATCH 074/107] Flash footer freemod/freestyle buttons when active --- .../Screens/OnlinePlay/FooterButtonFreeMods.cs | 2 ++ .../Screens/OnlinePlay/FooterButtonFreestyle.cs | 4 ++-- .../Screens/OnlinePlay/OnlinePlaySongSelect.cs | 2 +- osu.Game/Screens/Select/FooterButton.cs | 17 +++++++++++++++++ 4 files changed, 22 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs b/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs index 695ed74ab9..3605412b2b 100644 --- a/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs +++ b/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs @@ -26,6 +26,8 @@ namespace osu.Game.Screens.OnlinePlay public readonly Bindable> FreeMods = new Bindable>(); public readonly IBindable Freestyle = new Bindable(); + protected override bool IsActive => FreeMods.Value.Count > 0; + public new Action Action { set => throw new NotSupportedException("The click action is handled by the button itself."); } private OsuSpriteText count = null!; diff --git a/osu.Game/Screens/OnlinePlay/FooterButtonFreestyle.cs b/osu.Game/Screens/OnlinePlay/FooterButtonFreestyle.cs index d907fec489..6ee983af20 100644 --- a/osu.Game/Screens/OnlinePlay/FooterButtonFreestyle.cs +++ b/osu.Game/Screens/OnlinePlay/FooterButtonFreestyle.cs @@ -8,11 +8,10 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; -using osu.Game.Screens.Select; using osu.Game.Localisation; +using osu.Game.Screens.Select; namespace osu.Game.Screens.OnlinePlay { @@ -20,6 +19,7 @@ namespace osu.Game.Screens.OnlinePlay { public readonly Bindable Freestyle = new Bindable(); + protected override bool IsActive => Freestyle.Value; public new Action Action { set => throw new NotSupportedException("The click action is handled by the button itself."); } diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs index cf351b31bf..9bedecc221 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs @@ -42,7 +42,7 @@ namespace osu.Game.Screens.OnlinePlay protected override UserActivity InitialActivity => new UserActivity.InLobby(room); protected readonly Bindable> FreeMods = new Bindable>(Array.Empty()); - protected readonly Bindable Freestyle = new Bindable(); + protected readonly Bindable Freestyle = new Bindable(true); private readonly Room room; private readonly PlaylistItem? initialItem; diff --git a/osu.Game/Screens/Select/FooterButton.cs b/osu.Game/Screens/Select/FooterButton.cs index 128e750dca..dafa0b0c1c 100644 --- a/osu.Game/Screens/Select/FooterButton.cs +++ b/osu.Game/Screens/Select/FooterButton.cs @@ -25,6 +25,11 @@ namespace osu.Game.Screens.Select protected static readonly Vector2 SHEAR = new Vector2(SHEAR_WIDTH / Footer.HEIGHT, 0); + /// + /// Used to show an initial animation hinting at the enabled state. + /// + protected virtual bool IsActive => false; + public LocalisableString Text { get => SpriteText?.Text ?? default; @@ -124,6 +129,18 @@ namespace osu.Game.Screens.Select { base.LoadComplete(); Enabled.BindValueChanged(_ => updateDisplay(), true); + + if (IsActive) + { + box.ClearTransforms(); + + using (box.BeginDelayedSequence(200)) + { + box.FadeIn(200) + .Then() + .FadeOut(1500, Easing.OutQuint); + } + } } public Action Hovered; From c049ae69370629f8c8c888705b6cb6feb7ad2ef4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Feb 2025 18:45:00 +0900 Subject: [PATCH 075/107] Update height specification for playlist screen too --- osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index 957a51c467..7f2255e482 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -204,6 +204,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, Width = 90, + Height = 30, Text = "Select", Action = ShowUserModSelect, }, From 96db6964df2e1045eacedebae3bfdf95eb250983 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 12 Feb 2025 10:55:57 +0100 Subject: [PATCH 076/107] Adjust things further --- .../Skinning/Legacy/LegacySwell.cs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs index d3b5d54828..5d65ac6058 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs @@ -15,11 +15,14 @@ using osu.Game.Rulesets.Objects.Drawables; using osu.Framework.Extensions.ObjectExtensions; using System; using System.Globalization; +using osu.Framework.Utils; namespace osu.Game.Rulesets.Taiko.Skinning.Legacy { public partial class LegacySwell : Container { + private const float scale_adjust = 768f / 480; + private DrawableSwell drawableSwell = null!; private Container bodyContainer = null!; @@ -80,12 +83,13 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy Anchor = Anchor.Centre, Origin = Anchor.Centre, Scale = new Vector2(1.86f * 0.8f), + Alpha = 0.8f, }, - remainingHitsText = new LegacySpriteText(LegacyFont.Combo) + remainingHitsText = new LegacySpriteText(LegacyFont.Score) { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Position = new Vector2(0f, 165f), + Position = new Vector2(0f, 130f), Scale = Vector2.One, }, } @@ -96,6 +100,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy Anchor = Anchor.Centre, Origin = Anchor.Centre, Alpha = 0, + Y = -40, }, }, }, @@ -159,11 +164,10 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy } clearAnimation - .MoveTo(new Vector2(0, 0)) - .MoveTo(new Vector2(0, -90), clear_fade_in * 2, Easing.Out) + .MoveToOffset(new Vector2(0, -90 * scale_adjust), clear_fade_in * 2, Easing.Out) .ScaleTo(0.4f) .ScaleTo(1f, clear_fade_in * 2, Easing.Out) - .FadeIn(clear_fade_in) + .FadeIn() .Delay(clear_fade_in * 3) .FadeOut(clear_fade_in * 2.5); } From 0ac08158e33867092f76f94b1534ba3bc1ce962c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 12 Feb 2025 11:20:27 +0100 Subject: [PATCH 077/107] Fix transforms from swell progress being cleared on completion by not using transforms --- .../Objects/Drawables/DrawableSwell.cs | 4 +-- .../Skinning/Default/DefaultSwell.cs | 26 +++++++++++------ .../Skinning/Legacy/LegacySwell.cs | 28 +++++++++++++------ 3 files changed, 39 insertions(+), 19 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs index 1dde4b6f9c..6ad14c87d1 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs @@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables /// public bool MustAlternate { get; internal set; } = true; - public event Action UpdateHitProgress; + public event Action UpdateHitProgress; public DrawableSwell() : this(null) @@ -125,7 +125,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables int numHits = ticks.Count(r => r.IsHit); - UpdateHitProgress?.Invoke(numHits, HitObject.RequiredHits); + UpdateHitProgress?.Invoke(numHits); if (numHits == HitObject.RequiredHits) ApplyMaxResult(); diff --git a/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs index a588f866c6..ac72ba73b8 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs @@ -8,10 +8,12 @@ using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Utils; using osu.Game.Graphics; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Objects.Drawables; +using osuTK; using osuTK.Graphics; namespace osu.Game.Rulesets.Taiko.Skinning.Default @@ -29,6 +31,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default private readonly CircularContainer targetRing; private readonly CircularContainer expandingRing; private readonly Drawable centreCircle; + private int numHits; public DefaultSwell() { @@ -125,18 +128,25 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default }; } - private void animateSwellProgress(int numHits, int requiredHits) + private void animateSwellProgress(int numHits) { - float completion = (float)numHits / requiredHits; + this.numHits = numHits; - centreCircle.RotateTo((float)(completion * drawableSwell.HitObject.Duration / 8), 4000, Easing.OutQuint); + float completion = (float)numHits / drawableSwell.HitObject.RequiredHits; + expandingRing.Alpha += Math.Clamp(completion / 16, 0.1f, 0.6f); + } - expandingRing.ScaleTo(1f + Math.Min(target_ring_scale - 1f, (target_ring_scale - 1f) * completion * 1.3f), 260, Easing.OutQuint); + protected override void Update() + { + base.Update(); - expandingRing - .FadeTo(expandingRing.Alpha + Math.Clamp(completion / 16, 0.1f, 0.6f), 50) - .Then() - .FadeTo(completion / 8, 2000, Easing.OutQuint); + float completion = (float)numHits / drawableSwell.HitObject.RequiredHits; + + centreCircle.Rotation = (float)Interpolation.DampContinuously(centreCircle.Rotation, + (float)(completion * drawableSwell.HitObject.Duration / 8), 500, Math.Abs(Time.Elapsed)); + expandingRing.Scale = new Vector2((float)Interpolation.DampContinuously(expandingRing.Scale.X, + 1f + Math.Min(target_ring_scale - 1f, (target_ring_scale - 1f) * completion * 1.3f), 35, Math.Abs(Time.Elapsed))); + expandingRing.Alpha = (float)Interpolation.DampContinuously(expandingRing.Alpha, completion / 16, 250, Math.Abs(Time.Elapsed)); } private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs index 5d65ac6058..62ccd05a06 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs @@ -35,6 +35,8 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy private bool samplePlayed; + private int numHits; + public LegacySwell() { Anchor = Anchor.Centre; @@ -112,17 +114,25 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy clearSample = skin.GetSample(new SampleInfo("spinner-osu")); } - private void animateSwellProgress(int numHits, int requiredHits) + private void animateSwellProgress(int numHits) { - int remainingHits = requiredHits - numHits; - remainingHitsText.Text = remainingHits.ToString(CultureInfo.InvariantCulture); - remainingHitsText.ScaleTo(1.6f - (0.6f * ((float)remainingHits / requiredHits)), 60, Easing.Out); + this.numHits = numHits; + remainingHitsText.Text = (drawableSwell.HitObject.RequiredHits - numHits).ToString(CultureInfo.InvariantCulture); + spinnerCircle.Scale = new Vector2(Math.Min(0.94f, spinnerCircle.Scale.X + 0.02f)); + } - spinnerCircle.ClearTransforms(); - spinnerCircle - .RotateTo(180f * numHits, 1000, Easing.OutQuint) - .ScaleTo(Math.Min(0.94f, spinnerCircle.Scale.X + 0.02f)) - .ScaleTo(0.8f, 400, Easing.Out); + protected override void Update() + { + base.Update(); + + int requiredHits = drawableSwell.HitObject.RequiredHits; + int remainingHits = requiredHits - numHits; + remainingHitsText.Scale = new Vector2((float)Interpolation.DampContinuously( + remainingHitsText.Scale.X, 1.6f - (0.6f * ((float)remainingHits / requiredHits)), 17.5, Math.Abs(Time.Elapsed))); + + spinnerCircle.Rotation = (float)Interpolation.DampContinuously(spinnerCircle.Rotation, 180f * numHits, 130, Math.Abs(Time.Elapsed)); + spinnerCircle.Scale = new Vector2((float)Interpolation.DampContinuously( + spinnerCircle.Scale.X, 0.8f, 120, Math.Abs(Time.Elapsed))); } private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state) From e385848edcbfbab7eaf0618a01ffb98aeed209d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 12 Feb 2025 11:30:30 +0100 Subject: [PATCH 078/107] Add missing animation of warning sprite --- osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs index 62ccd05a06..c9e03d3508 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs @@ -22,6 +22,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy public partial class LegacySwell : Container { private const float scale_adjust = 768f / 480; + private static readonly Vector2 swell_display_position = new Vector2(250f, 100f); private DrawableSwell drawableSwell = null!; @@ -60,7 +61,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Position = new Vector2(250f, 100f), // ballparked to be horizontally centred on 4:3 resolution + Position = swell_display_position, // ballparked to be horizontally centred on 4:3 resolution Children = new Drawable[] { @@ -152,7 +153,10 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy const double body_transition_duration = 200; - warning.FadeOut(body_transition_duration); + warning.MoveTo(swell_display_position, body_transition_duration) + .ScaleTo(3, body_transition_duration, Easing.Out) + .FadeOut(body_transition_duration); + bodyContainer.FadeIn(body_transition_duration); approachCircle.ResizeTo(0.1f * 0.8f, swell.Duration); } From d87a775e716801705b1de47cc4d2776770c348ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 12 Feb 2025 13:19:55 +0100 Subject: [PATCH 079/107] Fix clear sample potentially playing multiple times simultaneously --- osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs index c9e03d3508..9f1b692984 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs @@ -8,7 +8,6 @@ using osu.Game.Skinning; using osu.Framework.Graphics.Sprites; using osu.Game.Rulesets.Taiko.Objects.Drawables; using osu.Game.Rulesets.Taiko.Objects; -using osu.Framework.Audio.Sample; using osu.Game.Audio; using osuTK; using osu.Game.Rulesets.Objects.Drawables; @@ -31,7 +30,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy private Sprite spinnerCircle = null!; private Sprite approachCircle = null!; private Sprite clearAnimation = null!; - private ISample? clearSample; + private SkinnableSound clearSample = null!; private LegacySpriteText remainingHitsText = null!; private bool samplePlayed; @@ -107,12 +106,12 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy }, }, }, + clearSample = new SkinnableSound(new SampleInfo("spinner-osu")), }; drawableSwell = (DrawableSwell)hitObject; drawableSwell.UpdateHitProgress += animateSwellProgress; drawableSwell.ApplyCustomUpdateState += updateStateTransforms; - clearSample = skin.GetSample(new SampleInfo("spinner-osu")); } private void animateSwellProgress(int numHits) @@ -173,7 +172,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy { if (!samplePlayed) { - clearSample?.Play(); + clearSample.Play(); samplePlayed = true; } From ee6dcbd80899c3865803311b372c8f8623092ade Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 12 Feb 2025 14:12:43 +0100 Subject: [PATCH 080/107] Fix android build again Another month, another freak android build failure. From what I can tell, this time the build is broken because the second- -to-last workaround applied to fix it, namely explicitly specifying the version of workloads to install, stopped working, presumably because github pushed a new .NET SDK version to runners, and microsoft didn't actually put up a set of workload packages whose version matches the .NET SDK version 1:1. Thankfully, the fix to the *last* android build breakage (which caused the workload installation to completely and irrecoverably fail), appears to be rolling out this week, and *also* fix that same second-last issue, so both workarounds of specifying the workload version and pinning the image to `windows-2019` appear to no longer be required. Note that the newest image version, 20250209.1.0, is still not fully rolled out yet, thus rather than just fix all builds, this will fix like 20% of builds until the newest image is fully rolled out. But I guess a 20% passing build is better than a 0% passing build, in a sense...? --- .github/workflows/ci.yml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a88f1320cd..d75f09f184 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -96,7 +96,7 @@ jobs: build-only-android: name: Build only (Android) - runs-on: windows-2019 + runs-on: windows-latest timeout-minutes: 60 steps: - name: Checkout @@ -114,10 +114,7 @@ jobs: dotnet-version: "8.0.x" - name: Install .NET workloads - # since windows image 20241113.3.0, not specifying a version here - # installs the .NET 7 version of android workload for very unknown reasons. - # revisit once we upgrade to .NET 9, it's probably fixed there. - run: dotnet workload install android --version (dotnet --version) + run: dotnet workload install android - name: Compile run: dotnet build -c Debug osu.Android.slnf From 550d21df42a11202b932194e6e40bd90e384b2e3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Feb 2025 00:21:08 +0900 Subject: [PATCH 081/107] Fix failing tests due to text change --- .../Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs index 36f5bba384..37a3cc2faf 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs @@ -266,7 +266,7 @@ namespace osu.Game.Tests.Visual.Multiplayer private void assertQueueTabCount(int count) { - string queueTabText = count > 0 ? $"Queue ({count})" : "Queue"; + string queueTabText = count > 0 ? $"Up next ({count})" : "Up next"; AddUntilStep($"Queue tab shows \"{queueTabText}\"", () => { return this.ChildrenOfType.OsuTabItem>() From 7d6701f8e9383f1a1790103f8b29d598fdc13bb7 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 13 Feb 2025 01:20:42 +0900 Subject: [PATCH 082/107] Attempt to fix intermittent collections test --- .../Visual/Collections/TestSceneManageCollectionsDialog.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs b/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs index 0f2f716a07..60675018e9 100644 --- a/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs +++ b/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs @@ -376,6 +376,6 @@ namespace osu.Game.Tests.Visual.Collections private void assertCollectionName(int index, string name) => AddUntilStep($"item {index + 1} has correct name", - () => dialog.ChildrenOfType().Single().OrderedItems.ElementAt(index).ChildrenOfType().First().Text == name); + () => dialog.ChildrenOfType().Single().OrderedItems.ElementAtOrDefault(index)?.ChildrenOfType().First().Text == name); } } From 315a480931e256c8e79a7193c54dad451e75cd94 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Feb 2025 00:03:30 +0900 Subject: [PATCH 083/107] Disallow focus on difficulty range slider Alternative to https://github.com/ppy/osu/pull/31749. Closes https://github.com/ppy/osu/issues/31559. --- osu.Game/Graphics/UserInterface/RangeSlider.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Graphics/UserInterface/RangeSlider.cs b/osu.Game/Graphics/UserInterface/RangeSlider.cs index 422c2ca4a3..acf10ce827 100644 --- a/osu.Game/Graphics/UserInterface/RangeSlider.cs +++ b/osu.Game/Graphics/UserInterface/RangeSlider.cs @@ -162,6 +162,8 @@ namespace osu.Game.Graphics.UserInterface protected partial class BoundSlider : RoundedSliderBar { + public override bool AcceptsFocus => false; + public new Nub Nub => base.Nub; public string? DefaultString; From 965038598975043dc148bf14b14a3adf6b688eb6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Feb 2025 00:06:20 +0900 Subject: [PATCH 084/107] Also disable sliderbar focus when disabled --- osu.Game/Graphics/UserInterface/OsuSliderBar.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Graphics/UserInterface/OsuSliderBar.cs b/osu.Game/Graphics/UserInterface/OsuSliderBar.cs index 334fe343ae..4b52ac4a3a 100644 --- a/osu.Game/Graphics/UserInterface/OsuSliderBar.cs +++ b/osu.Game/Graphics/UserInterface/OsuSliderBar.cs @@ -18,6 +18,8 @@ namespace osu.Game.Graphics.UserInterface public abstract partial class OsuSliderBar : SliderBar, IHasTooltip where T : struct, INumber, IMinMaxValue { + public override bool AcceptsFocus => !Current.Disabled; + public bool PlaySamplesOnAdjust { get; set; } = true; /// From 601e6d8a70e953b59f0066fbe6de75ed16091c09 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Feb 2025 13:53:42 +0900 Subject: [PATCH 085/107] Refactor pass for code quality --- .../AddPlaylistToCollectionButton.cs | 57 ++++++++++++------- 1 file changed, 35 insertions(+), 22 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs index 8b5d5c752c..d4b89a5b28 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs @@ -2,10 +2,11 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Collections; using osu.Game.Database; @@ -17,7 +18,7 @@ using Realms; namespace osu.Game.Screens.OnlinePlay.Playlists { - public partial class AddPlaylistToCollectionButton : RoundedButton + public partial class AddPlaylistToCollectionButton : RoundedButton, IHasTooltip { private readonly Room room; private readonly Bindable downloadedBeatmapsCount = new Bindable(0); @@ -34,7 +35,6 @@ namespace osu.Game.Screens.OnlinePlay.Playlists public AddPlaylistToCollectionButton(Room room) { this.room = room; - Text = formatButtonText(downloadedBeatmapsCount.Value, collectionExists.Value); } [BackgroundDependencyLoader] @@ -43,31 +43,31 @@ namespace osu.Game.Screens.OnlinePlay.Playlists Action = () => { if (room.Playlist.Count == 0) - { - notifications?.Post(new SimpleErrorNotification { Text = "Cannot add local beatmaps" }); return; - } - var beatmaps = realmAccess.Realm.All().Filter(formatFilterQuery(room.Playlist)).ToList(); + var beatmaps = getBeatmapsForPlaylist(realmAccess.Realm).ToArray(); var collection = realmAccess.Realm.All().FirstOrDefault(c => c.Name == room.Name); if (collection == null) { - collection = new BeatmapCollection(room.Name, beatmaps.Select(i => i.MD5Hash).Distinct().ToList()); + collection = new BeatmapCollection(room.Name); realmAccess.Realm.Write(() => realmAccess.Realm.Add(collection)); notifications?.Post(new SimpleNotification { Text = $"Created new collection: {room.Name}" }); } else { - collection.ToLive(realmAccess).PerformWrite(c => - { - beatmaps = beatmaps.Where(i => !c.BeatmapMD5Hashes.Contains(i.MD5Hash)).ToList(); - foreach (var item in beatmaps) - c.BeatmapMD5Hashes.Add(item.MD5Hash); - notifications?.Post(new SimpleNotification { Text = $"Updated collection: {room.Name}" }); - }); + notifications?.Post(new SimpleNotification { Text = $"Updated collection: {room.Name}" }); } + + collection.ToLive(realmAccess).PerformWrite(c => + { + foreach (var item in beatmaps) + { + if (!c.BeatmapMD5Hashes.Contains(item.MD5Hash)) + c.BeatmapMD5Hashes.Add(item.MD5Hash); + } + }); }; } @@ -76,13 +76,28 @@ namespace osu.Game.Screens.OnlinePlay.Playlists base.LoadComplete(); if (room.Playlist.Count > 0) - beatmapSubscription = realmAccess.RegisterForNotifications(r => r.All().Filter(formatFilterQuery(room.Playlist)), (sender, _) => downloadedBeatmapsCount.Value = sender.Count); + { + beatmapSubscription = + realmAccess.RegisterForNotifications(getBeatmapsForPlaylist, (sender, _) => downloadedBeatmapsCount.Value = sender.Count); + } - collectionSubscription = realmAccess.RegisterForNotifications(r => r.All().Where(c => c.Name == room.Name), (sender, _) => collectionExists.Value = sender.Count > 0); + collectionSubscription = realmAccess.RegisterForNotifications(r => r.All().Where(c => c.Name == room.Name), (sender, _) => collectionExists.Value = sender.Any()); - downloadedBeatmapsCount.BindValueChanged(_ => Text = formatButtonText(downloadedBeatmapsCount.Value, collectionExists.Value)); + downloadedBeatmapsCount.BindValueChanged(_ => updateButtonText()); + collectionExists.BindValueChanged(_ => updateButtonText(), true); + } - collectionExists.BindValueChanged(_ => Text = formatButtonText(downloadedBeatmapsCount.Value, collectionExists.Value), true); + private IQueryable getBeatmapsForPlaylist(Realm r) + { + return r.All().Filter(string.Join(" OR ", room.Playlist.Select(item => $"(OnlineID == {item.Beatmap.OnlineID})").Distinct())); + } + + private void updateButtonText() + { + if (!collectionExists.Value) + Text = $"Create new collection with {downloadedBeatmapsCount.Value} beatmaps"; + else + Text = $"Update collection with {downloadedBeatmapsCount.Value} beatmaps"; } protected override void Dispose(bool isDisposing) @@ -93,8 +108,6 @@ namespace osu.Game.Screens.OnlinePlay.Playlists collectionSubscription?.Dispose(); } - private string formatFilterQuery(IReadOnlyList playlistItems) => string.Join(" OR ", playlistItems.Select(item => $"(OnlineID == {item.Beatmap.OnlineID})").Distinct()); - - private string formatButtonText(int count, bool collectionExists) => $"Add {count} {(count == 1 ? "beatmap" : "beatmaps")} to {(collectionExists ? "collection" : "new collection")}"; + public LocalisableString TooltipText => "Only downloaded beatmaps will be added to the collection"; } } From 8561df40c52bc60a16335e77b6024ae6d50c6984 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Feb 2025 14:30:33 +0900 Subject: [PATCH 086/107] Add better messaging and handling of edge cases --- .../AddPlaylistToCollectionButton.cs | 110 ++++++++++++------ 1 file changed, 77 insertions(+), 33 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs index d4b89a5b28..595e9ad15c 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs @@ -2,9 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics.Cursor; using osu.Framework.Localisation; using osu.Game.Beatmaps; @@ -21,13 +21,15 @@ namespace osu.Game.Screens.OnlinePlay.Playlists public partial class AddPlaylistToCollectionButton : RoundedButton, IHasTooltip { private readonly Room room; - private readonly Bindable downloadedBeatmapsCount = new Bindable(0); - private readonly Bindable collectionExists = new Bindable(false); + private IDisposable? beatmapSubscription; private IDisposable? collectionSubscription; + private Live? collection; + private HashSet localBeatmapHashes = new HashSet(); + [Resolved] - private RealmAccess realmAccess { get; set; } = null!; + private RealmAccess realm { get; set; } = null!; [Resolved(canBeNull: true)] private INotificationOverlay? notifications { get; set; } @@ -45,29 +47,29 @@ namespace osu.Game.Screens.OnlinePlay.Playlists if (room.Playlist.Count == 0) return; - var beatmaps = getBeatmapsForPlaylist(realmAccess.Realm).ToArray(); + var beatmaps = getBeatmapsForPlaylist(realm.Realm).ToArray(); - var collection = realmAccess.Realm.All().FirstOrDefault(c => c.Name == room.Name); + int countBefore = 0; + int countAfter = 0; - if (collection == null) + collection ??= realm.Realm.Write(() => realm.Realm.Add(new BeatmapCollection(room.Name)).ToLive(realm)); + collection.PerformWrite(c => { - collection = new BeatmapCollection(room.Name); - realmAccess.Realm.Write(() => realmAccess.Realm.Add(collection)); - notifications?.Post(new SimpleNotification { Text = $"Created new collection: {room.Name}" }); - } - else - { - notifications?.Post(new SimpleNotification { Text = $"Updated collection: {room.Name}" }); - } + countBefore = c.BeatmapMD5Hashes.Count; - collection.ToLive(realmAccess).PerformWrite(c => - { foreach (var item in beatmaps) { if (!c.BeatmapMD5Hashes.Contains(item.MD5Hash)) c.BeatmapMD5Hashes.Add(item.MD5Hash); } + + countAfter = c.BeatmapMD5Hashes.Count; }); + + if (countBefore == 0) + notifications?.Post(new SimpleNotification { Text = $"Created new collection \"{room.Name}\" with {countAfter} beatmaps." }); + else + notifications?.Post(new SimpleNotification { Text = $"Added {countAfter - countBefore} beatmaps to collection \"{room.Name}\"." }); }; } @@ -75,16 +77,53 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { base.LoadComplete(); - if (room.Playlist.Count > 0) + Enabled.Value = false; + + if (room.Playlist.Count == 0) + return; + + beatmapSubscription = realm.RegisterForNotifications(getBeatmapsForPlaylist, (sender, _) => { - beatmapSubscription = - realmAccess.RegisterForNotifications(getBeatmapsForPlaylist, (sender, _) => downloadedBeatmapsCount.Value = sender.Count); - } + localBeatmapHashes = sender.Select(b => b.MD5Hash).ToHashSet(); + Schedule(updateButtonState); + }); - collectionSubscription = realmAccess.RegisterForNotifications(r => r.All().Where(c => c.Name == room.Name), (sender, _) => collectionExists.Value = sender.Any()); + collectionSubscription = realm.RegisterForNotifications(r => r.All().Where(c => c.Name == room.Name), (sender, _) => + { + collection = sender.FirstOrDefault()?.ToLive(realm); + Schedule(updateButtonState); + }); + } - downloadedBeatmapsCount.BindValueChanged(_ => updateButtonText()); - collectionExists.BindValueChanged(_ => updateButtonText(), true); + private void updateButtonState() + { + int countToAdd = getCountToBeAdded(); + + if (collection == null) + Text = $"Create new collection with {countToAdd} beatmaps"; + else + Text = $"Update collection with {countToAdd} beatmaps"; + + Enabled.Value = countToAdd > 0; + } + + private int getCountToBeAdded() + { + if (collection == null) + return localBeatmapHashes.Count; + + return collection.PerformRead(c => + { + int count = localBeatmapHashes.Count; + + foreach (string hash in localBeatmapHashes) + { + if (c.BeatmapMD5Hashes.Contains(hash)) + count--; + } + + return count; + }); } private IQueryable getBeatmapsForPlaylist(Realm r) @@ -92,14 +131,6 @@ namespace osu.Game.Screens.OnlinePlay.Playlists return r.All().Filter(string.Join(" OR ", room.Playlist.Select(item => $"(OnlineID == {item.Beatmap.OnlineID})").Distinct())); } - private void updateButtonText() - { - if (!collectionExists.Value) - Text = $"Create new collection with {downloadedBeatmapsCount.Value} beatmaps"; - else - Text = $"Update collection with {downloadedBeatmapsCount.Value} beatmaps"; - } - protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); @@ -108,6 +139,19 @@ namespace osu.Game.Screens.OnlinePlay.Playlists collectionSubscription?.Dispose(); } - public LocalisableString TooltipText => "Only downloaded beatmaps will be added to the collection"; + public LocalisableString TooltipText + { + get + { + if (Enabled.Value) + return string.Empty; + + int currentCollectionCount = collection?.PerformRead(c => c.BeatmapMD5Hashes.Count) ?? 0; + if (room.Playlist.DistinctBy(i => i.Beatmap.OnlineID).Count() == currentCollectionCount) + return "All beatmaps have been added!"; + + return "Download some beatmaps first."; + } + } } } From f9b7a8ed103e39fbd5a791699e5c99b366736766 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Feb 2025 14:49:25 +0900 Subject: [PATCH 087/107] Make realm operation asynchronous for good measure --- .../AddPlaylistToCollectionButton.cs | 49 +++++++++++++------ 1 file changed, 35 insertions(+), 14 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs index 595e9ad15c..741173f9a3 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs @@ -47,14 +47,18 @@ namespace osu.Game.Screens.OnlinePlay.Playlists if (room.Playlist.Count == 0) return; - var beatmaps = getBeatmapsForPlaylist(realm.Realm).ToArray(); - int countBefore = 0; int countAfter = 0; - collection ??= realm.Realm.Write(() => realm.Realm.Add(new BeatmapCollection(room.Name)).ToLive(realm)); - collection.PerformWrite(c => + Text = "Updating collection..."; + Enabled.Value = false; + + realm.WriteAsync(r => { + var beatmaps = getBeatmapsForPlaylist(r).ToArray(); + var c = getCollectionsForPlaylist(r).FirstOrDefault() + ?? r.Add(new BeatmapCollection(room.Name)); + countBefore = c.BeatmapMD5Hashes.Count; foreach (var item in beatmaps) @@ -64,12 +68,13 @@ namespace osu.Game.Screens.OnlinePlay.Playlists } countAfter = c.BeatmapMD5Hashes.Count; - }); - - if (countBefore == 0) - notifications?.Post(new SimpleNotification { Text = $"Created new collection \"{room.Name}\" with {countAfter} beatmaps." }); - else - notifications?.Post(new SimpleNotification { Text = $"Added {countAfter - countBefore} beatmaps to collection \"{room.Name}\"." }); + }).ContinueWith(_ => Schedule(() => + { + if (countBefore == 0) + notifications?.Post(new SimpleNotification { Text = $"Created new collection \"{room.Name}\" with {countAfter} beatmaps." }); + else + notifications?.Post(new SimpleNotification { Text = $"Added {countAfter - countBefore} beatmaps to collection \"{room.Name}\"." }); + })); }; } @@ -77,6 +82,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { base.LoadComplete(); + // will be updated via updateButtonState() when ready. Enabled.Value = false; if (room.Playlist.Count == 0) @@ -88,7 +94,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists Schedule(updateButtonState); }); - collectionSubscription = realm.RegisterForNotifications(r => r.All().Where(c => c.Name == room.Name), (sender, _) => + collectionSubscription = realm.RegisterForNotifications(getCollectionsForPlaylist, (sender, _) => { collection = sender.FirstOrDefault()?.ToLive(realm); Schedule(updateButtonState); @@ -101,8 +107,10 @@ namespace osu.Game.Screens.OnlinePlay.Playlists if (collection == null) Text = $"Create new collection with {countToAdd} beatmaps"; + else if (hasAllItemsInCollection) + Text = "Collection complete!"; else - Text = $"Update collection with {countToAdd} beatmaps"; + Text = $"Add {countToAdd} beatmaps to collection"; Enabled.Value = countToAdd > 0; } @@ -126,11 +134,25 @@ namespace osu.Game.Screens.OnlinePlay.Playlists }); } + private IQueryable getCollectionsForPlaylist(Realm r) => r.All().Where(c => c.Name == room.Name); + private IQueryable getBeatmapsForPlaylist(Realm r) { return r.All().Filter(string.Join(" OR ", room.Playlist.Select(item => $"(OnlineID == {item.Beatmap.OnlineID})").Distinct())); } + private bool hasAllItemsInCollection + { + get + { + if (collection == null) + return false; + + return room.Playlist.DistinctBy(i => i.Beatmap.OnlineID).Count() == + collection.PerformRead(c => c.BeatmapMD5Hashes.Count); + } + } + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); @@ -146,8 +168,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists if (Enabled.Value) return string.Empty; - int currentCollectionCount = collection?.PerformRead(c => c.BeatmapMD5Hashes.Count) ?? 0; - if (room.Playlist.DistinctBy(i => i.Beatmap.OnlineID).Count() == currentCollectionCount) + if (hasAllItemsInCollection) return "All beatmaps have been added!"; return "Download some beatmaps first."; From 8ce28d56bbe245eed781e0055ea0befd72533f8e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Feb 2025 14:58:04 +0900 Subject: [PATCH 088/107] Fix tests not waiting enough --- .../Playlists/TestSceneAddPlaylistToCollectionButton.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Playlists/TestSceneAddPlaylistToCollectionButton.cs b/osu.Game.Tests/Visual/Playlists/TestSceneAddPlaylistToCollectionButton.cs index f18488170d..46c93d9ae2 100644 --- a/osu.Game.Tests/Visual/Playlists/TestSceneAddPlaylistToCollectionButton.cs +++ b/osu.Game.Tests/Visual/Playlists/TestSceneAddPlaylistToCollectionButton.cs @@ -77,9 +77,9 @@ namespace osu.Game.Tests.Visual.Playlists AddStep("click button", () => InputManager.Click(MouseButton.Left)); - AddAssert("notification shown", () => notificationOverlay.AllNotifications.FirstOrDefault(n => n.Text.ToString().StartsWith("Created", StringComparison.Ordinal)) != null); + AddUntilStep("notification shown", () => notificationOverlay.AllNotifications.Any(n => n.Text.ToString().StartsWith("Created new collection", StringComparison.Ordinal))); - AddAssert("realm is updated", () => Realm.Realm.All().FirstOrDefault(c => c.Name == room.Name) != null); + AddUntilStep("realm is updated", () => Realm.Realm.All().FirstOrDefault(c => c.Name == room.Name) != null); } private void importBeatmap() => AddStep("import beatmap", () => From 3f3cb3df2a5b12ae2fb9cfa8b3db1daa076f9c44 Mon Sep 17 00:00:00 2001 From: Rudi Herouard Date: Mon, 20 Jan 2025 16:35:21 +0100 Subject: [PATCH 089/107] Fix toolbox settings hiding when dragging a slider --- osu.Game/Overlays/SettingsToolboxGroup.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/SettingsToolboxGroup.cs b/osu.Game/Overlays/SettingsToolboxGroup.cs index f8cf218564..cf72125007 100644 --- a/osu.Game/Overlays/SettingsToolboxGroup.cs +++ b/osu.Game/Overlays/SettingsToolboxGroup.cs @@ -8,6 +8,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; +using osu.Framework.Input; using osu.Framework.Input.Events; using osu.Framework.Layout; using osu.Game.Graphics; @@ -54,6 +55,8 @@ namespace osu.Game.Overlays private IconButton expandButton = null!; + private InputManager inputManager = null!; + /// /// Create a new instance. /// @@ -125,6 +128,8 @@ namespace osu.Game.Overlays { base.LoadComplete(); + inputManager = GetContainingInputManager()!; + Expanded.BindValueChanged(_ => updateExpandedState(true)); updateExpandedState(false); @@ -172,7 +177,9 @@ namespace osu.Game.Overlays // potentially continuing to get processed while content has changed to autosize. content.ClearTransforms(); - if (Expanded.Value || IsHovered) + bool sliderDraggedInHimself = inputManager.DraggedDrawable.IsRootedAt(this); + + if (Expanded.Value || IsHovered || sliderDraggedInHimself) { content.AutoSizeAxes = Axes.Y; content.AutoSizeDuration = animate ? transition_duration : 0; From 9456e376f370b2ea0260a781fd6f90e1e87ad106 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Feb 2025 15:15:05 +0900 Subject: [PATCH 090/107] Fix expanded state not updating on drag end --- osu.Game/Overlays/SettingsToolboxGroup.cs | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/SettingsToolboxGroup.cs b/osu.Game/Overlays/SettingsToolboxGroup.cs index cf72125007..dd41f156f3 100644 --- a/osu.Game/Overlays/SettingsToolboxGroup.cs +++ b/osu.Game/Overlays/SettingsToolboxGroup.cs @@ -57,6 +57,8 @@ namespace osu.Game.Overlays private InputManager inputManager = null!; + private Drawable? draggedChild; + /// /// Create a new instance. /// @@ -161,6 +163,13 @@ namespace osu.Game.Overlays headerText.FadeTo(headerText.DrawWidth < DrawWidth ? 1 : 0, 150, Easing.OutQuint); headerTextVisibilityCache.Validate(); } + + // Dragged child finished its drag operation. + if (draggedChild != null && inputManager.DraggedDrawable != draggedChild) + { + draggedChild = null; + updateExpandedState(true); + } } protected override bool OnInvalidate(Invalidation invalidation, InvalidationSource source) @@ -173,13 +182,17 @@ namespace osu.Game.Overlays private void updateExpandedState(bool animate) { + // before we collapse down, let's double check the user is not dragging a UI control contained within us. + if (inputManager.DraggedDrawable.IsRootedAt(this)) + { + draggedChild = inputManager.DraggedDrawable; + } + // clearing transforms is necessary to avoid a previous height transform // potentially continuing to get processed while content has changed to autosize. content.ClearTransforms(); - bool sliderDraggedInHimself = inputManager.DraggedDrawable.IsRootedAt(this); - - if (Expanded.Value || IsHovered || sliderDraggedInHimself) + if (Expanded.Value || IsHovered || draggedChild != null) { content.AutoSizeAxes = Axes.Y; content.AutoSizeDuration = animate ? transition_duration : 0; From 88188e8fcb4b15d0214d7106810f10b1f5c66fbe Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Feb 2025 16:00:19 +0900 Subject: [PATCH 091/107] Add API models for teams --- .../Online/API/Requests/Responses/APITeam.cs | 23 +++++++++++++++++++ .../Online/API/Requests/Responses/APIUser.cs | 4 ++++ 2 files changed, 27 insertions(+) create mode 100644 osu.Game/Online/API/Requests/Responses/APITeam.cs diff --git a/osu.Game/Online/API/Requests/Responses/APITeam.cs b/osu.Game/Online/API/Requests/Responses/APITeam.cs new file mode 100644 index 0000000000..b4fcc2d26e --- /dev/null +++ b/osu.Game/Online/API/Requests/Responses/APITeam.cs @@ -0,0 +1,23 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Newtonsoft.Json; + +namespace osu.Game.Online.API.Requests.Responses +{ + [JsonObject(MemberSerialization.OptIn)] + public class APITeam + { + [JsonProperty(@"id")] + public int Id { get; set; } = 1; + + [JsonProperty(@"name")] + public string Name { get; set; } = string.Empty; + + [JsonProperty(@"short_name")] + public string ShortName { get; set; } = string.Empty; + + [JsonProperty(@"flag_url")] + public string FlagUrl = string.Empty; + } +} diff --git a/osu.Game/Online/API/Requests/Responses/APIUser.cs b/osu.Game/Online/API/Requests/Responses/APIUser.cs index 30fceab852..92b7d9d874 100644 --- a/osu.Game/Online/API/Requests/Responses/APIUser.cs +++ b/osu.Game/Online/API/Requests/Responses/APIUser.cs @@ -55,6 +55,10 @@ namespace osu.Game.Online.API.Requests.Responses set => countryCodeString = value.ToString(); } + [JsonProperty(@"team")] + [CanBeNull] + public APITeam Team { get; set; } + [JsonProperty(@"profile_colour")] public string Colour; From 303961d1015f2e32549680a76fa2b68112236166 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Feb 2025 16:19:55 +0900 Subject: [PATCH 092/107] Add drawable implementations of team logo --- .../Online/Leaderboards/LeaderboardScore.cs | 6 ++ .../Overlays/BeatmapSet/Scores/ScoreTable.cs | 15 +++- .../BeatmapSet/Scores/TopScoreUserSection.cs | 27 +++++- .../Profile/Header/TopHeaderContainer.cs | 6 ++ .../Participants/ParticipantPanel.cs | 6 ++ .../Leaderboards/LeaderboardScoreV2.cs | 6 ++ .../OnlinePlay/TestRoomRequestsHandler.cs | 11 ++- .../Users/Drawables/UpdateableTeamFlag.cs | 86 +++++++++++++++++++ osu.Game/Users/UserGridPanel.cs | 3 +- osu.Game/Users/UserPanel.cs | 5 ++ osu.Game/Users/UserRankPanel.cs | 3 +- 11 files changed, 167 insertions(+), 7 deletions(-) create mode 100644 osu.Game/Users/Drawables/UpdateableTeamFlag.cs diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index 52074119b8..11e1710e75 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -199,6 +199,12 @@ namespace osu.Game.Online.Leaderboards Origin = Anchor.CentreLeft, Size = new Vector2(28, 20), }, + new UpdateableTeamFlag(user.Team) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Size = new Vector2(40, 20), + }, new DateLabel(Score.Date) { Anchor = Anchor.CentreLeft, diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs index c70c41feed..be6ad49150 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs @@ -160,7 +160,20 @@ namespace osu.Game.Overlays.BeatmapSet.Scores { Size = new Vector2(19, 14), }, - username, + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(4), + Children = new Drawable[] + { + new UpdateableTeamFlag(score.User.Team) + { + Size = new Vector2(28, 14), + }, + username, + } + }, #pragma warning disable 618 new StatisticText(score.MaxCombo, score.BeatmapInfo!.MaxCombo, @"0\x"), #pragma warning restore 618 diff --git a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreUserSection.cs b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreUserSection.cs index 13ba9fb74b..14c9bedc67 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreUserSection.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreUserSection.cs @@ -27,7 +27,9 @@ namespace osu.Game.Overlays.BeatmapSet.Scores private readonly UpdateableAvatar avatar; private readonly LinkFlowContainer usernameText; private readonly DrawableDate achievedOn; + private readonly UpdateableFlag flag; + private readonly UpdateableTeamFlag teamFlag; public TopScoreUserSection() { @@ -112,12 +114,30 @@ namespace osu.Game.Overlays.BeatmapSet.Scores }, } }, - flag = new UpdateableFlag + new FillFlowContainer { + AutoSizeAxes = Axes.Both, Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Size = new Vector2(19, 14), - Margin = new MarginPadding { Top = 3 }, // makes spacing look more even + Direction = FillDirection.Horizontal, + Spacing = new Vector2(4), + Children = new Drawable[] + { + flag = new UpdateableFlag + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Size = new Vector2(19, 14), + Margin = new MarginPadding { Top = 3 }, // makes spacing look more even + }, + teamFlag = new UpdateableTeamFlag + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Size = new Vector2(28, 14), + Margin = new MarginPadding { Top = 3 }, // makes spacing look more even + }, + } }, } } @@ -139,6 +159,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores { avatar.User = value.User; flag.CountryCode = value.User.CountryCode; + teamFlag.Team = value.User.Team; achievedOn.Date = value.Date; usernameText.Clear(); diff --git a/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs index ba2cd5b705..5f404375e6 100644 --- a/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs +++ b/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs @@ -42,6 +42,7 @@ namespace osu.Game.Overlays.Profile.Header private ExternalLinkButton openUserExternally = null!; private OsuSpriteText titleText = null!; private UpdateableFlag userFlag = null!; + private UpdateableTeamFlag teamFlag = null!; private OsuHoverContainer userCountryContainer = null!; private OsuSpriteText userCountryText = null!; private GroupBadgeFlow groupBadgeFlow = null!; @@ -166,6 +167,10 @@ namespace osu.Game.Overlays.Profile.Header { Size = new Vector2(28, 20), }, + teamFlag = new UpdateableTeamFlag + { + Size = new Vector2(40, 20), + }, userCountryContainer = new OsuHoverContainer { AutoSizeAxes = Axes.Both, @@ -215,6 +220,7 @@ namespace osu.Game.Overlays.Profile.Header usernameText.Text = user?.Username ?? string.Empty; openUserExternally.Link = $@"{api.Endpoints.WebsiteUrl}/users/{user?.Id ?? 0}"; userFlag.CountryCode = user?.CountryCode ?? default; + teamFlag.Team = user?.Team; userCountryText.Text = (user?.CountryCode ?? default).GetDescription(); userCountryContainer.Action = () => rankingsOverlay?.ShowCountry(user?.CountryCode ?? default); supporterTag.SupportLevel = user?.SupportLevel ?? 0; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs index 0fa2be44f3..0cedfb9909 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs @@ -140,6 +140,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants Size = new Vector2(28, 20), CountryCode = user?.CountryCode ?? default }, + new UpdateableTeamFlag(user?.Team) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Size = new Vector2(40, 20), + }, new OsuSpriteText { Anchor = Anchor.CentreLeft, diff --git a/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs b/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs index a2253b413c..978d6eca32 100644 --- a/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs +++ b/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs @@ -339,6 +339,12 @@ namespace osu.Game.Screens.SelectV2.Leaderboards Origin = Anchor.CentreLeft, Size = new Vector2(24, 16), }, + new UpdateableTeamFlag(user.Team) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Size = new Vector2(40, 20), + }, new DateLabel(score.Date) { Anchor = Anchor.CentreLeft, diff --git a/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs b/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs index c9149bda22..d73fd5ab22 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; using Newtonsoft.Json; +using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Online.API; @@ -221,7 +222,15 @@ namespace osu.Game.Tests.Visual.OnlinePlay : new APIUser { Id = id, - Username = $"User {id}" + Username = $"User {id}", + Team = RNG.NextBool() + ? new APITeam + { + Name = "Collective Wangs", + ShortName = "WANG", + FlagUrl = "https://assets.ppy.sh/teams/logo/1/wanglogo.jpg", + } + : null, }) .Where(u => u != null).ToList(), }); diff --git a/osu.Game/Users/Drawables/UpdateableTeamFlag.cs b/osu.Game/Users/Drawables/UpdateableTeamFlag.cs new file mode 100644 index 0000000000..486cb697a1 --- /dev/null +++ b/osu.Game/Users/Drawables/UpdateableTeamFlag.cs @@ -0,0 +1,86 @@ +// 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.Sprites; +using osu.Framework.Graphics.Textures; +using osu.Framework.Localisation; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API.Requests.Responses; + +namespace osu.Game.Users.Drawables +{ + /// + /// A team logo which can update to a new team when needed. + /// + public partial class UpdateableTeamFlag : ModelBackedDrawable + { + public APITeam? Team + { + get => Model; + set => Model = value; + } + + protected override double LoadDelay => 200; + + public UpdateableTeamFlag(APITeam? team = null) + { + Team = team; + + Masking = true; + } + + protected override Drawable? CreateDrawable(APITeam? team) + { + if (team == null) + return Empty(); + + return new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new TeamFlag(team) + { + RelativeSizeAxes = Axes.Both + }, + new HoverClickSounds() + } + }; + } + + // 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; + + protected override void Update() + { + base.Update(); + + CornerRadius = DrawHeight / 8; + } + + public partial class TeamFlag : Sprite, IHasTooltip + { + private readonly APITeam team; + + public LocalisableString TooltipText { get; } + + public TeamFlag(APITeam team) + { + this.team = team; + TooltipText = team.Name; + } + + [BackgroundDependencyLoader] + private void load(LargeTextureStore textures) + { + if (!string.IsNullOrEmpty(team.Name)) + Texture = textures.Get(team.FlagUrl); + } + } + } +} diff --git a/osu.Game/Users/UserGridPanel.cs b/osu.Game/Users/UserGridPanel.cs index fce543415d..f62c9ab4e7 100644 --- a/osu.Game/Users/UserGridPanel.cs +++ b/osu.Game/Users/UserGridPanel.cs @@ -82,9 +82,10 @@ namespace osu.Game.Users AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, Spacing = new Vector2(6), - Children = new Drawable[] + Children = new[] { CreateFlag(), + CreateTeamLogo(), // supporter icon is being added later } } diff --git a/osu.Game/Users/UserPanel.cs b/osu.Game/Users/UserPanel.cs index 0d3ea52611..09a5cb414f 100644 --- a/osu.Game/Users/UserPanel.cs +++ b/osu.Game/Users/UserPanel.cs @@ -130,6 +130,11 @@ namespace osu.Game.Users Action = Action, }; + protected Drawable CreateTeamLogo() => new UpdateableTeamFlag(User.Team) + { + Size = new Vector2(52, 26), + }; + public MenuItem[] ContextMenuItems { get diff --git a/osu.Game/Users/UserRankPanel.cs b/osu.Game/Users/UserRankPanel.cs index 5e3ae172be..ff8adf055c 100644 --- a/osu.Game/Users/UserRankPanel.cs +++ b/osu.Game/Users/UserRankPanel.cs @@ -147,9 +147,10 @@ namespace osu.Game.Users AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, Spacing = new Vector2(6), - Children = new Drawable[] + Children = new[] { CreateFlag(), + CreateTeamLogo(), // supporter icon is being added later } } From 44faabddcd79b0ada819d03cb10044b377e5fe89 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Feb 2025 16:41:59 +0900 Subject: [PATCH 093/107] Add skinnable team flag --- osu.Game/Screens/Play/HUD/PlayerTeamFlag.cs | 56 +++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 osu.Game/Screens/Play/HUD/PlayerTeamFlag.cs diff --git a/osu.Game/Screens/Play/HUD/PlayerTeamFlag.cs b/osu.Game/Screens/Play/HUD/PlayerTeamFlag.cs new file mode 100644 index 0000000000..f8ef03c58c --- /dev/null +++ b/osu.Game/Screens/Play/HUD/PlayerTeamFlag.cs @@ -0,0 +1,56 @@ +// 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.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Skinning; +using osu.Game.Users.Drawables; +using osuTK; + +namespace osu.Game.Screens.Play.HUD +{ + public partial class PlayerTeamFlag : CompositeDrawable, ISerialisableDrawable + { + protected override bool ReceivePositionalInputAtSubTree(Vector2 screenSpacePos) => false; + + private readonly UpdateableTeamFlag flag; + + private const float default_size = 40f; + + [Resolved] + private GameplayState? gameplayState { get; set; } + + [Resolved] + private IAPIProvider api { get; set; } = null!; + + private IBindable? apiUser; + + public PlayerTeamFlag() + { + Size = new Vector2(default_size, default_size / 2f); + + InternalChild = flag = new UpdateableTeamFlag + { + RelativeSizeAxes = Axes.Both, + }; + } + + [BackgroundDependencyLoader] + private void load() + { + if (gameplayState != null) + flag.Team = gameplayState.Score.ScoreInfo.User.Team; + else + { + apiUser = api.LocalUser.GetBoundCopy(); + apiUser.BindValueChanged(u => flag.Team = u.NewValue.Team, true); + } + } + + public bool UsesFixedAnchor { get; set; } + } +} From 4184dd27180b3ae8407c8d06d86894950e8b1b67 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Feb 2025 17:18:25 +0900 Subject: [PATCH 094/107] Give more breathing room in leaderboard scores --- .../Online/Leaderboards/LeaderboardScore.cs | 46 ++++++++++++------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index 11e1710e75..0181c28218 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -189,7 +189,7 @@ namespace osu.Game.Online.Leaderboards RelativeSizeAxes = Axes.Y, Direction = FillDirection.Horizontal, Spacing = new Vector2(5f, 0f), - Width = 87f, + Width = 114f, Masking = true, Children = new Drawable[] { @@ -212,15 +212,6 @@ namespace osu.Game.Online.Leaderboards }, }, }, - new FillFlowContainer - { - Origin = Anchor.CentreLeft, - Anchor = Anchor.CentreLeft, - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Margin = new MarginPadding { Left = edge_margin }, - Children = statisticsLabels - }, }, }, }, @@ -240,6 +231,7 @@ namespace osu.Game.Online.Leaderboards GlowColour = Color4Extensions.FromHex(@"83ccfa"), Current = scoreManager.GetBindableTotalScoreString(Score), Font = OsuFont.Numeric.With(size: 23), + Margin = new MarginPadding { Top = 1 }, }, RankContainer = new Container { @@ -256,13 +248,32 @@ namespace osu.Game.Online.Leaderboards }, }, }, - modsContainer = new FillFlowContainer + new FillFlowContainer { + AutoSizeAxes = Axes.Both, Anchor = Anchor.BottomRight, Origin = Anchor.BottomRight, - AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, - ChildrenEnumerable = Score.Mods.AsOrdered().Select(mod => new ModIcon(mod) { Scale = new Vector2(0.375f) }) + Children = new Drawable[] + { + new FillFlowContainer + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Margin = new MarginPadding { Left = edge_margin }, + Children = statisticsLabels + }, + modsContainer = new FillFlowContainer + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + ChildrenEnumerable = Score.Mods.AsOrdered().Select(mod => new ModIcon(mod) { Scale = new Vector2(0.34f) }) + }, + } }, }, }, @@ -330,7 +341,7 @@ namespace osu.Game.Online.Leaderboards private partial class ScoreComponentLabel : Container, IHasTooltip { - private const float icon_size = 20; + private const float icon_size = 16; private readonly FillFlowContainer content; public override bool Contains(Vector2 screenSpacePos) => content.Contains(screenSpacePos); @@ -346,7 +357,7 @@ namespace osu.Game.Online.Leaderboards { AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, - Padding = new MarginPadding { Right = 10 }, + Padding = new MarginPadding { Right = 5 }, Children = new Drawable[] { new Container @@ -381,7 +392,8 @@ namespace osu.Game.Online.Leaderboards Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, Text = statistic.Value, - Font = OsuFont.GetFont(size: 17, weight: FontWeight.Bold, fixedWidth: true) + Spacing = new Vector2(-1, 0), + Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold, fixedWidth: true) }, }, }; @@ -412,7 +424,7 @@ namespace osu.Game.Online.Leaderboards public DateLabel(DateTimeOffset date) : base(date) { - Font = OsuFont.GetFont(size: 17, weight: FontWeight.Bold, italics: true); + Font = OsuFont.GetFont(size: 13, weight: FontWeight.Bold, italics: true); } protected override string Format() => Date.ToShortRelativeTime(TimeSpan.FromSeconds(30)); From 4e043e7cabc242b051275e84b17b88553c28844b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Feb 2025 18:35:27 +0900 Subject: [PATCH 095/107] Change how values are applied to (hopefully) simplify things --- osu.Game/Graphics/Containers/ScalingContainer.cs | 3 ++- osu.Game/OsuGame.cs | 6 ++++-- osu.iOS/OsuGameIOS.cs | 3 ++- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/osu.Game/Graphics/Containers/ScalingContainer.cs b/osu.Game/Graphics/Containers/ScalingContainer.cs index ac76c0546b..2a5ce23b64 100644 --- a/osu.Game/Graphics/Containers/ScalingContainer.cs +++ b/osu.Game/Graphics/Containers/ScalingContainer.cs @@ -116,7 +116,8 @@ namespace osu.Game.Graphics.Containers protected override void Update() { - TargetDrawSize = new Vector2(1024, 1024 / (game?.BaseAspectRatio ?? 1f)); + if (game != null) + TargetDrawSize = game.ScalingContainerTargetDrawSize; Scale = new Vector2(CurrentScale); Size = new Vector2(1 / CurrentScale); diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index ecc71822af..d379392a7d 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -72,6 +72,7 @@ using osu.Game.Skinning; using osu.Game.Updater; using osu.Game.Users; using osu.Game.Utils; +using osuTK; using osuTK.Graphics; using Sentry; @@ -814,9 +815,10 @@ namespace osu.Game protected virtual UpdateManager CreateUpdateManager() => new UpdateManager(); /// - /// The base aspect ratio to use in all s. + /// Adjust the globally applied in every . + /// Useful for changing how the game handles different aspect ratios. /// - protected internal virtual float BaseAspectRatio => 4f / 3f; + protected internal virtual Vector2 ScalingContainerTargetDrawSize { get; } = new Vector2(1024, 768); protected override Container CreateScalingContainer() => new ScalingContainer(ScalingMode.Everything); diff --git a/osu.iOS/OsuGameIOS.cs b/osu.iOS/OsuGameIOS.cs index 64b2292d62..883e89e38a 100644 --- a/osu.iOS/OsuGameIOS.cs +++ b/osu.iOS/OsuGameIOS.cs @@ -11,6 +11,7 @@ using osu.Game; using osu.Game.Screens; using osu.Game.Updater; using osu.Game.Utils; +using osuTK; using UIKit; namespace osu.iOS @@ -22,7 +23,7 @@ namespace osu.iOS public override bool HideUnlicensedContent => true; - protected override float BaseAspectRatio => (float)(UIScreen.MainScreen.Bounds.Width / UIScreen.MainScreen.Bounds.Height); + protected override Vector2 ScalingContainerTargetDrawSize => new Vector2(1024, 1024 * DrawHeight / DrawWidth); public OsuGameIOS(AppDelegate appDelegate) { From 248bf43ec9c84d2ea31eb0c51cf814760d79e035 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Feb 2025 18:35:43 +0900 Subject: [PATCH 096/107] Apply nullability to `ScalingContainer` --- .../Graphics/Containers/ScalingContainer.cs | 33 +++++++++---------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/osu.Game/Graphics/Containers/ScalingContainer.cs b/osu.Game/Graphics/Containers/ScalingContainer.cs index 2a5ce23b64..9d2a1c16af 100644 --- a/osu.Game/Graphics/Containers/ScalingContainer.cs +++ b/osu.Game/Graphics/Containers/ScalingContainer.cs @@ -1,9 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - -using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -27,17 +24,17 @@ namespace osu.Game.Graphics.Containers { internal const float TRANSITION_DURATION = 500; - private Bindable sizeX; - private Bindable sizeY; - private Bindable posX; - private Bindable posY; - private Bindable applySafeAreaPadding; + private Bindable sizeX = null!; + private Bindable sizeY = null!; + private Bindable posX = null!; + private Bindable posY = null!; + private Bindable applySafeAreaPadding = null!; - private Bindable safeAreaPadding; + private Bindable safeAreaPadding = null!; private readonly ScalingMode? targetMode; - private Bindable scalingMode; + private Bindable scalingMode = null!; private readonly Container content; protected override Container Content => content; @@ -46,9 +43,9 @@ namespace osu.Game.Graphics.Containers private readonly Container sizableContainer; - private BackgroundScreenStack backgroundStack; + private BackgroundScreenStack? backgroundStack; - private Bindable scalingMenuBackgroundDim; + private Bindable scalingMenuBackgroundDim = null!; private RectangleF? customRect; private bool customRectIsRelativePosition; @@ -89,7 +86,8 @@ namespace osu.Game.Graphics.Containers public partial class ScalingDrawSizePreservingFillContainer : DrawSizePreservingFillContainer { private readonly bool applyUIScale; - private Bindable uiScale; + + private Bindable? uiScale; protected float CurrentScale { get; private set; } = 1; @@ -101,8 +99,7 @@ namespace osu.Game.Graphics.Containers } [Resolved(canBeNull: true)] - [CanBeNull] - private OsuGame game { get; set; } + private OsuGame? game { get; set; } [BackgroundDependencyLoader] private void load(OsuConfigManager osuConfig) @@ -240,13 +237,13 @@ namespace osu.Game.Graphics.Containers private partial class SizeableAlwaysInputContainer : Container { [Resolved] - private GameHost host { get; set; } + private GameHost host { get; set; } = null!; [Resolved] - private ISafeArea safeArea { get; set; } + private ISafeArea safeArea { get; set; } = null!; [Resolved] - private OsuConfigManager config { get; set; } + private OsuConfigManager config { get; set; } = null!; private readonly bool confineHostCursor; private readonly LayoutValue cursorRectCache = new LayoutValue(Invalidation.RequiredParentSizeToFit); From 26a2d0394e5d39de630524166691d86a929a501f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Feb 2025 19:04:26 +0900 Subject: [PATCH 097/107] Invalidate drawable on potential presence change --- osu.Game/Users/Drawables/UpdateableTeamFlag.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Users/Drawables/UpdateableTeamFlag.cs b/osu.Game/Users/Drawables/UpdateableTeamFlag.cs index 486cb697a1..1efde2af68 100644 --- a/osu.Game/Users/Drawables/UpdateableTeamFlag.cs +++ b/osu.Game/Users/Drawables/UpdateableTeamFlag.cs @@ -21,7 +21,11 @@ namespace osu.Game.Users.Drawables public APITeam? Team { get => Model; - set => Model = value; + set + { + Model = value; + Invalidate(Invalidation.Presence); + } } protected override double LoadDelay => 200; From 82c16dee60e1e8702d95657d654d36934c083ac2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Feb 2025 19:05:13 +0900 Subject: [PATCH 098/107] Add missing `LongRunningLoad` attribute --- .../Users/Drawables/UpdateableTeamFlag.cs | 27 +++++++++---------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/osu.Game/Users/Drawables/UpdateableTeamFlag.cs b/osu.Game/Users/Drawables/UpdateableTeamFlag.cs index 1efde2af68..9c2bbb7e3e 100644 --- a/osu.Game/Users/Drawables/UpdateableTeamFlag.cs +++ b/osu.Game/Users/Drawables/UpdateableTeamFlag.cs @@ -42,18 +42,7 @@ namespace osu.Game.Users.Drawables if (team == null) return Empty(); - return new Container - { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - new TeamFlag(team) - { - RelativeSizeAxes = Axes.Both - }, - new HoverClickSounds() - } - }; + return new TeamFlag(team) { RelativeSizeAxes = Axes.Both }; } // Generally we just want team flags to disappear if the user doesn't have one. @@ -67,7 +56,8 @@ namespace osu.Game.Users.Drawables CornerRadius = DrawHeight / 8; } - public partial class TeamFlag : Sprite, IHasTooltip + [LongRunningLoad] + public partial class TeamFlag : CompositeDrawable, IHasTooltip { private readonly APITeam team; @@ -82,8 +72,15 @@ namespace osu.Game.Users.Drawables [BackgroundDependencyLoader] private void load(LargeTextureStore textures) { - if (!string.IsNullOrEmpty(team.Name)) - Texture = textures.Get(team.FlagUrl); + InternalChildren = new Drawable[] + { + new HoverClickSounds(), + new Sprite + { + RelativeSizeAxes = Axes.Both, + Texture = textures.Get(team.FlagUrl) + } + }; } } } From b86eeabef08d8eb3d45848939f2ea36a44790cc7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Feb 2025 19:07:02 +0900 Subject: [PATCH 099/107] Fix one more misalignment on leaderboard scores --- osu.Game/Online/Leaderboards/LeaderboardScore.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index 0181c28218..fc30f158f0 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -180,6 +180,7 @@ namespace osu.Game.Online.Leaderboards Height = 28, Direction = FillDirection.Horizontal, Spacing = new Vector2(10f, 0f), + Margin = new MarginPadding { Bottom = -2 }, Children = new Drawable[] { flagBadgeAndDateContainer = new FillFlowContainer From 1b5101ed5e155c19c0a37894ed3c5ea374ec55a5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Feb 2025 19:30:23 +0900 Subject: [PATCH 100/107] Add team flag display to rankings overlays --- osu.Game/Overlays/KudosuTable.cs | 4 ++-- .../Overlays/Rankings/Tables/CountriesTable.cs | 2 +- .../Overlays/Rankings/Tables/RankingsTable.cs | 17 +++++++---------- .../Overlays/Rankings/Tables/UserBasedTable.cs | 6 ++++-- 4 files changed, 14 insertions(+), 15 deletions(-) diff --git a/osu.Game/Overlays/KudosuTable.cs b/osu.Game/Overlays/KudosuTable.cs index 93884435a4..d6eaf586b9 100644 --- a/osu.Game/Overlays/KudosuTable.cs +++ b/osu.Game/Overlays/KudosuTable.cs @@ -80,7 +80,7 @@ namespace osu.Game.Overlays protected override CountryCode GetCountryCode(APIUser item) => item.CountryCode; - protected override Drawable CreateFlagContent(APIUser item) + protected override Drawable[] CreateFlagContent(APIUser item) { var username = new LinkFlowContainer(t => t.Font = OsuFont.GetFont(size: TEXT_SIZE, italics: true)) { @@ -89,7 +89,7 @@ namespace osu.Game.Overlays TextAnchor = Anchor.CentreLeft }; username.AddUserLink(item); - return username; + return [username]; } } } diff --git a/osu.Game/Overlays/Rankings/Tables/CountriesTable.cs b/osu.Game/Overlays/Rankings/Tables/CountriesTable.cs index fb3e58d2ac..733aa7ca54 100644 --- a/osu.Game/Overlays/Rankings/Tables/CountriesTable.cs +++ b/osu.Game/Overlays/Rankings/Tables/CountriesTable.cs @@ -34,7 +34,7 @@ namespace osu.Game.Overlays.Rankings.Tables protected override CountryCode GetCountryCode(CountryStatistics item) => item.Code; - protected override Drawable CreateFlagContent(CountryStatistics item) => new CountryName(item.Code); + protected override Drawable[] CreateFlagContent(CountryStatistics item) => [new CountryName(item.Code)]; protected override Drawable[] CreateAdditionalContent(CountryStatistics item) => new Drawable[] { diff --git a/osu.Game/Overlays/Rankings/Tables/RankingsTable.cs b/osu.Game/Overlays/Rankings/Tables/RankingsTable.cs index b9f7e443ca..f4ed41800a 100644 --- a/osu.Game/Overlays/Rankings/Tables/RankingsTable.cs +++ b/osu.Game/Overlays/Rankings/Tables/RankingsTable.cs @@ -80,7 +80,7 @@ namespace osu.Game.Overlays.Rankings.Tables protected abstract CountryCode GetCountryCode(TModel item); - protected abstract Drawable CreateFlagContent(TModel item); + protected abstract Drawable[] CreateFlagContent(TModel item); private OsuSpriteText createIndexDrawable(int index) => new RowText { @@ -92,16 +92,13 @@ namespace osu.Game.Overlays.Rankings.Tables { AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, - Spacing = new Vector2(10, 0), + Spacing = new Vector2(5, 0), Margin = new MarginPadding { Bottom = row_spacing }, - Children = new[] - { - new UpdateableFlag(GetCountryCode(item)) - { - Size = new Vector2(28, 20), - }, - CreateFlagContent(item) - } + Children = + [ + new UpdateableFlag(GetCountryCode(item)) { Size = new Vector2(28, 20) }, + ..CreateFlagContent(item) + ] }; protected class RankingsTableColumn : TableColumn diff --git a/osu.Game/Overlays/Rankings/Tables/UserBasedTable.cs b/osu.Game/Overlays/Rankings/Tables/UserBasedTable.cs index 4d25065578..c651108ec3 100644 --- a/osu.Game/Overlays/Rankings/Tables/UserBasedTable.cs +++ b/osu.Game/Overlays/Rankings/Tables/UserBasedTable.cs @@ -14,6 +14,8 @@ using osu.Game.Users; using osu.Game.Scoring; using osu.Framework.Localisation; using osu.Game.Resources.Localisation.Web; +using osu.Game.Users.Drawables; +using osuTK; namespace osu.Game.Overlays.Rankings.Tables { @@ -61,7 +63,7 @@ namespace osu.Game.Overlays.Rankings.Tables protected sealed override CountryCode GetCountryCode(UserStatistics item) => item.User.CountryCode; - protected sealed override Drawable CreateFlagContent(UserStatistics item) + protected sealed override Drawable[] CreateFlagContent(UserStatistics item) { var username = new LinkFlowContainer(t => t.Font = OsuFont.GetFont(size: TEXT_SIZE, italics: true)) { @@ -70,7 +72,7 @@ namespace osu.Game.Overlays.Rankings.Tables TextAnchor = Anchor.CentreLeft }; username.AddUserLink(item.User); - return username; + return [new UpdateableTeamFlag(item.User.Team) { Size = new Vector2(40, 20) }, username]; } protected sealed override Drawable[] CreateAdditionalContent(UserStatistics item) => new[] From 55809f5e0d7429dcaf8a59d6c1c82323bc8055de Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Fri, 14 Feb 2025 06:15:32 -0500 Subject: [PATCH 101/107] Apply changes to Android --- osu.Android/OsuGameAndroid.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Android/OsuGameAndroid.cs b/osu.Android/OsuGameAndroid.cs index 0f2451f0a0..e725f9245f 100644 --- a/osu.Android/OsuGameAndroid.cs +++ b/osu.Android/OsuGameAndroid.cs @@ -12,6 +12,7 @@ using osu.Game; using osu.Game.Screens; using osu.Game.Updater; using osu.Game.Utils; +using osuTK; namespace osu.Android { @@ -20,6 +21,8 @@ namespace osu.Android [Cached] private readonly OsuGameActivity gameActivity; + protected override Vector2 ScalingContainerTargetDrawSize => new Vector2(1024, 1024 * DrawHeight / DrawWidth); + public OsuGameAndroid(OsuGameActivity activity) : base(null) { From 27b9a6b7a386fb975df780dfd78d3ce3bcf114e9 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Fri, 14 Feb 2025 06:15:56 -0500 Subject: [PATCH 102/107] Reset UI scale for mobile platforms --- .../.idea/deploymentTargetSelector.xml | 10 ++++++++++ osu.Game/Configuration/OsuConfigManager.cs | 13 ++++++++----- 2 files changed, 18 insertions(+), 5 deletions(-) create mode 100644 .idea/.idea.osu.Android/.idea/deploymentTargetSelector.xml diff --git a/.idea/.idea.osu.Android/.idea/deploymentTargetSelector.xml b/.idea/.idea.osu.Android/.idea/deploymentTargetSelector.xml new file mode 100644 index 0000000000..4432459b86 --- /dev/null +++ b/.idea/.idea.osu.Android/.idea/deploymentTargetSelector.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 1244dd8cfc..76d06f3665 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -238,7 +238,7 @@ namespace osu.Game.Configuration public void Migrate() { - // arrives as 2020.123.0 + // arrives as 2020.123.0-lazer string rawVersion = Get(OsuSetting.Version); if (rawVersion.Length < 6) @@ -251,11 +251,14 @@ namespace osu.Game.Configuration if (!int.TryParse(pieces[0], out int year)) return; if (!int.TryParse(pieces[1], out int monthDay)) return; - // ReSharper disable once UnusedVariable - int combined = (year * 10000) + monthDay; + int combined = year * 10000 + monthDay; - // migrations can be added here using a condition like: - // if (combined < 20220103) { performMigration() } + if (combined < 20250214) + { + // UI scaling on mobile platforms has been internally adjusted such that 1x UI scale looks correctly zoomed in than before. + if (RuntimeInfo.IsMobile) + GetBindable(OsuSetting.UIScale).SetDefault(); + } } public override TrackedSettings CreateTrackedSettings() From ef2f482d041840bb4875a19b3f9a351b0415a63c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 14 Feb 2025 12:40:54 +0100 Subject: [PATCH 103/107] Fix skin deserialisation test --- .../Archives/modified-argon-20250214.osk | Bin 0 -> 1724 bytes osu.Game.Tests/Skins/SkinDeserialisationTest.cs | 2 ++ 2 files changed, 2 insertions(+) create mode 100644 osu.Game.Tests/Resources/Archives/modified-argon-20250214.osk diff --git a/osu.Game.Tests/Resources/Archives/modified-argon-20250214.osk b/osu.Game.Tests/Resources/Archives/modified-argon-20250214.osk new file mode 100644 index 0000000000000000000000000000000000000000..74abef25caa81004c11911a9d223909871da3299 GIT binary patch literal 1724 zcmZ{k2T;>l5QqPvBSjL5l|$*B&^+NzKsZ`35Q0HasRALCgd|9F4y8*dCn_Ze7J3UH zEf|nGO6Q0`b1=#Rl_Vz3y6>7BlANrc9&8eklY|tv zVI`T2nXcBXvieq!P2UZFqL3J_Ku%O3Z-w6t$7~^D)+eGen7nB8?2Pwp;etW*BCrxH z;eHc#u^#w7K~UhK(TCgsur31tQk(?g9vG|@>Nhi7U=R-LjSVJ#$#jW4g&RL1)-|9Ejozs>R#YJ&u;ia&3oMki*M;&wH(~k5NDD`iJ)KX=B);?Ela*&jx*7ht5pW>`|Qe8KWThWWUIAzhf zde~z&Rkq)rUlt++ai~{!35_8+=tzcw~yXl5`PiWV`aid|IF$LuJ+n}T+);r%ID zh!xQ!l1!HM3L7EUg^iWCvuxlV>t#W7j&K=Jy{vyY^Pb}fQ#O?-zfu!;dcMgr%Z)8r zKw=+tO_P|dH!K#$>Rm{*)HXeWfCF#2bUQ9(36$L$!qZW=^QcP?MZi86(Df>HOK7D< zGN`d&UL>#0_%wGuBia240Rg|_{Jd1w4W2Y+Omp3kGPiiFoM2U`*YfIEQsiz1PR%2U z6g`&#Gb{{F`i?ge{wUZd{>C`iwzHgQ)rApzF|lyPmk;i!JIrY zSa-N__^cGQ<)t!axQ?RMP7T%l;OZ{2_Ex>*)`55}*t2H36C{2rWpcG3a~gPm*y@Cj zJVt}=>MIhkJW`c8n`+U^nxICo=qpl*J zPySSNNUCcil)@YbCk(e4t_O~brt*<#FUuThMJ7Itlout+d@)abt4N;dSX)wl)hE#R zf4#Ti^)+42A2;BbD8~>19^pP1?*OlFG}>pQq54< From b21dd01de7263ecb6fa2817409b23e9eb16427c6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 15 Feb 2025 00:03:41 +0900 Subject: [PATCH 104/107] Use fixed width for digital clock display Supersedes and closes https://github.com/ppy/osu/pull/31093. --- .../Overlays/Toolbar/DigitalClockDisplay.cs | 39 ++++++++++++++----- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/osu.Game/Overlays/Toolbar/DigitalClockDisplay.cs b/osu.Game/Overlays/Toolbar/DigitalClockDisplay.cs index ada2f6ff86..bd1c944847 100644 --- a/osu.Game/Overlays/Toolbar/DigitalClockDisplay.cs +++ b/osu.Game/Overlays/Toolbar/DigitalClockDisplay.cs @@ -7,8 +7,10 @@ using System; using osu.Framework.Allocation; using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osuTK; namespace osu.Game.Overlays.Toolbar { @@ -17,6 +19,8 @@ namespace osu.Game.Overlays.Toolbar private OsuSpriteText realTime; private OsuSpriteText gameTime; + private FillFlowContainer runningText; + private bool showRuntime = true; public bool ShowRuntime @@ -52,17 +56,36 @@ namespace osu.Game.Overlays.Toolbar [BackgroundDependencyLoader] private void load(OsuColour colours) { - AutoSizeAxes = Axes.Y; + AutoSizeAxes = Axes.Both; InternalChildren = new Drawable[] { - realTime = new OsuSpriteText(), - gameTime = new OsuSpriteText + realTime = new OsuSpriteText + { + Font = OsuFont.Default.With(fixedWidth: true), + Spacing = new Vector2(-1.5f, 0), + }, + runningText = new FillFlowContainer { Y = 14, Colour = colours.PinkLight, - Font = OsuFont.Default.With(size: 10, weight: FontWeight.SemiBold), - } + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(2, 0), + Children = new Drawable[] + { + new OsuSpriteText + { + Text = "running", + Font = OsuFont.Default.With(size: 10, weight: FontWeight.SemiBold), + }, + gameTime = new OsuSpriteText + { + Font = OsuFont.Default.With(size: 10, fixedWidth: true, weight: FontWeight.SemiBold), + Spacing = new Vector2(-0.5f, 0), + } + } + }, }; updateMetrics(); @@ -71,14 +94,12 @@ namespace osu.Game.Overlays.Toolbar protected override void UpdateDisplay(DateTimeOffset now) { realTime.Text = now.ToLocalisableString(use24HourDisplay ? @"HH:mm:ss" : @"h:mm:ss tt"); - gameTime.Text = $"running {new TimeSpan(TimeSpan.TicksPerSecond * (int)(Clock.CurrentTime / 1000)):c}"; + gameTime.Text = $"{new TimeSpan(TimeSpan.TicksPerSecond * (int)(Clock.CurrentTime / 1000)):c}"; } private void updateMetrics() { - Width = showRuntime || !use24HourDisplay ? 66 : 45; // Allows for space for game time up to 99 days (in the padding area since this is quite rare). - - gameTime.FadeTo(showRuntime ? 1 : 0); + runningText.FadeTo(showRuntime ? 1 : 0); } } } From 7eb32ef35139793b5513c792eb3a5608fee3c207 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Sun, 16 Feb 2025 13:43:16 -0800 Subject: [PATCH 105/107] Fix team flag layout on user profile --- .../Profile/Header/TopHeaderContainer.cs | 60 +++++++++++++------ 1 file changed, 43 insertions(+), 17 deletions(-) diff --git a/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs index 5f404375e6..d6bc726c18 100644 --- a/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs +++ b/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs @@ -42,9 +42,10 @@ namespace osu.Game.Overlays.Profile.Header private ExternalLinkButton openUserExternally = null!; private OsuSpriteText titleText = null!; private UpdateableFlag userFlag = null!; - private UpdateableTeamFlag teamFlag = null!; private OsuHoverContainer userCountryContainer = null!; private OsuSpriteText userCountryText = null!; + private UpdateableTeamFlag teamFlag = null!; + private OsuSpriteText teamText = null!; private GroupBadgeFlow groupBadgeFlow = null!; private ToggleCoverButton coverToggle = null!; private PreviousUsernamesDisplay previousUsernamesDisplay = null!; @@ -161,27 +162,51 @@ namespace osu.Game.Overlays.Profile.Header { AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, + Spacing = new Vector2(10, 0), Children = new Drawable[] { - userFlag = new UpdateableFlag - { - Size = new Vector2(28, 20), - }, - teamFlag = new UpdateableTeamFlag - { - Size = new Vector2(40, 20), - }, - userCountryContainer = new OsuHoverContainer + new FillFlowContainer { AutoSizeAxes = Axes.Both, - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Margin = new MarginPadding { Left = 5 }, - Child = userCountryText = new OsuSpriteText + Direction = FillDirection.Horizontal, + Spacing = new Vector2(4, 0), + Children = new Drawable[] { - Font = OsuFont.GetFont(size: 14f, weight: FontWeight.Regular), - }, + userFlag = new UpdateableFlag + { + Size = new Vector2(28, 20), + }, + userCountryContainer = new OsuHoverContainer + { + AutoSizeAxes = Axes.Both, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Child = userCountryText = new OsuSpriteText + { + Font = OsuFont.GetFont(size: 14f, weight: FontWeight.Regular), + }, + }, + } }, + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(4, 0), + Children = new Drawable[] + { + teamFlag = new UpdateableTeamFlag + { + Size = new Vector2(40, 20), + }, + teamText = new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Font = OsuFont.GetFont(size: 14f, weight: FontWeight.Regular), + }, + } + } } }, } @@ -220,9 +245,10 @@ namespace osu.Game.Overlays.Profile.Header usernameText.Text = user?.Username ?? string.Empty; openUserExternally.Link = $@"{api.Endpoints.WebsiteUrl}/users/{user?.Id ?? 0}"; userFlag.CountryCode = user?.CountryCode ?? default; - teamFlag.Team = user?.Team; userCountryText.Text = (user?.CountryCode ?? default).GetDescription(); userCountryContainer.Action = () => rankingsOverlay?.ShowCountry(user?.CountryCode ?? default); + teamFlag.Team = user?.Team; + teamText.Text = user?.Team?.Name ?? string.Empty; supporterTag.SupportLevel = user?.SupportLevel ?? 0; titleText.Text = user?.Title ?? string.Empty; titleText.Colour = Color4Extensions.FromHex(user?.Colour ?? "fff"); From 1b333ad51c2147f5ab950a03f7de49a07721c01a Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sun, 16 Feb 2025 17:53:34 -0500 Subject: [PATCH 106/107] Add sample team to user profile test scene --- .../Visual/Online/TestSceneUserProfileOverlay.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs index d16ed46bd2..a4a9816337 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs @@ -346,6 +346,13 @@ namespace osu.Game.Tests.Visual.Online Twitter = "test_user", Discord = "test_user", Website = "https://google.com", + Team = new APITeam + { + Id = 1, + Name = "Collective Wangs", + ShortName = "WANG", + FlagUrl = "https://assets.ppy.sh/teams/logo/1/wanglogo.jpg", + } }; } } From afc2c521955f00654c3c824bebe294afabf4d221 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sun, 16 Feb 2025 17:55:10 -0500 Subject: [PATCH 107/107] Add proper spacing between username, title, and country/team row --- osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs index d6bc726c18..3d9539ce1f 100644 --- a/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs +++ b/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs @@ -156,10 +156,11 @@ namespace osu.Game.Overlays.Profile.Header titleText = new OsuSpriteText { Font = OsuFont.GetFont(size: 16, weight: FontWeight.Regular), - Margin = new MarginPadding { Bottom = 5 } + Margin = new MarginPadding { Bottom = 3 }, }, new FillFlowContainer { + Margin = new MarginPadding { Top = 3 }, AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, Spacing = new Vector2(10, 0),