From f95744170f008e5aecf912a4a380e6191296089b Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 5 May 2021 07:06:26 +0300 Subject: [PATCH 001/106] Add skin config lookup for combo colours --- osu.Game/Skinning/SkinComboColourLookup.cs | 26 +++++++++++++++++++++ osu.Game/Skinning/SkinProvidingContainer.cs | 2 +- 2 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 osu.Game/Skinning/SkinComboColourLookup.cs diff --git a/osu.Game/Skinning/SkinComboColourLookup.cs b/osu.Game/Skinning/SkinComboColourLookup.cs new file mode 100644 index 0000000000..33e35a96fb --- /dev/null +++ b/osu.Game/Skinning/SkinComboColourLookup.cs @@ -0,0 +1,26 @@ +// 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.Types; + +namespace osu.Game.Skinning +{ + public class SkinComboColourLookup + { + /// + /// The index to use for deciding the combo colour. + /// + public readonly int ColourIndex; + + /// + /// The combo information requesting the colour. + /// + public readonly IHasComboInformation Combo; + + public SkinComboColourLookup(int colourIndex, IHasComboInformation combo) + { + ColourIndex = colourIndex; + Combo = combo; + } + } +} diff --git a/osu.Game/Skinning/SkinProvidingContainer.cs b/osu.Game/Skinning/SkinProvidingContainer.cs index cf22b2e820..a7fc1306c9 100644 --- a/osu.Game/Skinning/SkinProvidingContainer.cs +++ b/osu.Game/Skinning/SkinProvidingContainer.cs @@ -72,7 +72,7 @@ namespace osu.Game.Skinning { if (skin != null) { - if (lookup is GlobalSkinColours || lookup is SkinCustomColourLookup) + if (lookup is GlobalSkinColours || lookup is SkinComboColourLookup || lookup is SkinCustomColourLookup) return lookupWithFallback(lookup, AllowColourLookup); return lookupWithFallback(lookup, AllowConfigurationLookup); From 9be8d3f0d21b77a63110ea0335de65a3ce0dfc06 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 5 May 2021 07:12:17 +0300 Subject: [PATCH 002/106] Add overridable combo colour retrieval method and handle in legacy skin --- osu.Game/Skinning/LegacySkin.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index eae3b69233..e827dca232 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -16,6 +16,7 @@ using osu.Framework.IO.Stores; using osu.Game.Audio; using osu.Game.Beatmaps.Formats; using osu.Game.IO; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Play.HUD; using osuTK.Graphics; @@ -119,6 +120,9 @@ namespace osu.Game.Skinning break; + case SkinComboColourLookup comboColour: + return SkinUtils.As(GetComboColour(Configuration, comboColour.ColourIndex, comboColour.Combo)); + case SkinCustomColourLookup customColour: return SkinUtils.As(getCustomColour(Configuration, customColour.Lookup.ToString())); @@ -276,6 +280,18 @@ namespace osu.Game.Skinning return null; } + /// + /// Retrieves the correct combo colour for a given colour index and information on the combo. + /// + /// The source to retrieve the combo colours from. + /// The preferred index for retrieving the combo colour with. + /// Information on the combo whose using the returned colour. + protected virtual IBindable GetComboColour(IHasComboColours source, int colourIndex, IHasComboInformation combo) + { + var colour = source.ComboColours?[colourIndex % source.ComboColours.Count]; + return colour.HasValue ? new Bindable(colour.Value) : null; + } + private IBindable getCustomColour(IHasCustomColours source, string lookup) => source.CustomColours.TryGetValue(lookup, out var col) ? new Bindable(col) : null; From 78794935b47f95648ac620378ddfe11108933e59 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 5 May 2021 07:11:45 +0300 Subject: [PATCH 003/106] Handle combo colour lookups in other skins --- .../Gameplay/TestSceneHitObjectAccentColour.cs | 10 ++-------- osu.Game/Skinning/DefaultSkin.cs | 11 +++++++++-- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Gameplay/TestSceneHitObjectAccentColour.cs b/osu.Game.Tests/Gameplay/TestSceneHitObjectAccentColour.cs index 883791c35c..d08d08390b 100644 --- a/osu.Game.Tests/Gameplay/TestSceneHitObjectAccentColour.cs +++ b/osu.Game.Tests/Gameplay/TestSceneHitObjectAccentColour.cs @@ -127,14 +127,8 @@ namespace osu.Game.Tests.Gameplay { switch (lookup) { - case GlobalSkinColours global: - switch (global) - { - case GlobalSkinColours.ComboColours: - return SkinUtils.As(new Bindable>(ComboColours)); - } - - break; + case SkinComboColourLookup comboColour: + return SkinUtils.As(new Bindable(ComboColours[comboColour.ColourIndex % ComboColours.Count])); } throw new NotImplementedException(); diff --git a/osu.Game/Skinning/DefaultSkin.cs b/osu.Game/Skinning/DefaultSkin.cs index 0b3f5f3cde..a2d05a918c 100644 --- a/osu.Game/Skinning/DefaultSkin.cs +++ b/osu.Game/Skinning/DefaultSkin.cs @@ -8,6 +8,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Textures; using osu.Game.Audio; +using osu.Game.Beatmaps.Formats; using osuTK.Graphics; namespace osu.Game.Skinning @@ -28,10 +29,10 @@ namespace osu.Game.Skinning public override IBindable GetConfig(TLookup lookup) { + // todo: this code is pulled from LegacySkin and should not exist. + // will likely change based on how databased storage of skin configuration goes. switch (lookup) { - // todo: this code is pulled from LegacySkin and should not exist. - // will likely change based on how databased storage of skin configuration goes. case GlobalSkinColours global: switch (global) { @@ -40,9 +41,15 @@ namespace osu.Game.Skinning } break; + + case SkinComboColourLookup comboColour: + return SkinUtils.As(new Bindable(getComboColour(Configuration, comboColour.ColourIndex))); } return null; } + + private static Color4 getComboColour(IHasComboColours source, int colourIndex) + => source.ComboColours[colourIndex % source.ComboColours.Count]; } } From eeeb001d62279df9a5430b4345844dccc042acfd Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 5 May 2021 07:17:27 +0300 Subject: [PATCH 004/106] Refactor combo colour retrieval logic to request skin lookups instead --- osu.Game.Rulesets.Catch/Objects/Banana.cs | 3 ++- .../Objects/PalpableCatchHitObject.cs | 4 ++-- .../Objects/Drawables/DrawableHitObject.cs | 3 +-- .../Objects/Types/IHasComboInformation.cs | 23 +++++++++++++------ .../Timeline/TimelineHitObjectBlueprint.cs | 4 +--- 5 files changed, 22 insertions(+), 15 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/Banana.cs b/osu.Game.Rulesets.Catch/Objects/Banana.cs index 178306b3bc..e5a36d08db 100644 --- a/osu.Game.Rulesets.Catch/Objects/Banana.cs +++ b/osu.Game.Rulesets.Catch/Objects/Banana.cs @@ -9,6 +9,7 @@ using osu.Game.Audio; using osu.Game.Rulesets.Catch.Judgements; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects.Types; +using osu.Game.Skinning; using osu.Game.Utils; using osuTK.Graphics; @@ -31,7 +32,7 @@ namespace osu.Game.Rulesets.Catch.Objects } // override any external colour changes with banananana - Color4 IHasComboInformation.GetComboColour(IReadOnlyList comboColours) => getBananaColour(); + Color4 IHasComboInformation.GetComboColour(ISkin skin) => getBananaColour(); private Color4 getBananaColour() { diff --git a/osu.Game.Rulesets.Catch/Objects/PalpableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/PalpableCatchHitObject.cs index 0cd3af01df..24c12343e5 100644 --- a/osu.Game.Rulesets.Catch/Objects/PalpableCatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/PalpableCatchHitObject.cs @@ -1,9 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Generic; using osu.Framework.Bindables; using osu.Game.Rulesets.Objects.Types; +using osu.Game.Skinning; using osuTK.Graphics; namespace osu.Game.Rulesets.Catch.Objects @@ -43,6 +43,6 @@ namespace osu.Game.Rulesets.Catch.Objects } } - Color4 IHasComboInformation.GetComboColour(IReadOnlyList comboColours) => comboColours[(IndexInBeatmap + 1) % comboColours.Count]; + Color4 IHasComboInformation.GetComboColour(ISkin skin) => IHasComboInformation.GetSkinComboColour(this, skin, IndexInBeatmap + 1); } } diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 86c733c392..63ae2e286f 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -509,8 +509,7 @@ namespace osu.Game.Rulesets.Objects.Drawables { if (!(HitObject is IHasComboInformation combo)) return; - var comboColours = CurrentSkin.GetConfig>(GlobalSkinColours.ComboColours)?.Value ?? Array.Empty(); - AccentColour.Value = combo.GetComboColour(comboColours); + AccentColour.Value = combo.GetComboColour(CurrentSkin); } /// diff --git a/osu.Game/Rulesets/Objects/Types/IHasComboInformation.cs b/osu.Game/Rulesets/Objects/Types/IHasComboInformation.cs index 4f66802079..03e6f76cca 100644 --- a/osu.Game/Rulesets/Objects/Types/IHasComboInformation.cs +++ b/osu.Game/Rulesets/Objects/Types/IHasComboInformation.cs @@ -1,9 +1,8 @@ // 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 JetBrains.Annotations; using osu.Framework.Bindables; +using osu.Game.Skinning; using osuTK.Graphics; namespace osu.Game.Rulesets.Objects.Types @@ -40,11 +39,21 @@ namespace osu.Game.Rulesets.Objects.Types bool LastInCombo { get; set; } /// - /// Retrieves the colour of the combo described by this object from a set of possible combo colours. - /// Defaults to using to decide the colour. + /// Retrieves the colour of the combo described by this object. /// - /// A list of possible combo colours provided by the beatmap or skin. - /// The colour of the combo described by this object. - Color4 GetComboColour([NotNull] IReadOnlyList comboColours) => comboColours.Count > 0 ? comboColours[ComboIndex % comboColours.Count] : Color4.White; + /// The skin to retrieve the combo colour from, if wanted. + Color4 GetComboColour(ISkin skin) => GetSkinComboColour(this, skin, ComboIndex); + + /// + /// Retrieves the colour of the combo described by a given object from a given skin. + /// + /// The combo information, should be this. + /// The skin to retrieve the combo colour from. + /// The index to retrieve the combo colour with. + /// + protected static Color4 GetSkinComboColour(IHasComboInformation combo, ISkin skin, int comboIndex) + { + return skin.GetConfig(new SkinComboColourLookup(comboIndex, combo))?.Value ?? Color4.White; + } } } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs index dbe689be2f..67a28c4fa0 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -139,8 +138,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline if (!(Item is IHasComboInformation combo)) return; - var comboColours = skin.GetConfig>(GlobalSkinColours.ComboColours)?.Value ?? Array.Empty(); - var comboColour = combo.GetComboColour(comboColours); + var comboColour = combo.GetComboColour(skin); if (IsSelected) { From d3bb4ddbee09ebcab963add6fcce38945d01b99f Mon Sep 17 00:00:00 2001 From: ekrctb Date: Mon, 5 Jul 2021 19:05:49 +0900 Subject: [PATCH 005/106] Add an ad-hoc way to provide dependency to children --- .../Visual/DependencyProvidingContainer.cs | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 osu.Game/Tests/Visual/DependencyProvidingContainer.cs diff --git a/osu.Game/Tests/Visual/DependencyProvidingContainer.cs b/osu.Game/Tests/Visual/DependencyProvidingContainer.cs new file mode 100644 index 0000000000..cd3ae1123b --- /dev/null +++ b/osu.Game/Tests/Visual/DependencyProvidingContainer.cs @@ -0,0 +1,50 @@ +// 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.Linq; +using osu.Framework.Allocation; +using osu.Framework.Graphics.Containers; + +namespace osu.Game.Tests.Visual +{ + /// + /// A which providing ad-hoc dependencies to the child drawables. + /// + /// To provide a dependency, specify the dependency type to , then specify the dependency value to either or . + /// For each type specified in , the first value compatible with the type is selected and provided to the children. + /// + /// + /// + /// The and values of the dependencies must be set while this is not loaded. + /// + public class DependencyProvidingContainer : Container + { + /// + /// The types of the dependencies provided to the children. + /// + // TODO: should be an init-only property when C# 9 + public Type[] Types { get; set; } = Array.Empty(); + + /// + /// The dependency values provided to the children. + /// + public object[] Values { get; set; } = Array.Empty(); + + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) + { + var dependencyContainer = new DependencyContainer(base.CreateChildDependencies(parent)); + + foreach (var type in Types) + { + object value = Values.FirstOrDefault(v => type.IsInstanceOfType(v)) ?? + Children.FirstOrDefault(d => type.IsInstanceOfType(d)) ?? + throw new InvalidOperationException($"The type {type} is specified in this {nameof(DependencyProvidingContainer)}, but no corresponding value is provided."); + + dependencyContainer.CacheAs(type, value); + } + + return dependencyContainer; + } + } +} From 5a0a223b1b96d2f10630d888d54f68e06a9acb95 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Mon, 5 Jul 2021 19:07:29 +0900 Subject: [PATCH 006/106] Use `DependencyProvidingContainer` in `TestSceneCatcher` --- .../TestSceneCatcher.cs | 36 +++++++++---------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs index 8359657f84..0cf5e2b7a0 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs @@ -31,22 +31,9 @@ namespace osu.Game.Rulesets.Catch.Tests [Resolved] private OsuConfigManager config { get; set; } - [Cached] - private readonly DroppedObjectContainer droppedObjectContainer; - - private readonly Container trailContainer; - private TestCatcher catcher; - public TestSceneCatcher() - { - Add(trailContainer = new Container - { - Anchor = Anchor.Centre, - Depth = -1 - }); - Add(droppedObjectContainer = new DroppedObjectContainer()); - } + private DroppedObjectContainer droppedObjectContainer; [SetUp] public void SetUp() => Schedule(() => @@ -56,13 +43,24 @@ namespace osu.Game.Rulesets.Catch.Tests CircleSize = 0, }; - if (catcher != null) - Remove(catcher); - - Add(catcher = new TestCatcher(trailContainer, difficulty) + var trailContainer = new Container { + Anchor = Anchor.Centre, + }; + Child = new DependencyProvidingContainer + { + Types = new[] + { + typeof(DroppedObjectContainer), + }, + Children = new Drawable[] + { + droppedObjectContainer = new DroppedObjectContainer(), + catcher = new TestCatcher(trailContainer, difficulty), + trailContainer + }, Anchor = Anchor.Centre - }); + }; }); [Test] From 0db316d644064bd168454fcb1354b804ebe23918 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 7 Jul 2021 16:08:20 +0900 Subject: [PATCH 007/106] Add password scaffolding --- osu.Game/Online/Rooms/Room.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Online/Rooms/Room.cs b/osu.Game/Online/Rooms/Room.cs index b28680ffef..9270c4b69d 100644 --- a/osu.Game/Online/Rooms/Room.cs +++ b/osu.Game/Online/Rooms/Room.cs @@ -35,6 +35,9 @@ namespace osu.Game.Online.Rooms [JsonProperty("channel_id")] public readonly Bindable ChannelId = new Bindable(); + [JsonProperty("password")] + public readonly Bindable Password = new Bindable(); + [Cached] [JsonIgnore] public readonly Bindable Category = new Bindable(); @@ -143,6 +146,7 @@ namespace osu.Game.Online.Rooms ChannelId.Value = other.ChannelId.Value; Status.Value = other.Status.Value; + Password.Value = other.Password.Value; Availability.Value = other.Availability.Value; Type.Value = other.Type.Value; MaxParticipants.Value = other.MaxParticipants.Value; From 24f330e5c1ae697f8e02eefb314fcefc559f9d4b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 7 Jul 2021 16:12:28 +0900 Subject: [PATCH 008/106] Avoid `MatchSettingsOverlay` base class potentially accessing an uninitialised field --- .../OnlinePlay/Match/Components/MatchSettingsOverlay.cs | 4 ++++ .../Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs | 7 ++----- .../OnlinePlay/Playlists/PlaylistsMatchSettingsOverlay.cs | 7 ++----- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Match/Components/MatchSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Match/Components/MatchSettingsOverlay.cs index 5699da740c..61bb39d0c5 100644 --- a/osu.Game/Screens/OnlinePlay/Match/Components/MatchSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Match/Components/MatchSettingsOverlay.cs @@ -25,8 +25,12 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components private void load() { Masking = true; + + Add(Settings = CreateSettings()); } + protected abstract OnlinePlayComposite CreateSettings(); + protected override void PopIn() { Settings.MoveToY(0, TRANSITION_DURATION, Easing.OutQuint); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs index 2e180f31fd..81ee580a24 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs @@ -27,16 +27,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { public class MultiplayerMatchSettingsOverlay : MatchSettingsOverlay { - [BackgroundDependencyLoader] - private void load() - { - Child = Settings = new MatchSettings + protected override OnlinePlayComposite CreateSettings() + => new MatchSettings { RelativeSizeAxes = Axes.Both, RelativePositionAxes = Axes.Y, SettingsApplied = Hide }; - } protected class MatchSettings : OnlinePlayComposite { diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsMatchSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsMatchSettingsOverlay.cs index 5eb2b545cb..88ac5ef6e5 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsMatchSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsMatchSettingsOverlay.cs @@ -26,16 +26,13 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { public Action EditPlaylist; - [BackgroundDependencyLoader] - private void load() - { - Child = Settings = new MatchSettings + protected override OnlinePlayComposite CreateSettings() + => new MatchSettings { RelativeSizeAxes = Axes.Both, RelativePositionAxes = Axes.Y, EditPlaylist = () => EditPlaylist?.Invoke() }; - } protected class MatchSettings : OnlinePlayComposite { From 4fd6f2101c2d890372fc0c096d09df5c39284b6c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 7 Jul 2021 16:28:48 +0900 Subject: [PATCH 009/106] Add password textbox input --- osu.Game/Online/Rooms/Room.cs | 1 - .../Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/osu.Game/Online/Rooms/Room.cs b/osu.Game/Online/Rooms/Room.cs index 9270c4b69d..bccb6f854c 100644 --- a/osu.Game/Online/Rooms/Room.cs +++ b/osu.Game/Online/Rooms/Room.cs @@ -146,7 +146,6 @@ namespace osu.Game.Online.Rooms ChannelId.Value = other.ChannelId.Value; Status.Value = other.Status.Value; - Password.Value = other.Password.Value; Availability.Value = other.Availability.Value; Type.Value = other.Type.Value; MaxParticipants.Value = other.MaxParticipants.Value; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs index 81ee580a24..bc2f263a68 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs @@ -44,6 +44,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match public OsuTextBox NameField, MaxParticipantsField; public RoomAvailabilityPicker AvailabilityPicker; public GameTypePicker TypePicker; + public OsuTextBox PasswordTextBox; public TriangleButton ApplyButton; public OsuSpriteText ErrorText; @@ -190,12 +191,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match }, new Section("Password (optional)") { - Alpha = disabled_alpha, - Child = new SettingsPasswordTextBox + Child = PasswordTextBox = new SettingsPasswordTextBox { RelativeSizeAxes = Axes.X, TabbableContentContainer = this, - ReadOnly = true, }, }, } @@ -317,6 +316,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match currentRoom.Value.Name.Value = NameField.Text; currentRoom.Value.Availability.Value = AvailabilityPicker.Current.Value; currentRoom.Value.Type.Value = TypePicker.Current.Value; + currentRoom.Value.Password.Value = PasswordTextBox.Current.Value; if (int.TryParse(MaxParticipantsField.Text, out int max)) currentRoom.Value.MaxParticipants.Value = max; From 6a74fde0824fe127c614a06ef5b8afcc7c105f71 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 7 Jul 2021 18:53:13 +0900 Subject: [PATCH 010/106] Add `has_password` flag and region post only parameters --- osu.Game/Online/Rooms/Room.cs | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/osu.Game/Online/Rooms/Room.cs b/osu.Game/Online/Rooms/Room.cs index bccb6f854c..d1701d544e 100644 --- a/osu.Game/Online/Rooms/Room.cs +++ b/osu.Game/Online/Rooms/Room.cs @@ -35,9 +35,6 @@ namespace osu.Game.Online.Rooms [JsonProperty("channel_id")] public readonly Bindable ChannelId = new Bindable(); - [JsonProperty("password")] - public readonly Bindable Password = new Bindable(); - [Cached] [JsonIgnore] public readonly Bindable Category = new Bindable(); @@ -51,10 +48,6 @@ namespace osu.Game.Online.Rooms set => Category.Value = value; } - [Cached] - [JsonIgnore] - public readonly Bindable Duration = new Bindable(); - [Cached] [JsonIgnore] public readonly Bindable MaxAttempts = new Bindable(); @@ -79,6 +72,9 @@ namespace osu.Game.Online.Rooms [JsonProperty("current_user_score")] public readonly Bindable UserScore = new Bindable(); + [JsonProperty("has_password")] + public readonly BindableBool HasPassword = new BindableBool(); + [Cached] [JsonProperty("recent_participants")] public readonly BindableList RecentParticipants = new BindableList(); @@ -87,6 +83,15 @@ namespace osu.Game.Online.Rooms [JsonProperty("participant_count")] public readonly Bindable ParticipantCount = new Bindable(); + #region Properties only used for room creation request + + [JsonProperty("password")] + public readonly Bindable Password = new Bindable(); + + [Cached] + [JsonIgnore] + public readonly Bindable Duration = new Bindable(); + [JsonProperty("duration")] private int? duration { @@ -100,6 +105,8 @@ namespace osu.Game.Online.Rooms } } + #endregion + // Only supports retrieval for now [Cached] [JsonProperty("ends_at")] From 2ca11d458a3ea885ebdae8a033fd0c9eda984317 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 9 Jul 2021 00:53:10 +0900 Subject: [PATCH 011/106] Add password to room settings and multiplayer lounge interface --- .../Multiplayer/IMultiplayerLoungeServer.cs | 10 +++++++++ .../Multiplayer/InvalidPasswordException.cs | 22 +++++++++++++++++++ .../Online/Multiplayer/MultiplayerClient.cs | 2 ++ .../Multiplayer/MultiplayerRoomSettings.cs | 5 +++++ 4 files changed, 39 insertions(+) create mode 100644 osu.Game/Online/Multiplayer/InvalidPasswordException.cs diff --git a/osu.Game/Online/Multiplayer/IMultiplayerLoungeServer.cs b/osu.Game/Online/Multiplayer/IMultiplayerLoungeServer.cs index 4640640c5f..a04ec53578 100644 --- a/osu.Game/Online/Multiplayer/IMultiplayerLoungeServer.cs +++ b/osu.Game/Online/Multiplayer/IMultiplayerLoungeServer.cs @@ -15,6 +15,16 @@ namespace osu.Game.Online.Multiplayer /// /// The databased room ID. /// If the user is already in the requested (or another) room. + /// If the room required a password. Task JoinRoom(long roomId); + + /// + /// Request to join a multiplayer room with a provided password. + /// + /// The databased room ID. + /// The password for the join request. + /// If the user is already in the requested (or another) room. + /// If the room provided password was incorrect. + Task JoinRoom(long roomId, string password); } } diff --git a/osu.Game/Online/Multiplayer/InvalidPasswordException.cs b/osu.Game/Online/Multiplayer/InvalidPasswordException.cs new file mode 100644 index 0000000000..0441aea287 --- /dev/null +++ b/osu.Game/Online/Multiplayer/InvalidPasswordException.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 System; +using System.Runtime.Serialization; +using Microsoft.AspNetCore.SignalR; + +namespace osu.Game.Online.Multiplayer +{ + [Serializable] + public class InvalidPasswordException : HubException + { + public InvalidPasswordException() + { + } + + protected InvalidPasswordException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + } +} diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 2e65f7cf1c..a9f4dc9e2f 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -212,6 +212,8 @@ namespace osu.Game.Online.Multiplayer return ChangeSettings(new MultiplayerRoomSettings { Name = name.GetOr(Room.Settings.Name), + // TODO: add changing support + Password = Room.Settings.Password, BeatmapID = item.GetOr(existingPlaylistItem).BeatmapID, BeatmapChecksum = item.GetOr(existingPlaylistItem).Beatmap.Value.MD5Hash, RulesetID = item.GetOr(existingPlaylistItem).RulesetID, diff --git a/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs b/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs index ee72df4c10..4e94c5982f 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs @@ -36,12 +36,16 @@ namespace osu.Game.Online.Multiplayer [Key(6)] public long PlaylistItemId { get; set; } + [Key(7)] + public string Password { get; set; } = string.Empty; + public bool Equals(MultiplayerRoomSettings other) => BeatmapID == other.BeatmapID && BeatmapChecksum == other.BeatmapChecksum && RequiredMods.SequenceEqual(other.RequiredMods) && AllowedMods.SequenceEqual(other.AllowedMods) && RulesetID == other.RulesetID + && Password.Equals(other.Password, StringComparison.Ordinal) && Name.Equals(other.Name, StringComparison.Ordinal) && PlaylistItemId == other.PlaylistItemId; @@ -49,6 +53,7 @@ namespace osu.Game.Online.Multiplayer + $" Beatmap:{BeatmapID} ({BeatmapChecksum})" + $" RequiredMods:{string.Join(',', RequiredMods)}" + $" AllowedMods:{string.Join(',', AllowedMods)}" + + $" Password:{(string.IsNullOrEmpty(Password) ? "no" : "yes")}" + $" Ruleset:{RulesetID}" + $" Item:{PlaylistItemId}"; } From 5148069efe501424d788993f7dc719470e551b7a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 9 Jul 2021 16:01:45 +0900 Subject: [PATCH 012/106] Update signatures in line with no-overload methods (unsupported by signalr) --- osu.Game/Online/Multiplayer/IMultiplayerLoungeServer.cs | 2 +- osu.Game/Online/Multiplayer/MultiplayerClient.cs | 3 ++- osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs | 4 ++-- osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs | 5 +++-- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/osu.Game/Online/Multiplayer/IMultiplayerLoungeServer.cs b/osu.Game/Online/Multiplayer/IMultiplayerLoungeServer.cs index a04ec53578..0a618c8f5c 100644 --- a/osu.Game/Online/Multiplayer/IMultiplayerLoungeServer.cs +++ b/osu.Game/Online/Multiplayer/IMultiplayerLoungeServer.cs @@ -25,6 +25,6 @@ namespace osu.Game.Online.Multiplayer /// The password for the join request. /// If the user is already in the requested (or another) room. /// If the room provided password was incorrect. - Task JoinRoom(long roomId, string password); + Task JoinRoomWithPassword(long roomId, string password); } } diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index a9f4dc9e2f..543b3cd752 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -152,8 +152,9 @@ namespace osu.Game.Online.Multiplayer /// Joins the with a given ID. /// /// The room ID. + /// An optional password to use when joining the room. /// The joined . - protected abstract Task JoinRoom(long roomId); + protected abstract Task JoinRoom(long roomId, string? password = null); public Task LeaveRoom() { diff --git a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs index cf1e18e059..726e26ebe1 100644 --- a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs @@ -62,12 +62,12 @@ namespace osu.Game.Online.Multiplayer } } - protected override Task JoinRoom(long roomId) + protected override Task JoinRoom(long roomId, string? password = null) { if (!IsConnected.Value) return Task.FromCanceled(new CancellationToken(true)); - return connection.InvokeAsync(nameof(IMultiplayerServer.JoinRoom), roomId); + return connection.InvokeAsync(nameof(IMultiplayerServer.JoinRoomWithPassword), roomId, password ?? string.Empty); } protected override Task LeaveRoomInternal() diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index b0c8d6d19b..adc632a2b1 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -115,7 +115,7 @@ namespace osu.Game.Tests.Visual.Multiplayer ((IMultiplayerClient)this).UserBeatmapAvailabilityChanged(userId, newBeatmapAvailability); } - protected override Task JoinRoom(long roomId) + protected override Task JoinRoom(long roomId, string? password = null) { var apiRoom = roomManager.Rooms.Single(r => r.RoomID.Value == roomId); @@ -134,7 +134,8 @@ namespace osu.Game.Tests.Visual.Multiplayer BeatmapChecksum = apiRoom.Playlist.Last().Beatmap.Value.MD5Hash, RequiredMods = apiRoom.Playlist.Last().RequiredMods.Select(m => new APIMod(m)).ToArray(), AllowedMods = apiRoom.Playlist.Last().AllowedMods.Select(m => new APIMod(m)).ToArray(), - PlaylistItemId = apiRoom.Playlist.Last().ID + PlaylistItemId = apiRoom.Playlist.Last().ID, + Password = password ?? string.Empty, }, Users = { localUser }, Host = localUser From 84b0a3290ce2ff6767308797f46e76770d020204 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 9 Jul 2021 16:01:52 +0900 Subject: [PATCH 013/106] Add multiplayer lounge test coverage --- .../TestSceneMultiplayerLoungeSubScreen.cs | 44 +++++++++++++++++++ .../Visual/OnlinePlay/BasicTestRoomManager.cs | 5 ++- 2 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs new file mode 100644 index 0000000000..0d90b75be7 --- /dev/null +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs @@ -0,0 +1,44 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Screens; +using osu.Framework.Testing; +using osu.Game.Screens.OnlinePlay.Lounge; +using osu.Game.Screens.OnlinePlay.Lounge.Components; +using osu.Game.Screens.OnlinePlay.Multiplayer; +using osu.Game.Tests.Visual.OnlinePlay; + +namespace osu.Game.Tests.Visual.Multiplayer +{ + public class TestSceneMultiplayerLoungeSubScreen : OnlinePlayTestScene + { + protected new BasicTestRoomManager RoomManager => (BasicTestRoomManager)base.RoomManager; + + private LoungeSubScreen loungeScreen; + + public override void SetUpSteps() + { + base.SetUpSteps(); + + AddStep("push screen", () => LoadScreen(loungeScreen = new MultiplayerLoungeSubScreen())); + + AddUntilStep("wait for present", () => loungeScreen.IsCurrentScreen()); + } + + [Test] + public void TestJoinRoomWithoutPassword() + { + AddStep("add room", () => RoomManager.AddRooms(1, withPassword: false)); + } + + [Test] + public void TestJoinRoomWithPassword() + { + AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true)); + } + + private RoomsContainer roomsContainer => loungeScreen.ChildrenOfType().First(); + } +} diff --git a/osu.Game/Tests/Visual/OnlinePlay/BasicTestRoomManager.cs b/osu.Game/Tests/Visual/OnlinePlay/BasicTestRoomManager.cs index 813e617ac5..f2ca3d6357 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/BasicTestRoomManager.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/BasicTestRoomManager.cs @@ -43,7 +43,7 @@ namespace osu.Game.Tests.Visual.OnlinePlay { } - public void AddRooms(int count, RulesetInfo ruleset = null) + public void AddRooms(int count, RulesetInfo ruleset = null, bool withPassword = false) { for (int i = 0; i < count; i++) { @@ -53,7 +53,8 @@ namespace osu.Game.Tests.Visual.OnlinePlay Name = { Value = $"Room {i}" }, Host = { Value = new User { Username = "Host" } }, EndDate = { Value = DateTimeOffset.Now + TimeSpan.FromSeconds(10) }, - Category = { Value = i % 2 == 0 ? RoomCategory.Spotlight : RoomCategory.Normal } + Category = { Value = i % 2 == 0 ? RoomCategory.Spotlight : RoomCategory.Normal }, + Password = { Value = withPassword ? "password" : string.Empty } }; if (ruleset != null) From 08c40938db22e20a6ece17b1e26776a8664629e9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 10 Jul 2021 13:50:52 +0900 Subject: [PATCH 014/106] Add support for updating a room's password --- osu.Game/Online/Multiplayer/MultiplayerClient.cs | 8 ++++---- .../Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 543b3cd752..052489ca97 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -127,7 +127,7 @@ namespace osu.Game.Online.Multiplayer Debug.Assert(room.RoomID.Value != null); // Join the server-side room. - var joinedRoom = await JoinRoom(room.RoomID.Value.Value).ConfigureAwait(false); + var joinedRoom = await JoinRoom(room.RoomID.Value.Value, room.Password.Value).ConfigureAwait(false); Debug.Assert(joinedRoom != null); // Populate users. @@ -190,8 +190,9 @@ namespace osu.Game.Online.Multiplayer /// A room must be joined for this to have any effect. /// /// The new room name, if any. + /// The new password, if any. /// The new room playlist item, if any. - public Task ChangeSettings(Optional name = default, Optional item = default) + public Task ChangeSettings(Optional name = default, Optional password = default, Optional item = default) { if (Room == null) throw new InvalidOperationException("Must be joined to a match to change settings."); @@ -213,8 +214,7 @@ namespace osu.Game.Online.Multiplayer return ChangeSettings(new MultiplayerRoomSettings { Name = name.GetOr(Room.Settings.Name), - // TODO: add changing support - Password = Room.Settings.Password, + Password = password.GetOr(Room.Settings.Password), BeatmapID = item.GetOr(existingPlaylistItem).BeatmapID, BeatmapChecksum = item.GetOr(existingPlaylistItem).Beatmap.Value.MD5Hash, RulesetID = item.GetOr(existingPlaylistItem).RulesetID, diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs index bc2f263a68..fceb124e0a 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs @@ -303,7 +303,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match // Otherwise, update the room directly in preparation for it to be submitted to the API on match creation. if (client.Room != null) { - client.ChangeSettings(name: NameField.Text).ContinueWith(t => Schedule(() => + client.ChangeSettings(name: NameField.Text, password: PasswordTextBox.Text).ContinueWith(t => Schedule(() => { if (t.IsCompletedSuccessfully) onSuccess(currentRoom.Value); From f35d55c32f3805fe87e54b5c094c25be4975d154 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 10 Jul 2021 14:14:22 +0900 Subject: [PATCH 015/106] Fix `HasPassword` not being in sync with `Password` value for client-side rooms --- osu.Game/Online/Rooms/Room.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game/Online/Rooms/Room.cs b/osu.Game/Online/Rooms/Room.cs index d1701d544e..416dc7e5c0 100644 --- a/osu.Game/Online/Rooms/Room.cs +++ b/osu.Game/Online/Rooms/Room.cs @@ -126,6 +126,11 @@ namespace osu.Game.Online.Rooms [JsonIgnore] public readonly Bindable Position = new Bindable(-1); + public Room() + { + Password.BindValueChanged(p => HasPassword.Value = !string.IsNullOrEmpty(p.NewValue)); + } + /// /// Create a copy of this room without online information. /// Should be used to create a local copy of a room for submitting in the future. From 3c49b46c5fdfedd20691c307a3797529b3c18ed6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 10 Jul 2021 14:14:58 +0900 Subject: [PATCH 016/106] Add lock overlay for rooms which are password protected --- .../TestSceneLoungeRoomsContainer.cs | 6 +++ .../Lounge/Components/DrawableRoom.cs | 43 ++++++++++++++++++- 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs index 75cc687ee8..798748b4df 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs @@ -123,6 +123,12 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("3 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 3); } + [Test] + public void TestPasswordProtectedRooms() + { + AddStep("add rooms", () => RoomManager.AddRooms(3, withPassword: true)); + } + private bool checkRoomSelected(Room room) => SelectedRoom.Value == room; private void joinRequested(Room room) => room.Status.Value = new JoinedRoomStatus(); diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs index 35782c6104..6fec872a11 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs @@ -12,6 +12,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; @@ -46,6 +47,8 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components [Resolved] private BeatmapManager beatmaps { get; set; } + private Container content; + public readonly Room Room; private SelectionState state; @@ -124,7 +127,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components { RelativeSizeAxes = Axes.Both, Padding = new MarginPadding(SELECTION_BORDER_WIDTH), - Child = new Container + Child = content = new Container { RelativeSizeAxes = Axes.Both, Masking = true, @@ -204,6 +207,11 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components }, }, }; + + if (Room.HasPassword.Value) + { + content.Add(new PasswordProtectedIcon()); + } } protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) @@ -245,5 +253,38 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components parentScreen?.OpenNewRoom(Room.CreateCopy()); }) }; + + private class PasswordProtectedIcon : CompositeDrawable + { + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + Anchor = Anchor.TopRight; + Origin = Anchor.TopRight; + + Size = new Vector2(32); + + InternalChildren = new Drawable[] + { + new Box + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopCentre, + Colour = colours.Gray5, + Rotation = 45, + RelativeSizeAxes = Axes.Both, + Width = 2, + }, + new SpriteIcon + { + Icon = FontAwesome.Solid.Lock, + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Margin = new MarginPadding(6), + Size = new Vector2(14), + } + }; + } + } } } From 9f9d7f9125c61fde650d6dff79c031da148733d4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 10 Jul 2021 16:08:12 +0900 Subject: [PATCH 017/106] Add remaining pieces of password flow (for osu-web join request) --- .../TestScenePlaylistsMatchSettingsOverlay.cs | 2 +- osu.Game/Online/Multiplayer/MultiplayerClient.cs | 5 +++-- osu.Game/Online/Rooms/JoinRoomRequest.cs | 2 +- osu.Game/Screens/OnlinePlay/Components/RoomManager.cs | 5 ++++- osu.Game/Screens/OnlinePlay/IRoomManager.cs | 7 +++++-- osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs | 4 ++-- .../OnlinePlay/Multiplayer/MultiplayerRoomManager.cs | 10 +++++----- .../Tests/Visual/OnlinePlay/BasicTestRoomManager.cs | 2 +- 8 files changed, 22 insertions(+), 15 deletions(-) diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs index a320cb240f..cdc655500d 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs @@ -152,7 +152,7 @@ namespace osu.Game.Tests.Visual.Playlists onSuccess?.Invoke(room); } - public void JoinRoom(Room room, Action onSuccess = null, Action onError = null) => throw new NotImplementedException(); + public void JoinRoom(Room room, string password, Action onSuccess = null, Action onError = null) => throw new NotImplementedException(); public void PartRoom() => throw new NotImplementedException(); } diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 052489ca97..42d436ef11 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -115,7 +115,8 @@ namespace osu.Game.Online.Multiplayer /// Joins the for a given API . /// /// The API . - public async Task JoinRoom(Room room) + /// An optional password to use for the join operation. + public async Task JoinRoom(Room room, string? password = null) { var cancellationSource = joinCancellationSource = new CancellationTokenSource(); @@ -127,7 +128,7 @@ namespace osu.Game.Online.Multiplayer Debug.Assert(room.RoomID.Value != null); // Join the server-side room. - var joinedRoom = await JoinRoom(room.RoomID.Value.Value, room.Password.Value).ConfigureAwait(false); + var joinedRoom = await JoinRoom(room.RoomID.Value.Value, password ?? room.Password.Value).ConfigureAwait(false); Debug.Assert(joinedRoom != null); // Populate users. diff --git a/osu.Game/Online/Rooms/JoinRoomRequest.cs b/osu.Game/Online/Rooms/JoinRoomRequest.cs index faa20a3e6c..a82dc5a859 100644 --- a/osu.Game/Online/Rooms/JoinRoomRequest.cs +++ b/osu.Game/Online/Rooms/JoinRoomRequest.cs @@ -23,6 +23,6 @@ namespace osu.Game.Online.Rooms return req; } - protected override string Target => $"rooms/{room.RoomID.Value}/users/{User.Id}"; + protected override string Target => $"rooms/{room.RoomID.Value}/users/{User.Id}?password={room.Password.Value}"; } } diff --git a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs b/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs index 227a772b2d..da02f9624f 100644 --- a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs +++ b/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs @@ -84,8 +84,11 @@ namespace osu.Game.Screens.OnlinePlay.Components private JoinRoomRequest currentJoinRoomRequest; - public virtual void JoinRoom(Room room, Action onSuccess = null, Action onError = null) + public virtual void JoinRoom(Room room, string password = null, Action onSuccess = null, Action onError = null) { + // todo: send into JoinRoomRequest directly? + room.Password.Value = password; + currentJoinRoomRequest?.Cancel(); currentJoinRoomRequest = new JoinRoomRequest(room); diff --git a/osu.Game/Screens/OnlinePlay/IRoomManager.cs b/osu.Game/Screens/OnlinePlay/IRoomManager.cs index 8ff02536f3..34c1393ff1 100644 --- a/osu.Game/Screens/OnlinePlay/IRoomManager.cs +++ b/osu.Game/Screens/OnlinePlay/IRoomManager.cs @@ -6,6 +6,8 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Online.Rooms; +#nullable enable + namespace osu.Game.Screens.OnlinePlay { [Cached(typeof(IRoomManager))] @@ -32,15 +34,16 @@ namespace osu.Game.Screens.OnlinePlay /// The to create. /// An action to be invoked if the creation succeeds. /// An action to be invoked if an error occurred. - void CreateRoom(Room room, Action onSuccess = null, Action onError = null); + void CreateRoom(Room room, Action? onSuccess = null, Action? onError = null); /// /// Joins a . /// /// The to join. must be populated. + /// An optional password to use for the join operation. /// /// - void JoinRoom(Room room, Action onSuccess = null, Action onError = null); + void JoinRoom(Room room, string? password = null, Action? onSuccess = null, Action? onError = null); /// /// Parts the currently-joined . diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs index f24577a8a5..a8b80655f9 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs @@ -167,14 +167,14 @@ namespace osu.Game.Screens.OnlinePlay.Lounge filter.HoldFocus = false; } - private void joinRequested(Room room) + public void Join(Room room, string password) { if (joiningRoomOperation != null) return; joiningRoomOperation = ongoingOperationTracker?.BeginOperation(); - RoomManager?.JoinRoom(room, r => + RoomManager?.JoinRoom(room, password, r => { Open(room); joiningRoomOperation?.Dispose(); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomManager.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomManager.cs index 8526196902..cbba4babe5 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomManager.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomManager.cs @@ -38,9 +38,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer } public override void CreateRoom(Room room, Action onSuccess = null, Action onError = null) - => base.CreateRoom(room, r => joinMultiplayerRoom(r, onSuccess, onError), onError); + => base.CreateRoom(room, r => joinMultiplayerRoom(r, r.Password.Value, onSuccess, onError), onError); - public override void JoinRoom(Room room, Action onSuccess = null, Action onError = null) + public override void JoinRoom(Room room, string password = null, Action onSuccess = null, Action onError = null) { if (!multiplayerClient.IsConnected.Value) { @@ -56,7 +56,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer return; } - base.JoinRoom(room, r => joinMultiplayerRoom(r, onSuccess, onError), onError); + base.JoinRoom(room, password, r => joinMultiplayerRoom(r, password, onSuccess, onError), onError); } public override void PartRoom() @@ -79,11 +79,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer }); } - private void joinMultiplayerRoom(Room room, Action onSuccess = null, Action onError = null) + private void joinMultiplayerRoom(Room room, string password, Action onSuccess = null, Action onError = null) { Debug.Assert(room.RoomID.Value != null); - multiplayerClient.JoinRoom(room).ContinueWith(t => + multiplayerClient.JoinRoom(room, password).ContinueWith(t => { if (t.IsCompletedSuccessfully) Schedule(() => onSuccess?.Invoke(room)); diff --git a/osu.Game/Tests/Visual/OnlinePlay/BasicTestRoomManager.cs b/osu.Game/Tests/Visual/OnlinePlay/BasicTestRoomManager.cs index f2ca3d6357..5d3fa34ec5 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/BasicTestRoomManager.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/BasicTestRoomManager.cs @@ -37,7 +37,7 @@ namespace osu.Game.Tests.Visual.OnlinePlay onSuccess?.Invoke(room); } - public void JoinRoom(Room room, Action onSuccess = null, Action onError = null) => onSuccess?.Invoke(room); + public void JoinRoom(Room room, string password, Action onSuccess = null, Action onError = null) => onSuccess?.Invoke(room); public void PartRoom() { From e25b3518dc0988149a48ac4b8a91cddc644606c3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 11 Jul 2021 10:13:57 +0900 Subject: [PATCH 018/106] Make password popover display inside `RoomsContainer` rooms --- .../Lounge/Components/DrawableRoom.cs | 100 +++++++++++++++++- .../Lounge/Components/RoomsContainer.cs | 49 +++------ .../OnlinePlay/Lounge/LoungeSubScreen.cs | 2 +- 3 files changed, 114 insertions(+), 37 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs index 6fec872a11..bc860772b7 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs @@ -14,12 +14,15 @@ using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input.Bindings; +using osu.Framework.Input.Events; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; +using osu.Game.Input.Bindings; using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Components; using osuTK; @@ -27,7 +30,7 @@ using osuTK.Graphics; namespace osu.Game.Screens.OnlinePlay.Lounge.Components { - public class DrawableRoom : OsuClickableContainer, IStateful, IFilterable, IHasContextMenu + public class DrawableRoom : OsuClickableContainer, IStateful, IFilterable, IHasContextMenu, IHasPopover, IKeyBindingHandler { public const float SELECTION_BORDER_WIDTH = 4; private const float corner_radius = 5; @@ -47,6 +50,12 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components [Resolved] private BeatmapManager beatmaps { get; set; } + [Resolved] + private Bindable selectedRoom { get; set; } + + [Resolved(canBeNull: true)] + private LoungeSubScreen lounge { get; set; } + private Container content; public readonly Room Room; @@ -232,6 +241,22 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components Alpha = 0; } + public bool OnPressed(GlobalAction action) + { + switch (action) + { + case GlobalAction.Select: + // TODO: this needs to be able to show the popover on demand. + return true; + } + + return false; + } + + public void OnReleased(GlobalAction action) + { + } + protected override bool ShouldBeConsideredForInput(Drawable child) => state == SelectionState.Selected; private class RoomName : OsuSpriteText @@ -286,5 +311,78 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components }; } } + + protected override bool OnMouseDown(MouseDownEvent e) + { + if (selectedRoom.Value != Room) + return true; + + return base.OnMouseDown(e); + } + + protected override bool OnClick(ClickEvent e) + { + if (Room != selectedRoom.Value) + { + selectedRoom.Value = Room; + return true; + } + + if (Room.HasPassword.Value) + // we want our popover to show. this is a bit of a hack. + return false; + + lounge?.Join(Room, null); + + return base.OnClick(e); + } + + public Popover GetPopover() => new PasswordEntryPopover(Room); + + public class PasswordEntryPopover : Popover + { + [Resolved(canBeNull: true)] + private LoungeSubScreen lounge { get; set; } + + public PasswordEntryPopover(Room room) + { + OsuPasswordTextBox passwordTextbox; + + Child = new Container + { + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + Colour = Color4.OliveDrab, + RelativeSizeAxes = Axes.Both, + }, + new FillFlowContainer + { + Margin = new MarginPadding(10), + Spacing = new Vector2(5), + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Children = new Drawable[] + { + passwordTextbox = new OsuPasswordTextBox + { + Width = 200, + }, + new TriangleButton + { + Width = 80, + Text = "Join Room", + Action = () => lounge?.Join(room, passwordTextbox.Text) + } + } + }, + } + }; + } + + protected override Drawable CreateArrow() => Drawable.Empty(); + } } } diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs index 5f135a3e90..5a9721a8e3 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs @@ -10,6 +10,7 @@ using osu.Framework.Bindables; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Threading; @@ -24,8 +25,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components { public class RoomsContainer : CompositeDrawable, IKeyBindingHandler { - public Action JoinRequested; - private readonly IBindableList rooms = new BindableList(); private readonly FillFlowContainer roomFlow; @@ -51,16 +50,21 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; - InternalChild = new OsuContextMenuContainer + InternalChild = new PopoverContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Child = roomFlow = new FillFlowContainer + Child = new OsuContextMenuContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Spacing = new Vector2(2), + Child = roomFlow = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(2), + } } }; } @@ -121,19 +125,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components { foreach (var room in rooms) { - roomFlow.Add(new DrawableRoom(room) - { - Action = () => - { - if (room == selectedRoom.Value) - { - joinSelected(); - return; - } - - selectRoom(room); - } - }); + roomFlow.Add(new DrawableRoom(room)); } Filter(filter?.Value); @@ -150,7 +142,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components roomFlow.Remove(toRemove); - selectRoom(null); + selectedRoom.Value = null; } } @@ -160,18 +152,9 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components roomFlow.SetLayoutPosition(room, room.Room.Position.Value); } - private void selectRoom(Room room) => selectedRoom.Value = room; - - private void joinSelected() - { - if (selectedRoom.Value == null) return; - - JoinRequested?.Invoke(selectedRoom.Value); - } - protected override bool OnClick(ClickEvent e) { - selectRoom(null); + selectedRoom.Value = null; return base.OnClick(e); } @@ -181,10 +164,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components { switch (action) { - case GlobalAction.Select: - joinSelected(); - return true; - case GlobalAction.SelectNext: beginRepeatSelection(() => selectNext(1), action); return true; @@ -253,7 +232,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components // we already have a valid selection only change selection if we still have a room to switch to. if (room != null) - selectRoom(room); + selectedRoom.Value = room; } #endregion diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs index a8b80655f9..11298037d0 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs @@ -70,7 +70,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge RelativeSizeAxes = Axes.Both, ScrollbarOverlapsContent = false, Padding = new MarginPadding(10), - Child = roomsContainer = new RoomsContainer { JoinRequested = joinRequested } + Child = roomsContainer = new RoomsContainer() }, loadingLayer = new LoadingLayer(true), } From a3e0168a46d5f2ff87f08ed465ac94178256b84b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 12 Jul 2021 14:35:08 +0900 Subject: [PATCH 019/106] Update tests --- .../TestSceneLoungeRoomsContainer.cs | 20 +------------------ .../TestScenePlaylistsLoungeSubScreen.cs | 2 +- .../Lounge/Components/DrawableRoom.cs | 2 +- 3 files changed, 3 insertions(+), 21 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs index 798748b4df..4d5bf8f225 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs @@ -4,13 +4,11 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Graphics; -using osu.Game.Graphics; using osu.Game.Online.Rooms; using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Osu; using osu.Game.Screens.OnlinePlay.Lounge.Components; using osu.Game.Tests.Visual.OnlinePlay; -using osuTK.Graphics; using osuTK.Input; namespace osu.Game.Tests.Visual.Multiplayer @@ -29,7 +27,6 @@ namespace osu.Game.Tests.Visual.Multiplayer Anchor = Anchor.Centre, Origin = Anchor.Centre, Width = 0.5f, - JoinRequested = joinRequested }; }); @@ -43,11 +40,8 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("has 2 rooms", () => container.Rooms.Count == 2); AddAssert("first room removed", () => container.Rooms.All(r => r.Room.RoomID.Value != 0)); - AddStep("select first room", () => container.Rooms.First().Action?.Invoke()); + AddStep("select first room", () => container.Rooms.First().Click()); AddAssert("first room selected", () => checkRoomSelected(RoomManager.Rooms.First())); - - AddStep("join first room", () => container.Rooms.First().Action?.Invoke()); - AddAssert("first room joined", () => RoomManager.Rooms.First().Status.Value is JoinedRoomStatus); } [Test] @@ -66,9 +60,6 @@ namespace osu.Game.Tests.Visual.Multiplayer press(Key.Down); press(Key.Down); AddAssert("last room selected", () => checkRoomSelected(RoomManager.Rooms.Last())); - - press(Key.Enter); - AddAssert("last room joined", () => RoomManager.Rooms.Last().Status.Value is JoinedRoomStatus); } [Test] @@ -130,14 +121,5 @@ namespace osu.Game.Tests.Visual.Multiplayer } private bool checkRoomSelected(Room room) => SelectedRoom.Value == room; - - private void joinRequested(Room room) => room.Status.Value = new JoinedRoomStatus(); - - private class JoinedRoomStatus : RoomStatus - { - public override string Message => "Joined"; - - public override Color4 GetAppropriateColour(OsuColour colours) => colours.Yellow; - } } } diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs index b16b61c5c7..7bf161d1d0 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs @@ -37,7 +37,7 @@ namespace osu.Game.Tests.Visual.Playlists AddUntilStep("first room is not masked", () => checkRoomVisible(roomsContainer.Rooms.First())); - AddStep("select last room", () => roomsContainer.Rooms.Last().Action?.Invoke()); + AddStep("select last room", () => roomsContainer.Rooms.Last().Click()); AddUntilStep("first room is masked", () => !checkRoomVisible(roomsContainer.Rooms.First())); AddUntilStep("last room is not masked", () => checkRoomVisible(roomsContainer.Rooms.Last())); diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs index bc860772b7..678a97a04c 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs @@ -50,7 +50,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components [Resolved] private BeatmapManager beatmaps { get; set; } - [Resolved] + [Resolved(canBeNull: true)] private Bindable selectedRoom { get; set; } [Resolved(canBeNull: true)] From b4ca6b6188d90c4db0436a19ec57364b6942ac41 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 12 Jul 2021 14:53:12 +0900 Subject: [PATCH 020/106] Update popover logic to take advantage of new explicit popup functionality --- .../Lounge/Components/DrawableRoom.cs | 78 ++++++++++--------- 1 file changed, 42 insertions(+), 36 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs index 678a97a04c..1747dc6565 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -241,12 +242,25 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components Alpha = 0; } + public Popover GetPopover() => new PasswordEntryPopover(Room); + + public MenuItem[] ContextMenuItems => new MenuItem[] + { + new OsuMenuItem("Create copy", MenuItemType.Standard, () => + { + parentScreen?.OpenNewRoom(Room.CreateCopy()); + }) + }; + public bool OnPressed(GlobalAction action) { + if (selectedRoom.Value != Room) + return false; + switch (action) { case GlobalAction.Select: - // TODO: this needs to be able to show the popover on demand. + Click(); return true; } @@ -259,6 +273,33 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components protected override bool ShouldBeConsideredForInput(Drawable child) => state == SelectionState.Selected; + protected override bool OnMouseDown(MouseDownEvent e) + { + if (selectedRoom.Value != Room) + return true; + + return base.OnMouseDown(e); + } + + protected override bool OnClick(ClickEvent e) + { + if (Room != selectedRoom.Value) + { + selectedRoom.Value = Room; + return true; + } + + if (Room.HasPassword.Value) + { + this.ShowPopover(); + return true; + } + + lounge?.Join(Room, null); + + return base.OnClick(e); + } + private class RoomName : OsuSpriteText { [Resolved(typeof(Room), nameof(Online.Rooms.Room.Name))] @@ -271,14 +312,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components } } - public MenuItem[] ContextMenuItems => new MenuItem[] - { - new OsuMenuItem("Create copy", MenuItemType.Standard, () => - { - parentScreen?.OpenNewRoom(Room.CreateCopy()); - }) - }; - private class PasswordProtectedIcon : CompositeDrawable { [BackgroundDependencyLoader] @@ -312,33 +345,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components } } - protected override bool OnMouseDown(MouseDownEvent e) - { - if (selectedRoom.Value != Room) - return true; - - return base.OnMouseDown(e); - } - - protected override bool OnClick(ClickEvent e) - { - if (Room != selectedRoom.Value) - { - selectedRoom.Value = Room; - return true; - } - - if (Room.HasPassword.Value) - // we want our popover to show. this is a bit of a hack. - return false; - - lounge?.Join(Room, null); - - return base.OnClick(e); - } - - public Popover GetPopover() => new PasswordEntryPopover(Room); - public class PasswordEntryPopover : Popover { [Resolved(canBeNull: true)] From 947460c3c54d46900385f3321ecfae0d8c55a855 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 12 Jul 2021 15:49:09 +0900 Subject: [PATCH 021/106] Add test flow for joining passworded rooms via UI --- .../TestSceneMultiplayerLoungeSubScreen.cs | 35 ++++++++++++++++++- .../Visual/OnlinePlay/BasicTestRoomManager.cs | 8 ++++- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs index 0d90b75be7..3ba0f4969a 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs @@ -3,12 +3,16 @@ using System.Linq; using NUnit.Framework; +using osu.Framework.Graphics.UserInterface; using osu.Framework.Screens; using osu.Framework.Testing; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Lounge; using osu.Game.Screens.OnlinePlay.Lounge.Components; using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Tests.Visual.OnlinePlay; +using osuTK.Input; namespace osu.Game.Tests.Visual.Multiplayer { @@ -18,6 +22,9 @@ namespace osu.Game.Tests.Visual.Multiplayer private LoungeSubScreen loungeScreen; + private Room lastJoinedRoom; + private string lastJoinedPassword; + public override void SetUpSteps() { base.SetUpSteps(); @@ -25,20 +32,46 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("push screen", () => LoadScreen(loungeScreen = new MultiplayerLoungeSubScreen())); AddUntilStep("wait for present", () => loungeScreen.IsCurrentScreen()); + + AddStep("bind to event", () => + { + lastJoinedRoom = null; + lastJoinedPassword = null; + RoomManager.JoinRoomRequested = onRoomJoined; + }); } [Test] public void TestJoinRoomWithoutPassword() { AddStep("add room", () => RoomManager.AddRooms(1, withPassword: false)); + AddStep("select room", () => InputManager.Key(Key.Down)); + AddStep("join room", () => InputManager.Key(Key.Enter)); + + AddAssert("room join requested", () => lastJoinedRoom == RoomManager.Rooms.First()); + AddAssert("room join password correct", () => lastJoinedPassword == null); } [Test] public void TestJoinRoomWithPassword() { + DrawableRoom.PasswordEntryPopover passwordEntryPopover = null; + AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true)); + AddStep("select room", () => InputManager.Key(Key.Down)); + AddStep("attempt join room", () => InputManager.Key(Key.Enter)); + AddUntilStep("password prompt appeared", () => (passwordEntryPopover = loungeScreen.ChildrenOfType().FirstOrDefault()) != null); + AddStep("enter password in text box", () => passwordEntryPopover.ChildrenOfType().First().Text = "password"); + AddStep("press join room button", () => passwordEntryPopover.ChildrenOfType().First().Click()); + + AddAssert("room join requested", () => lastJoinedRoom == RoomManager.Rooms.First()); + AddAssert("room join password correct", () => lastJoinedPassword == "password"); } - private RoomsContainer roomsContainer => loungeScreen.ChildrenOfType().First(); + private void onRoomJoined(Room room, string password) + { + lastJoinedRoom = room; + lastJoinedPassword = password; + } } } diff --git a/osu.Game/Tests/Visual/OnlinePlay/BasicTestRoomManager.cs b/osu.Game/Tests/Visual/OnlinePlay/BasicTestRoomManager.cs index 5d3fa34ec5..82c7266598 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/BasicTestRoomManager.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/BasicTestRoomManager.cs @@ -26,6 +26,8 @@ namespace osu.Game.Tests.Visual.OnlinePlay public readonly BindableList Rooms = new BindableList(); + public Action JoinRoomRequested; + public IBindable InitialRoomsReceived { get; } = new Bindable(true); IBindableList IRoomManager.Rooms => Rooms; @@ -37,7 +39,11 @@ namespace osu.Game.Tests.Visual.OnlinePlay onSuccess?.Invoke(room); } - public void JoinRoom(Room room, string password, Action onSuccess = null, Action onError = null) => onSuccess?.Invoke(room); + public void JoinRoom(Room room, string password, Action onSuccess = null, Action onError = null) + { + JoinRoomRequested?.Invoke(room, password); + onSuccess?.Invoke(room); + } public void PartRoom() { From 413f8adb36e36115deb020160acee13b8a1e6ec7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 12 Jul 2021 15:53:28 +0900 Subject: [PATCH 022/106] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 9280eaf97c..f9ec8dd099 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,7 +52,7 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 8a3c69e40c..8f9a57167f 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/osu.iOS.props b/osu.iOS.props index 2eea646c61..c6e52b8dd5 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -93,7 +93,7 @@ - + From bbc3a013c888596a8e4501033c9bd813112735bc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 12 Jul 2021 18:29:50 +0900 Subject: [PATCH 023/106] Use `BasicPopover` for now --- .../Lounge/Components/DrawableRoom.cs | 46 ++++++++----------- 1 file changed, 20 insertions(+), 26 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs index 1747dc6565..a95e0c2b16 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs @@ -345,50 +345,44 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components } } - public class PasswordEntryPopover : Popover + public class PasswordEntryPopover : BasicPopover { + private readonly Room room; + [Resolved(canBeNull: true)] private LoungeSubScreen lounge { get; set; } public PasswordEntryPopover(Room room) + { + this.room = room; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) { OsuPasswordTextBox passwordTextbox; - Child = new Container + Child = new FillFlowContainer { + Margin = new MarginPadding(10), + Spacing = new Vector2(5), AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, Children = new Drawable[] { - new Box + passwordTextbox = new OsuPasswordTextBox { - Colour = Color4.OliveDrab, - RelativeSizeAxes = Axes.Both, + Width = 200, }, - new FillFlowContainer + new TriangleButton { - Margin = new MarginPadding(10), - Spacing = new Vector2(5), - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Children = new Drawable[] - { - passwordTextbox = new OsuPasswordTextBox - { - Width = 200, - }, - new TriangleButton - { - Width = 80, - Text = "Join Room", - Action = () => lounge?.Join(room, passwordTextbox.Text) - } - } - }, + Width = 80, + Text = "Join Room", + Action = () => lounge?.Join(room, passwordTextbox.Text) + } } }; } - - protected override Drawable CreateArrow() => Drawable.Empty(); } } } From c5319c06c2e29ddc1f1903ad48b1ecd7e832224b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 12 Jul 2021 18:54:07 +0900 Subject: [PATCH 024/106] Add password attributes to `CopyFrom` to make testing work better --- osu.Game/Online/Rooms/Room.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Online/Rooms/Room.cs b/osu.Game/Online/Rooms/Room.cs index 416dc7e5c0..f79a410bd9 100644 --- a/osu.Game/Online/Rooms/Room.cs +++ b/osu.Game/Online/Rooms/Room.cs @@ -159,6 +159,8 @@ namespace osu.Game.Online.Rooms ChannelId.Value = other.ChannelId.Value; Status.Value = other.Status.Value; Availability.Value = other.Availability.Value; + Password.Value = other.Password.Value; + HasPassword.Value = other.HasPassword.Value; Type.Value = other.Type.Value; MaxParticipants.Value = other.MaxParticipants.Value; ParticipantCount.Value = other.ParticipantCount.Value; From 4dea2d97782151a1a8e383cd09cd225662b99d15 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 12 Jul 2021 18:54:17 +0900 Subject: [PATCH 025/106] Dismiss popovers on returning to lounge --- .../Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs | 7 +++++++ osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs | 6 +++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs index 5a9721a8e3..8ab80d947c 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs @@ -7,6 +7,7 @@ using System.Collections.Specialized; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -244,5 +245,11 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components if (roomManager != null) roomManager.RoomsUpdated -= updateSorting; } + + public void HideAnyPopovers() + { + // must be called on a child of the PopoverContainer due to parent traversal not considering self. + roomFlow.HidePopover(); + } } } diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs index 11298037d0..dd6106b868 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs @@ -46,10 +46,11 @@ namespace osu.Game.Screens.OnlinePlay.Lounge [CanBeNull] private IDisposable joiningRoomOperation { get; set; } + private RoomsContainer roomsContainer; + [BackgroundDependencyLoader] private void load() { - RoomsContainer roomsContainer; OsuScrollContainer scrollContainer; InternalChildren = new Drawable[] @@ -165,6 +166,9 @@ namespace osu.Game.Screens.OnlinePlay.Lounge { base.OnSuspending(next); filter.HoldFocus = false; + + // ensure any password prompt is dismissed. + roomsContainer.HideAnyPopovers(); } public void Join(Room room, string password) From 125bd36ab161f683811d93d434ecbbcddc12f6ac Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Jul 2021 14:27:07 +0900 Subject: [PATCH 026/106] Send password in request ctor directly --- osu.Game/Online/Rooms/JoinRoomRequest.cs | 7 +++++-- osu.Game/Screens/OnlinePlay/Components/RoomManager.cs | 5 +---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game/Online/Rooms/JoinRoomRequest.cs b/osu.Game/Online/Rooms/JoinRoomRequest.cs index a82dc5a859..53bd2a6106 100644 --- a/osu.Game/Online/Rooms/JoinRoomRequest.cs +++ b/osu.Game/Online/Rooms/JoinRoomRequest.cs @@ -10,19 +10,22 @@ namespace osu.Game.Online.Rooms public class JoinRoomRequest : APIRequest { private readonly Room room; + private readonly string password; - public JoinRoomRequest(Room room) + public JoinRoomRequest(Room room, string password) { this.room = room; + this.password = password; } protected override WebRequest CreateWebRequest() { var req = base.CreateWebRequest(); req.Method = HttpMethod.Put; + req.AddParameter("password", password); return req; } - protected override string Target => $"rooms/{room.RoomID.Value}/users/{User.Id}?password={room.Password.Value}"; + protected override string Target => $"rooms/{room.RoomID.Value}/users/{User.Id}"; } } diff --git a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs b/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs index da02f9624f..422576648c 100644 --- a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs +++ b/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs @@ -86,11 +86,8 @@ namespace osu.Game.Screens.OnlinePlay.Components public virtual void JoinRoom(Room room, string password = null, Action onSuccess = null, Action onError = null) { - // todo: send into JoinRoomRequest directly? - room.Password.Value = password; - currentJoinRoomRequest?.Cancel(); - currentJoinRoomRequest = new JoinRoomRequest(room); + currentJoinRoomRequest = new JoinRoomRequest(room, password); currentJoinRoomRequest.Success += () => { From 6409a518dbba46a9f5f30f88f6b560442c9daa9c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Jul 2021 16:34:56 +0900 Subject: [PATCH 027/106] Focus password text box on popover display --- .../OnlinePlay/Lounge/Components/DrawableRoom.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs index a95e0c2b16..b8c4b57378 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs @@ -357,11 +357,11 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components this.room = room; } + private OsuPasswordTextBox passwordTextbox; + [BackgroundDependencyLoader] private void load(OsuColour colours) { - OsuPasswordTextBox passwordTextbox; - Child = new FillFlowContainer { Margin = new MarginPadding(10), @@ -383,6 +383,13 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components } }; } + + protected override void LoadComplete() + { + base.LoadComplete(); + + GetContainingInputManager().ChangeFocus(passwordTextbox); + } } } } From 481e4dedb0d7c49e0a2470af5645ceba3cb2d521 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Jul 2021 16:35:49 +0900 Subject: [PATCH 028/106] Move `PopoverContainer` to `OsuGameBase` --- osu.Game/OsuGameBase.cs | 7 ++++++- .../OnlinePlay/Lounge/Components/RoomsContainer.cs | 14 ++++---------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 5878727ad8..4b5fa4f62e 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -13,6 +13,7 @@ using osu.Framework.Development; using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Framework.IO.Stores; using osu.Framework.Platform; using osu.Game.Beatmaps; @@ -341,7 +342,11 @@ namespace osu.Game globalBindings = new GlobalActionContainer(this) }; - MenuCursorContainer.Child = content = new OsuTooltipContainer(MenuCursorContainer.Cursor) { RelativeSizeAxes = Axes.Both }; + MenuCursorContainer.Child = new PopoverContainer + { + RelativeSizeAxes = Axes.Both, + Child = content = new OsuTooltipContainer(MenuCursorContainer.Cursor) { RelativeSizeAxes = Axes.Both } + }; base.Content.Add(CreateScalingContainer().WithChildren(mainContent)); diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs index 8ab80d947c..0910ff1a7a 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs @@ -11,7 +11,6 @@ using osu.Framework.Extensions; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Cursor; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Threading; @@ -51,21 +50,16 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; - InternalChild = new PopoverContainer + InternalChild = new OsuContextMenuContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Child = new OsuContextMenuContainer + Child = roomFlow = new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Child = roomFlow = new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Spacing = new Vector2(2), - } + Direction = FillDirection.Vertical, + Spacing = new Vector2(2), } }; } From cc09a8b5badf002dedaa80bd0b57cb94e9abff75 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 14 Jul 2021 23:55:46 +0900 Subject: [PATCH 029/106] Update to use `OsuPopover` implementation --- osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs index b8c4b57378..7f4ea0908d 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs @@ -23,6 +23,7 @@ using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; +using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Input.Bindings; using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Components; @@ -345,7 +346,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components } } - public class PasswordEntryPopover : BasicPopover + public class PasswordEntryPopover : OsuPopover { private readonly Room room; From 9d693c75cfad98bd011bf0f75427f21372994c38 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 14 Jul 2021 23:56:52 +0900 Subject: [PATCH 030/106] Add `Schedule` to restore password text box focus behaviour --- osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs index 7f4ea0908d..84c20a6acc 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs @@ -389,7 +389,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components { base.LoadComplete(); - GetContainingInputManager().ChangeFocus(passwordTextbox); + Schedule(() => GetContainingInputManager().ChangeFocus(passwordTextbox)); } } } From 8cc2d2e79e6679c3c541b6999bc1b8dbdd332f1d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 15 Jul 2021 13:51:28 +0900 Subject: [PATCH 031/106] Update beat synced container tests to be usable --- .../TestSceneBeatSyncedContainer.cs | 82 ++++++++++++------- 1 file changed, 52 insertions(+), 30 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs index 82b7e65c4f..c952163ba5 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs @@ -5,18 +5,18 @@ using System; using System.Collections.Generic; using System.Linq; using NUnit.Framework; -using osu.Framework.Allocation; using osu.Framework.Audio.Track; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Testing; using osu.Framework.Timing; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; -using osu.Game.Overlays; +using osu.Game.Rulesets.Osu; using osuTK.Graphics; namespace osu.Game.Tests.Visual.UserInterface @@ -24,37 +24,55 @@ namespace osu.Game.Tests.Visual.UserInterface [TestFixture] public class TestSceneBeatSyncedContainer : OsuTestScene { - private readonly NowPlayingOverlay np; + private BeatContainer beatContainer; + private DecoupleableInterpolatingFramedClock decoupledClock; - public TestSceneBeatSyncedContainer() + [SetUpSteps] + public void SetUpSteps() { - Clock = new FramedClock(); - Clock.ProcessFrame(); - - AddRange(new Drawable[] + AddStep("Set beatmap", () => { - new BeatContainer - { - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - }, - np = new NowPlayingOverlay - { - Origin = Anchor.TopRight, - Anchor = Anchor.TopRight, - } + Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); }); + + AddStep("Create beat sync container", () => + { + Children = new Drawable[] + { + beatContainer = new BeatContainer + { + Clock = decoupledClock = new DecoupleableInterpolatingFramedClock + { + IsCoupled = false, + }, + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + }, + }; + + decoupledClock.ChangeSource(Beatmap.Value.Track); + }); + + AddStep("Start playback", () => decoupledClock.Start()); } - protected override void LoadComplete() + [Test] + public void TestFirstBeatAtFirstTimingPoint() { - base.LoadComplete(); - np.ToggleVisibility(); + AddStep("Set time before zero", () => + { + decoupledClock.Seek(-1000); + }); + + AddStep("bind event", () => + { + beatContainer.NewBeat = (i, point, effectControlPoint, channelAmplitudes) => { }; + }); } private class BeatContainer : BeatSyncedContainer { - private const int flash_layer_heigth = 150; + private const int flash_layer_height = 150; private readonly InfoString timingPointCount; private readonly InfoString currentTimingPoint; @@ -64,12 +82,10 @@ namespace osu.Game.Tests.Visual.UserInterface private readonly InfoString adjustedBeatLength; private readonly InfoString timeUntilNextBeat; private readonly InfoString timeSinceLastBeat; + private readonly InfoString currentTime; private readonly Box flashLayer; - [Resolved] - private MusicController musicController { get; set; } - public BeatContainer() { RelativeSizeAxes = Axes.X; @@ -82,7 +98,7 @@ namespace osu.Game.Tests.Visual.UserInterface Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, AutoSizeAxes = Axes.Both, - Margin = new MarginPadding { Bottom = flash_layer_heigth }, + Margin = new MarginPadding { Bottom = flash_layer_height }, Children = new Drawable[] { new Box @@ -98,6 +114,7 @@ namespace osu.Game.Tests.Visual.UserInterface Direction = FillDirection.Vertical, Children = new Drawable[] { + currentTime = new InfoString(@"Current time"), timingPointCount = new InfoString(@"Timing points amount"), currentTimingPoint = new InfoString(@"Current timing point"), beatCount = new InfoString(@"Beats amount (in the current timing point)"), @@ -116,7 +133,7 @@ namespace osu.Game.Tests.Visual.UserInterface Anchor = Anchor.BottomCentre, Origin = Anchor.BottomCentre, RelativeSizeAxes = Axes.X, - Height = flash_layer_heigth, + Height = flash_layer_height, Children = new Drawable[] { new Box @@ -164,7 +181,7 @@ namespace osu.Game.Tests.Visual.UserInterface if (timingPoints.Count == 0) return 0; if (timingPoints[^1] == current) - return (int)Math.Ceiling((musicController.CurrentTrack.Length - current.Time) / current.BeatLength); + return (int)Math.Ceiling((Clock.CurrentTime - current.Time) / current.BeatLength); return (int)Math.Ceiling((getNextTimingPoint(current).Time - current.Time) / current.BeatLength); } @@ -174,8 +191,11 @@ namespace osu.Game.Tests.Visual.UserInterface base.Update(); timeUntilNextBeat.Value = TimeUntilNextBeat; timeSinceLastBeat.Value = TimeSinceLastBeat; + currentTime.Value = Clock.CurrentTime; } + public Action NewBeat; + protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes) { base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes); @@ -187,7 +207,9 @@ namespace osu.Game.Tests.Visual.UserInterface beatsPerMinute.Value = 60000 / timingPoint.BeatLength; adjustedBeatLength.Value = timingPoint.BeatLength; - flashLayer.FadeOutFromOne(timingPoint.BeatLength); + flashLayer.FadeOutFromOne(timingPoint.BeatLength / 4); + + NewBeat?.Invoke(beatIndex, timingPoint, effectPoint, amplitudes); } } @@ -200,7 +222,7 @@ namespace osu.Game.Tests.Visual.UserInterface public double Value { - set => valueText.Text = $"{value:G}"; + set => valueText.Text = $"{value:0.##}"; } public InfoString(string header) From a3129ad00e22dc44c9f4965f8ef0702b22ce6a91 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 15 Jul 2021 14:30:38 +0900 Subject: [PATCH 032/106] Refactor `BeatSyncedContainer` to support `GameplayClock` --- .../TestSceneBeatSyncedContainer.cs | 35 ++++++----- .../Containers/BeatSyncedContainer.cs | 62 ++++++++++++++----- 2 files changed, 66 insertions(+), 31 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs index c952163ba5..f58f9fcaaf 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs @@ -11,12 +11,12 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Testing; -using osu.Framework.Timing; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Rulesets.Osu; +using osu.Game.Screens.Play; using osuTK.Graphics; namespace osu.Game.Tests.Visual.UserInterface @@ -25,7 +25,8 @@ namespace osu.Game.Tests.Visual.UserInterface public class TestSceneBeatSyncedContainer : OsuTestScene { private BeatContainer beatContainer; - private DecoupleableInterpolatingFramedClock decoupledClock; + + private MasterGameplayClockContainer gameplayClockContainer; [SetUpSteps] public void SetUpSteps() @@ -39,21 +40,18 @@ namespace osu.Game.Tests.Visual.UserInterface { Children = new Drawable[] { - beatContainer = new BeatContainer + gameplayClockContainer = new MasterGameplayClockContainer(Beatmap.Value, 0) { - Clock = decoupledClock = new DecoupleableInterpolatingFramedClock + Child = beatContainer = new BeatContainer { - IsCoupled = false, + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, }, - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - }, + } }; - - decoupledClock.ChangeSource(Beatmap.Value.Track); }); - AddStep("Start playback", () => decoupledClock.Start()); + AddStep("Start playback", () => gameplayClockContainer.Start()); } [Test] @@ -61,7 +59,7 @@ namespace osu.Game.Tests.Visual.UserInterface { AddStep("Set time before zero", () => { - decoupledClock.Seek(-1000); + gameplayClockContainer.Seek(-1000); }); AddStep("bind event", () => @@ -150,8 +148,13 @@ namespace osu.Game.Tests.Visual.UserInterface } } }; + } - Beatmap.ValueChanged += delegate + protected override void LoadComplete() + { + base.LoadComplete(); + + Beatmap.BindValueChanged(_ => { timingPointCount.Value = 0; currentTimingPoint.Value = 0; @@ -161,7 +164,7 @@ namespace osu.Game.Tests.Visual.UserInterface adjustedBeatLength.Value = 0; timeUntilNextBeat.Value = 0; timeSinceLastBeat.Value = 0; - }; + }, true); } private List timingPoints => Beatmap.Value.Beatmap.ControlPointInfo.TimingPoints.ToList(); @@ -181,7 +184,7 @@ namespace osu.Game.Tests.Visual.UserInterface if (timingPoints.Count == 0) return 0; if (timingPoints[^1] == current) - return (int)Math.Ceiling((Clock.CurrentTime - current.Time) / current.BeatLength); + return (int)Math.Ceiling((BeatSyncClock.CurrentTime - current.Time) / current.BeatLength); return (int)Math.Ceiling((getNextTimingPoint(current).Time - current.Time) / current.BeatLength); } @@ -191,7 +194,7 @@ namespace osu.Game.Tests.Visual.UserInterface base.Update(); timeUntilNextBeat.Value = TimeUntilNextBeat; timeSinceLastBeat.Value = TimeSinceLastBeat; - currentTime.Value = Clock.CurrentTime; + currentTime.Value = BeatSyncClock.CurrentTime; } public Action NewBeat; diff --git a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs index e2a0c09a6b..cb26406d64 100644 --- a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs +++ b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs @@ -1,19 +1,29 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Diagnostics; using osu.Framework.Allocation; using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Graphics.Containers; +using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Screens.Play; namespace osu.Game.Graphics.Containers { + /// + /// A container which fires a callback when a new beat is reached. + /// Consumes a parent or (whichever is first available). + /// + /// + /// This container does not set its own clock to the source used for beat matching. + /// This means that if the beat source clock is playing faster or slower, animations may unexpectedly overlap. + /// Make sure this container's Clock is also set to the expected source (or within a parent element which provides this). + /// public class BeatSyncedContainer : Container { - protected readonly IBindable Beatmap = new Bindable(); - private int lastBeat; private TimingControlPoint lastTimingPoint; @@ -45,15 +55,45 @@ namespace osu.Game.Graphics.Containers protected bool IsBeatSyncedWithTrack { get; private set; } + protected virtual void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes) + { + } + + [Resolved] + protected IBindable Beatmap { get; private set; } + + [Resolved(canBeNull: true)] + protected GameplayClock GameplayClock { get; private set; } + + protected IClock BeatSyncClock + { + get + { + if (GameplayClock != null) + return GameplayClock; + + if (Beatmap.Value.TrackLoaded) + return Beatmap.Value.Track; + + return null; + } + } + protected override void Update() { ITrack track = null; IBeatmap beatmap = null; double currentTrackTime = 0; + TimingControlPoint timingPoint = null; EffectControlPoint effectPoint = null; + var clock = BeatSyncClock; + + if (clock == null) + return; + if (Beatmap.Value.TrackLoaded && Beatmap.Value.BeatmapLoaded) { track = Beatmap.Value.Track; @@ -62,7 +102,7 @@ namespace osu.Game.Graphics.Containers if (track != null && beatmap != null && track.IsRunning && track.Length > 0) { - currentTrackTime = track.CurrentTime + EarlyActivationMilliseconds; + currentTrackTime = clock.CurrentTime + EarlyActivationMilliseconds; timingPoint = beatmap.ControlPointInfo.TimingPointAt(currentTrackTime); effectPoint = beatmap.ControlPointInfo.EffectPointAt(currentTrackTime); @@ -70,13 +110,15 @@ namespace osu.Game.Graphics.Containers IsBeatSyncedWithTrack = timingPoint?.BeatLength > 0; - if (timingPoint == null || !IsBeatSyncedWithTrack) + if (!IsBeatSyncedWithTrack) { - currentTrackTime = Clock.CurrentTime; + currentTrackTime = clock.CurrentTime; timingPoint = TimingControlPoint.DEFAULT; effectPoint = EffectControlPoint.DEFAULT; } + Debug.Assert(timingPoint != null); + double beatLength = timingPoint.BeatLength / Divisor; while (beatLength < MinimumBeatLength) @@ -103,15 +145,5 @@ namespace osu.Game.Graphics.Containers lastBeat = beatIndex; lastTimingPoint = timingPoint; } - - [BackgroundDependencyLoader] - private void load(IBindable beatmap) - { - Beatmap.BindTo(beatmap); - } - - protected virtual void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes) - { - } } } From cab8b941322b2731605778a914badf9388e5b67c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 15 Jul 2021 14:36:37 +0900 Subject: [PATCH 033/106] Add failing test --- .../TestSceneBeatSyncedContainer.cs | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs index f58f9fcaaf..ac1f88ad33 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Testing; +using osu.Framework.Utils; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics; using osu.Game.Graphics.Containers; @@ -57,15 +58,28 @@ namespace osu.Game.Tests.Visual.UserInterface [Test] public void TestFirstBeatAtFirstTimingPoint() { - AddStep("Set time before zero", () => - { - gameplayClockContainer.Seek(-1000); - }); + int? lastBeatIndex = null; + double? lastBpm = null; AddStep("bind event", () => { - beatContainer.NewBeat = (i, point, effectControlPoint, channelAmplitudes) => { }; + beatContainer.NewBeat = (i, timingControlPoint, effectControlPoint, channelAmplitudes) => + { + lastBeatIndex = i; + lastBpm = timingControlPoint.BPM; + }; }); + + AddStep("Set time before zero", () => + { + lastBeatIndex = null; + lastBpm = null; + gameplayClockContainer.Seek(-1000); + }); + + AddUntilStep("wait for trigger", () => lastBpm != null); + AddAssert("bpm is from beatmap", () => lastBpm != null&&Precision.AlmostEquals(lastBpm.Value, 128)); + AddAssert("beat index is less than zero", () => lastBeatIndex < 0); } private class BeatContainer : BeatSyncedContainer From 5ecf6511e6543300fdba03197b5ec53583db54be Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 15 Jul 2021 14:37:08 +0900 Subject: [PATCH 034/106] Fix default timing points being used if "track" is not running --- osu.Game/Graphics/Containers/BeatSyncedContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs index cb26406d64..6d5f4d63ac 100644 --- a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs +++ b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs @@ -100,7 +100,7 @@ namespace osu.Game.Graphics.Containers beatmap = Beatmap.Value.Beatmap; } - if (track != null && beatmap != null && track.IsRunning && track.Length > 0) + if (track != null && beatmap != null && clock.IsRunning && track.Length > 0) { currentTrackTime = clock.CurrentTime + EarlyActivationMilliseconds; From c47ff1919c0075cb64b7e1539885f26b6adc012a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 15 Jul 2021 14:56:49 +0900 Subject: [PATCH 035/106] Fix regression in idle behaviour and refactor further --- .../Containers/BeatSyncedContainer.cs | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs index 6d5f4d63ac..929a300831 100644 --- a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs +++ b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs @@ -21,6 +21,8 @@ namespace osu.Game.Graphics.Containers /// This container does not set its own clock to the source used for beat matching. /// This means that if the beat source clock is playing faster or slower, animations may unexpectedly overlap. /// Make sure this container's Clock is also set to the expected source (or within a parent element which provides this). + /// + /// This container will also trigger beat events when the beat matching clock is paused at 's BPM. /// public class BeatSyncedContainer : Container { @@ -53,6 +55,9 @@ namespace osu.Game.Graphics.Containers /// public double MinimumBeatLength { get; set; } + /// + /// Whether this container is currently tracking a beatmap's timing data. + /// protected bool IsBeatSyncedWithTrack { get; private set; } protected virtual void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes) @@ -84,35 +89,36 @@ namespace osu.Game.Graphics.Containers ITrack track = null; IBeatmap beatmap = null; - double currentTrackTime = 0; + TimingControlPoint timingPoint; + EffectControlPoint effectPoint; - TimingControlPoint timingPoint = null; - EffectControlPoint effectPoint = null; - - var clock = BeatSyncClock; + IClock clock = BeatSyncClock; if (clock == null) return; + double currentTrackTime = clock.CurrentTime + EarlyActivationMilliseconds; + if (Beatmap.Value.TrackLoaded && Beatmap.Value.BeatmapLoaded) { track = Beatmap.Value.Track; beatmap = Beatmap.Value.Beatmap; } - if (track != null && beatmap != null && clock.IsRunning && track.Length > 0) + IsBeatSyncedWithTrack = beatmap != null && clock.IsRunning && track?.Length > 0; + + if (IsBeatSyncedWithTrack) { - currentTrackTime = clock.CurrentTime + EarlyActivationMilliseconds; + Debug.Assert(beatmap != null); timingPoint = beatmap.ControlPointInfo.TimingPointAt(currentTrackTime); effectPoint = beatmap.ControlPointInfo.EffectPointAt(currentTrackTime); } - - IsBeatSyncedWithTrack = timingPoint?.BeatLength > 0; - - if (!IsBeatSyncedWithTrack) + else { - currentTrackTime = clock.CurrentTime; + // this may be the case where the beat syncing clock has been paused. + // we still want to show an idle animation, so use this container's time instead. + currentTrackTime = Clock.CurrentTime; timingPoint = TimingControlPoint.DEFAULT; effectPoint = EffectControlPoint.DEFAULT; } From 77bfe700e091fd16af9b27edddfd987ec0094579 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 15 Jul 2021 14:59:57 +0900 Subject: [PATCH 036/106] Add test coverage of idle beat --- .../TestSceneBeatSyncedContainer.cs | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs index ac1f88ad33..3ca6bf782a 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs @@ -78,10 +78,33 @@ namespace osu.Game.Tests.Visual.UserInterface }); AddUntilStep("wait for trigger", () => lastBpm != null); - AddAssert("bpm is from beatmap", () => lastBpm != null&&Precision.AlmostEquals(lastBpm.Value, 128)); + AddAssert("bpm is from beatmap", () => lastBpm != null && Precision.AlmostEquals(lastBpm.Value, 128)); AddAssert("beat index is less than zero", () => lastBeatIndex < 0); } + [Test] + public void TestIdleBeatOnPausedClock() + { + double? lastBpm = null; + + AddStep("bind event", () => + { + beatContainer.NewBeat = (i, timingControlPoint, effectControlPoint, channelAmplitudes) => lastBpm = timingControlPoint.BPM; + }); + + AddUntilStep("wait for trigger", () => lastBpm != null); + AddAssert("bpm is from beatmap", () => lastBpm != null && Precision.AlmostEquals(lastBpm.Value, 128)); + + AddStep("pause gameplay clock", () => + { + lastBpm = null; + gameplayClockContainer.Stop(); + }); + + AddUntilStep("wait for trigger", () => lastBpm != null); + AddAssert("bpm is from beatmap", () => lastBpm != null && Precision.AlmostEquals(lastBpm.Value, 60)); + } + private class BeatContainer : BeatSyncedContainer { private const int flash_layer_height = 150; From 98a1f40a982fe52d6482877c2850ab8cc13ab309 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 15 Jul 2021 15:10:14 +0900 Subject: [PATCH 037/106] Ensure `EarlyActivationMilliseconds` is applied even in idle state --- osu.Game/Graphics/Containers/BeatSyncedContainer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs index 929a300831..55f694f17e 100644 --- a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs +++ b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs @@ -97,7 +97,7 @@ namespace osu.Game.Graphics.Containers if (clock == null) return; - double currentTrackTime = clock.CurrentTime + EarlyActivationMilliseconds; + double currentTrackTime = clock.CurrentTime; if (Beatmap.Value.TrackLoaded && Beatmap.Value.BeatmapLoaded) { @@ -123,7 +123,7 @@ namespace osu.Game.Graphics.Containers effectPoint = EffectControlPoint.DEFAULT; } - Debug.Assert(timingPoint != null); + currentTrackTime += EarlyActivationMilliseconds; double beatLength = timingPoint.BeatLength / Divisor; From d0fc25888683d7fc75e4da27c111acc77ad08364 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 15 Jul 2021 15:11:42 +0900 Subject: [PATCH 038/106] Remove unused `OsuLogo.BeatMatching` --- osu.Game/Screens/Menu/OsuLogo.cs | 4 ---- osu.Game/Screens/OsuScreen.cs | 1 - 2 files changed, 5 deletions(-) diff --git a/osu.Game/Screens/Menu/OsuLogo.cs b/osu.Game/Screens/Menu/OsuLogo.cs index 283be913b0..a9376325cd 100644 --- a/osu.Game/Screens/Menu/OsuLogo.cs +++ b/osu.Game/Screens/Menu/OsuLogo.cs @@ -72,8 +72,6 @@ namespace osu.Game.Screens.Menu set => colourAndTriangles.FadeTo(value ? 1 : 0, transition_length, Easing.OutQuint); } - public bool BeatMatching = true; - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => logoContainer.ReceivePositionalInputAt(screenSpacePos); public bool Ripple @@ -272,8 +270,6 @@ namespace osu.Game.Screens.Menu { base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes); - if (!BeatMatching) return; - lastBeatIndex = beatIndex; var beatLength = timingPoint.BeatLength; diff --git a/osu.Game/Screens/OsuScreen.cs b/osu.Game/Screens/OsuScreen.cs index aeb51813e4..c3b2612e79 100644 --- a/osu.Game/Screens/OsuScreen.cs +++ b/osu.Game/Screens/OsuScreen.cs @@ -242,7 +242,6 @@ namespace osu.Game.Screens logo.Anchor = Anchor.TopLeft; logo.Origin = Anchor.Centre; logo.RelativePositionAxes = Axes.Both; - logo.BeatMatching = true; logo.Triangles = true; logo.Ripple = true; } From 3197f599bb2a9d5a1f516416293d5ac0ab0e636b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 15 Jul 2021 16:01:46 +0900 Subject: [PATCH 039/106] Add failing test showing `OnNewBeat` can execute far away from an actual beat --- .../TestSceneBeatSyncedContainer.cs | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs index 3ca6bf782a..5bfaf35bf1 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs @@ -56,7 +56,34 @@ namespace osu.Game.Tests.Visual.UserInterface } [Test] - public void TestFirstBeatAtFirstTimingPoint() + public void TestSeekBackDoesntPlayMidBeat() + { + int? lastBeatIndex = null; + double? lastActuationTime = null; + TimingControlPoint lastTimingPoint = null; + + AddStep("bind event", () => + { + beatContainer.NewBeat = (i, timingControlPoint, effectControlPoint, channelAmplitudes) => + { + lastActuationTime = gameplayClockContainer.CurrentTime; + lastTimingPoint = timingControlPoint; + lastBeatIndex = i; + }; + }); + + AddStep("Set time before zero", () => + { + lastBeatIndex = null; + gameplayClockContainer.Seek(-1000); + }); + + AddUntilStep("wait for trigger", () => lastBeatIndex != null); + AddAssert("trigger is near beat length", () => lastActuationTime != null && lastBeatIndex != null && Precision.AlmostEquals(lastTimingPoint.Time + lastBeatIndex.Value * lastTimingPoint.BeatLength, lastActuationTime.Value, 32)); + } + + [Test] + public void TestNegativeBeatsStillUsingBeatmapTiming() { int? lastBeatIndex = null; double? lastBpm = null; From b6996d647e9cf62232412e350eb4cc3d98f51680 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 15 Jul 2021 16:13:13 +0900 Subject: [PATCH 040/106] Add ability to disable mistimed event firings --- .../TestSceneBeatSyncedContainer.cs | 31 ++++++++++++++----- .../Containers/BeatSyncedContainer.cs | 23 ++++++++++++-- 2 files changed, 45 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs index 5bfaf35bf1..2c5433e4aa 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs @@ -25,7 +25,7 @@ namespace osu.Game.Tests.Visual.UserInterface [TestFixture] public class TestSceneBeatSyncedContainer : OsuTestScene { - private BeatContainer beatContainer; + private TestBeatSyncedContainer beatContainer; private MasterGameplayClockContainer gameplayClockContainer; @@ -43,7 +43,7 @@ namespace osu.Game.Tests.Visual.UserInterface { gameplayClockContainer = new MasterGameplayClockContainer(Beatmap.Value, 0) { - Child = beatContainer = new BeatContainer + Child = beatContainer = new TestBeatSyncedContainer { Anchor = Anchor.BottomCentre, Origin = Anchor.BottomCentre, @@ -55,13 +55,16 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("Start playback", () => gameplayClockContainer.Start()); } - [Test] - public void TestSeekBackDoesntPlayMidBeat() + [TestCase(false)] + [TestCase(true)] + public void TestDisallowMistimedEventFiring(bool allowMistimed) { int? lastBeatIndex = null; double? lastActuationTime = null; TimingControlPoint lastTimingPoint = null; + AddStep("set mistimed to disallow", () => beatContainer.AllowMistimedEventFiring = allowMistimed); + AddStep("bind event", () => { beatContainer.NewBeat = (i, timingControlPoint, effectControlPoint, channelAmplitudes) => @@ -79,7 +82,15 @@ namespace osu.Game.Tests.Visual.UserInterface }); AddUntilStep("wait for trigger", () => lastBeatIndex != null); - AddAssert("trigger is near beat length", () => lastActuationTime != null && lastBeatIndex != null && Precision.AlmostEquals(lastTimingPoint.Time + lastBeatIndex.Value * lastTimingPoint.BeatLength, lastActuationTime.Value, 32)); + + if (!allowMistimed) + { + AddAssert("trigger is near beat length", () => lastActuationTime != null && lastBeatIndex != null && Precision.AlmostEquals(lastTimingPoint.Time + lastBeatIndex.Value * lastTimingPoint.BeatLength, lastActuationTime.Value, BeatSyncedContainer.MISTIMED_ALLOWANCE)); + } + else + { + AddAssert("trigger is not near beat length", () => lastActuationTime != null && lastBeatIndex != null && !Precision.AlmostEquals(lastTimingPoint.Time + lastBeatIndex.Value * lastTimingPoint.BeatLength, lastActuationTime.Value, BeatSyncedContainer.MISTIMED_ALLOWANCE)); + } } [Test] @@ -132,10 +143,16 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert("bpm is from beatmap", () => lastBpm != null && Precision.AlmostEquals(lastBpm.Value, 60)); } - private class BeatContainer : BeatSyncedContainer + private class TestBeatSyncedContainer : BeatSyncedContainer { private const int flash_layer_height = 150; + public new bool AllowMistimedEventFiring + { + get => base.AllowMistimedEventFiring; + set => base.AllowMistimedEventFiring = value; + } + private readonly InfoString timingPointCount; private readonly InfoString currentTimingPoint; private readonly InfoString beatCount; @@ -148,7 +165,7 @@ namespace osu.Game.Tests.Visual.UserInterface private readonly Box flashLayer; - public BeatContainer() + public TestBeatSyncedContainer() { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; diff --git a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs index 55f694f17e..78ba716cca 100644 --- a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs +++ b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs @@ -1,6 +1,7 @@ // 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 osu.Framework.Allocation; using osu.Framework.Audio.Track; @@ -35,6 +36,19 @@ namespace osu.Game.Graphics.Containers /// protected double EarlyActivationMilliseconds; + /// + /// While this container automatically applied an animation delay (meaning any animations inside a implementation will + /// always be correctly timed), the event itself can potentially fire away from the related beat. + /// + /// By setting this to false, cases where the event is to be fired more than from the related beat will be skipped. + /// + protected bool AllowMistimedEventFiring = true; + + /// + /// The maximum deviance from the actual beat that an can fire when is set to false. + /// + public const double MISTIMED_ALLOWANCE = 16; + /// /// The time in milliseconds until the next beat. /// @@ -145,8 +159,13 @@ namespace osu.Game.Graphics.Containers if (timingPoint == lastTimingPoint && beatIndex == lastBeat) return; - using (BeginDelayedSequence(-TimeSinceLastBeat)) - OnNewBeat(beatIndex, timingPoint, effectPoint, track?.CurrentAmplitudes ?? ChannelAmplitudes.Empty); + // as this event is sometimes used for sound triggers where `BeginDelayedSequence` has no effect, avoid firing it if too far away from the beat. + // this can happen after a seek operation. + if (AllowMistimedEventFiring || Math.Abs(TimeSinceLastBeat) < MISTIMED_ALLOWANCE) + { + using (BeginDelayedSequence(-TimeSinceLastBeat)) + OnNewBeat(beatIndex, timingPoint, effectPoint, track?.CurrentAmplitudes ?? ChannelAmplitudes.Empty); + } lastBeat = beatIndex; lastTimingPoint = timingPoint; From efdc8fa8a6cfdebabf5b97ab663e910c9eb3d0f4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 15 Jul 2021 19:08:12 +0900 Subject: [PATCH 041/106] Fix incorrect step name Co-authored-by: Henry Lin --- .../Visual/UserInterface/TestSceneBeatSyncedContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs index 2c5433e4aa..6bd5c7bcd8 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs @@ -63,7 +63,7 @@ namespace osu.Game.Tests.Visual.UserInterface double? lastActuationTime = null; TimingControlPoint lastTimingPoint = null; - AddStep("set mistimed to disallow", () => beatContainer.AllowMistimedEventFiring = allowMistimed); + AddStep($"set mistimed to {(allowMistimed ? "allowed" : "disallowed")}", () => beatContainer.AllowMistimedEventFiring = allowMistimed); AddStep("bind event", () => { From db4d64effb1f208b528af2ff4d0bd5a1954d86f2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 17 Jul 2021 14:29:18 +0900 Subject: [PATCH 042/106] Rename incorrect step --- .../Visual/UserInterface/TestSceneBeatSyncedContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs index 6bd5c7bcd8..6b56f339d8 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs @@ -140,7 +140,7 @@ namespace osu.Game.Tests.Visual.UserInterface }); AddUntilStep("wait for trigger", () => lastBpm != null); - AddAssert("bpm is from beatmap", () => lastBpm != null && Precision.AlmostEquals(lastBpm.Value, 60)); + AddAssert("bpm is default", () => lastBpm != null && Precision.AlmostEquals(lastBpm.Value, 60)); } private class TestBeatSyncedContainer : BeatSyncedContainer From 23ed77f2c6f10135e3b849e5adf96008372b9cd4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 17 Jul 2021 14:33:02 +0900 Subject: [PATCH 043/106] Fix test failure under visual tests due to double firing --- .../UserInterface/TestSceneBeatSyncedContainer.cs | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs index 6b56f339d8..e5bcc08924 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs @@ -65,19 +65,16 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep($"set mistimed to {(allowMistimed ? "allowed" : "disallowed")}", () => beatContainer.AllowMistimedEventFiring = allowMistimed); - AddStep("bind event", () => + AddStep("Set time before zero", () => { beatContainer.NewBeat = (i, timingControlPoint, effectControlPoint, channelAmplitudes) => { lastActuationTime = gameplayClockContainer.CurrentTime; lastTimingPoint = timingControlPoint; lastBeatIndex = i; + beatContainer.NewBeat = null; }; - }); - AddStep("Set time before zero", () => - { - lastBeatIndex = null; gameplayClockContainer.Seek(-1000); }); @@ -99,19 +96,14 @@ namespace osu.Game.Tests.Visual.UserInterface int? lastBeatIndex = null; double? lastBpm = null; - AddStep("bind event", () => + AddStep("Set time before zero", () => { beatContainer.NewBeat = (i, timingControlPoint, effectControlPoint, channelAmplitudes) => { lastBeatIndex = i; lastBpm = timingControlPoint.BPM; }; - }); - AddStep("Set time before zero", () => - { - lastBeatIndex = null; - lastBpm = null; gameplayClockContainer.Seek(-1000); }); From 87d3bd4b9389a3582f15ccfbf054dd8b3fe8e01a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 17 Jul 2021 14:35:43 +0900 Subject: [PATCH 044/106] Fix time until next beat potentially being exactly zero at point of trigger --- osu.Game/Graphics/Containers/BeatSyncedContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs index 78ba716cca..6e4901ab1a 100644 --- a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs +++ b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs @@ -151,7 +151,7 @@ namespace osu.Game.Graphics.Containers beatIndex--; TimeUntilNextBeat = (timingPoint.Time - currentTrackTime) % beatLength; - if (TimeUntilNextBeat < 0) + if (TimeUntilNextBeat <= 0) TimeUntilNextBeat += beatLength; TimeSinceLastBeat = beatLength - TimeUntilNextBeat; From d609839ff68e27e1e76d12d3f3cec601d8d68ce6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 17 Jul 2021 14:56:10 +0900 Subject: [PATCH 045/106] Fix test not working due to popover container being too global --- .../Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs | 5 ++++- osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs | 6 ++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs index 3ba0f4969a..27fc1dcd51 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs @@ -3,6 +3,9 @@ using System.Linq; using NUnit.Framework; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.UserInterface; using osu.Framework.Screens; using osu.Framework.Testing; @@ -60,7 +63,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true)); AddStep("select room", () => InputManager.Key(Key.Down)); AddStep("attempt join room", () => InputManager.Key(Key.Enter)); - AddUntilStep("password prompt appeared", () => (passwordEntryPopover = loungeScreen.ChildrenOfType().FirstOrDefault()) != null); + AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType().FirstOrDefault()) != null); AddStep("enter password in text box", () => passwordEntryPopover.ChildrenOfType().First().Text = "password"); AddStep("press join room button", () => passwordEntryPopover.ChildrenOfType().First().Click()); diff --git a/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs b/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs index 01dd7a25c8..5d2309b88c 100644 --- a/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs +++ b/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs @@ -3,6 +3,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; using osu.Framework.Testing.Input; using osu.Game.Graphics.Cursor; @@ -35,8 +36,9 @@ namespace osu.Game.Tests.Visual MenuCursorContainer cursorContainer; CompositeDrawable mainContent = - (cursorContainer = new MenuCursorContainer { RelativeSizeAxes = Axes.Both }) - .WithChild(content = new OsuTooltipContainer(cursorContainer.Cursor) { RelativeSizeAxes = Axes.Both }); + new PopoverContainer { RelativeSizeAxes = Axes.Both } + .WithChild(cursorContainer = new MenuCursorContainer { RelativeSizeAxes = Axes.Both }) + .WithChild(content = new OsuTooltipContainer(cursorContainer.Cursor) { RelativeSizeAxes = Axes.Both }); if (CreateNestedActionContainer) { From c966cb053024e180838a8538ec6d46d0f9eb09bb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 17 Jul 2021 15:04:18 +0900 Subject: [PATCH 046/106] Fix dependency lookup failing due to location of `PopoverContainer` --- .../Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs index 84c20a6acc..951b00376e 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs @@ -243,7 +243,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components Alpha = 0; } - public Popover GetPopover() => new PasswordEntryPopover(Room); + public Popover GetPopover() => new PasswordEntryPopover(Room) { JoinRequested = lounge.Join }; public MenuItem[] ContextMenuItems => new MenuItem[] { @@ -350,8 +350,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components { private readonly Room room; - [Resolved(canBeNull: true)] - private LoungeSubScreen lounge { get; set; } + public Action JoinRequested; public PasswordEntryPopover(Room room) { @@ -379,7 +378,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components { Width = 80, Text = "Join Room", - Action = () => lounge?.Join(room, passwordTextbox.Text) + Action = () => JoinRequested?.Invoke(room, passwordTextbox.Text) } } }; From 567a94a28bd90a5082fda0c43a54a38cc1fdbbda Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 17 Jul 2021 15:35:08 +0900 Subject: [PATCH 047/106] Remove unused using statements --- .../Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs index 27fc1dcd51..bde9ea89ed 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs @@ -3,9 +3,6 @@ using System.Linq; using NUnit.Framework; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.UserInterface; using osu.Framework.Screens; using osu.Framework.Testing; From 64cf9b702ededf6e54e4908df54630baec3130c7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 17 Jul 2021 17:26:11 +0900 Subject: [PATCH 048/106] Fix incorrec nesting of manual input manager test containers --- .../Tests/Visual/OsuManualInputManagerTestScene.cs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs b/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs index 5d2309b88c..c5e2e67eaf 100644 --- a/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs +++ b/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs @@ -35,10 +35,16 @@ namespace osu.Game.Tests.Visual { MenuCursorContainer cursorContainer; - CompositeDrawable mainContent = - new PopoverContainer { RelativeSizeAxes = Axes.Both } - .WithChild(cursorContainer = new MenuCursorContainer { RelativeSizeAxes = Axes.Both }) - .WithChild(content = new OsuTooltipContainer(cursorContainer.Cursor) { RelativeSizeAxes = Axes.Both }); + CompositeDrawable mainContent = new PopoverContainer + { + RelativeSizeAxes = Axes.Both, + Child = cursorContainer = new MenuCursorContainer { RelativeSizeAxes = Axes.Both, } + }; + + cursorContainer.Child = content = new OsuTooltipContainer(cursorContainer.Cursor) + { + RelativeSizeAxes = Axes.Both + }; if (CreateNestedActionContainer) { From e6b8307b8e637479713accbc4a797a6a5818a7db Mon Sep 17 00:00:00 2001 From: Lucas A Date: Sat, 17 Jul 2021 14:46:14 +0200 Subject: [PATCH 049/106] Localise `ProfileHeader` --- osu.Game/Overlays/Profile/ProfileHeader.cs | 33 +++++++++++++++++++--- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/Profile/ProfileHeader.cs b/osu.Game/Overlays/Profile/ProfileHeader.cs index c947ef0781..32bca68f0e 100644 --- a/osu.Game/Overlays/Profile/ProfileHeader.cs +++ b/osu.Game/Overlays/Profile/ProfileHeader.cs @@ -7,12 +7,14 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Localisation; using osu.Game.Overlays.Profile.Header; +using osu.Game.Resources.Localisation.Web; using osu.Game.Users; namespace osu.Game.Overlays.Profile { - public class ProfileHeader : TabControlOverlayHeader + public class ProfileHeader : TabControlOverlayHeader { private UserCoverBackground coverContainer; @@ -27,8 +29,6 @@ namespace osu.Game.Overlays.Profile User.ValueChanged += e => updateDisplay(e.NewValue); - TabControl.AddItem("info"); - TabControl.AddItem("modding"); centreHeaderContainer.DetailsVisible.BindValueChanged(visible => detailHeaderContainer.Expanded = visible.NewValue, true); } @@ -96,7 +96,7 @@ namespace osu.Game.Overlays.Profile { public ProfileHeaderTitle() { - Title = "player info"; + Title = PageTitleStrings.MainUsersControllerDefault; IconTexture = "Icons/Hexacons/profile"; } } @@ -106,4 +106,29 @@ namespace osu.Game.Overlays.Profile protected override double LoadDelay => 0; } } + + [LocalisableEnum(typeof(ProfileHeaderTabEnumLocalisationMapper))] + public enum ProfileHeaderTab + { + Info, + Modding, + } + + public class ProfileHeaderTabEnumLocalisationMapper : EnumLocalisationMapper + { + public override LocalisableString Map(ProfileHeaderTab value) + { + switch (value) + { + case ProfileHeaderTab.Info: + return LayoutStrings.HeaderUsersShow; + + case ProfileHeaderTab.Modding: + return LayoutStrings.HeaderUsersModding; + + default: + return string.Empty; + } + } + } } From d9c7ea202672298d2ff31678218eae955d3f9271 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Sat, 17 Jul 2021 14:57:05 +0200 Subject: [PATCH 050/106] Localise profile section titles. --- osu.Game/Overlays/Profile/ProfileSection.cs | 3 ++- osu.Game/Overlays/Profile/Sections/AboutSection.cs | 5 ++++- osu.Game/Overlays/Profile/Sections/BeatmapsSection.cs | 4 +++- osu.Game/Overlays/Profile/Sections/HistoricalSection.cs | 4 +++- osu.Game/Overlays/Profile/Sections/KudosuSection.cs | 4 +++- osu.Game/Overlays/Profile/Sections/MedalsSection.cs | 5 ++++- osu.Game/Overlays/Profile/Sections/RanksSection.cs | 4 +++- osu.Game/Overlays/Profile/Sections/RecentSection.cs | 4 +++- 8 files changed, 25 insertions(+), 8 deletions(-) diff --git a/osu.Game/Overlays/Profile/ProfileSection.cs b/osu.Game/Overlays/Profile/ProfileSection.cs index 21f7921da6..1a5f562fff 100644 --- a/osu.Game/Overlays/Profile/ProfileSection.cs +++ b/osu.Game/Overlays/Profile/ProfileSection.cs @@ -8,6 +8,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Sprites; @@ -17,7 +18,7 @@ namespace osu.Game.Overlays.Profile { public abstract class ProfileSection : Container { - public abstract string Title { get; } + public abstract LocalisableString Title { get; } public abstract string Identifier { get; } diff --git a/osu.Game/Overlays/Profile/Sections/AboutSection.cs b/osu.Game/Overlays/Profile/Sections/AboutSection.cs index 1bc01cfd9e..c224d2b1be 100644 --- a/osu.Game/Overlays/Profile/Sections/AboutSection.cs +++ b/osu.Game/Overlays/Profile/Sections/AboutSection.cs @@ -1,11 +1,14 @@ // 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.Localisation; +using osu.Game.Resources.Localisation.Web; + namespace osu.Game.Overlays.Profile.Sections { public class AboutSection : ProfileSection { - public override string Title => "me!"; + public override LocalisableString Title => UsersStrings.ShowExtraMeTitle; public override string Identifier => "me"; } diff --git a/osu.Game/Overlays/Profile/Sections/BeatmapsSection.cs b/osu.Game/Overlays/Profile/Sections/BeatmapsSection.cs index c283de42f3..67aaf7d41e 100644 --- a/osu.Game/Overlays/Profile/Sections/BeatmapsSection.cs +++ b/osu.Game/Overlays/Profile/Sections/BeatmapsSection.cs @@ -1,14 +1,16 @@ // 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.Localisation; using osu.Game.Online.API.Requests; using osu.Game.Overlays.Profile.Sections.Beatmaps; +using osu.Game.Resources.Localisation.Web; namespace osu.Game.Overlays.Profile.Sections { public class BeatmapsSection : ProfileSection { - public override string Title => "Beatmaps"; + public override LocalisableString Title => UsersStrings.ShowExtraBeatmapsTitle; public override string Identifier => "beatmaps"; diff --git a/osu.Game/Overlays/Profile/Sections/HistoricalSection.cs b/osu.Game/Overlays/Profile/Sections/HistoricalSection.cs index 4fbb7fc7d7..09ca492aa9 100644 --- a/osu.Game/Overlays/Profile/Sections/HistoricalSection.cs +++ b/osu.Game/Overlays/Profile/Sections/HistoricalSection.cs @@ -2,15 +2,17 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Graphics; +using osu.Framework.Localisation; using osu.Game.Online.API.Requests; using osu.Game.Overlays.Profile.Sections.Historical; using osu.Game.Overlays.Profile.Sections.Ranks; +using osu.Game.Resources.Localisation.Web; namespace osu.Game.Overlays.Profile.Sections { public class HistoricalSection : ProfileSection { - public override string Title => "Historical"; + public override LocalisableString Title => UsersStrings.ShowExtraHistoricalTitle; public override string Identifier => "historical"; diff --git a/osu.Game/Overlays/Profile/Sections/KudosuSection.cs b/osu.Game/Overlays/Profile/Sections/KudosuSection.cs index a9e9952257..ffa7ef4eaf 100644 --- a/osu.Game/Overlays/Profile/Sections/KudosuSection.cs +++ b/osu.Game/Overlays/Profile/Sections/KudosuSection.cs @@ -3,12 +3,14 @@ using osu.Framework.Graphics; using osu.Game.Overlays.Profile.Sections.Kudosu; +using osu.Framework.Localisation; +using osu.Game.Resources.Localisation.Web; namespace osu.Game.Overlays.Profile.Sections { public class KudosuSection : ProfileSection { - public override string Title => "Kudosu!"; + public override LocalisableString Title => UsersStrings.ShowExtraKudosuTitle; public override string Identifier => "kudosu"; diff --git a/osu.Game/Overlays/Profile/Sections/MedalsSection.cs b/osu.Game/Overlays/Profile/Sections/MedalsSection.cs index 575a2f2c19..3512333e27 100644 --- a/osu.Game/Overlays/Profile/Sections/MedalsSection.cs +++ b/osu.Game/Overlays/Profile/Sections/MedalsSection.cs @@ -1,11 +1,14 @@ // 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.Localisation; +using osu.Game.Resources.Localisation.Web; + namespace osu.Game.Overlays.Profile.Sections { public class MedalsSection : ProfileSection { - public override string Title => "Medals"; + public override LocalisableString Title => UsersStrings.ShowExtraMedalsTitle; public override string Identifier => "medals"; } diff --git a/osu.Game/Overlays/Profile/Sections/RanksSection.cs b/osu.Game/Overlays/Profile/Sections/RanksSection.cs index 33f7c2f71a..a7931c8675 100644 --- a/osu.Game/Overlays/Profile/Sections/RanksSection.cs +++ b/osu.Game/Overlays/Profile/Sections/RanksSection.cs @@ -3,12 +3,14 @@ using osu.Game.Overlays.Profile.Sections.Ranks; using osu.Game.Online.API.Requests; +using osu.Framework.Localisation; +using osu.Game.Resources.Localisation.Web; namespace osu.Game.Overlays.Profile.Sections { public class RanksSection : ProfileSection { - public override string Title => "Ranks"; + public override LocalisableString Title => UsersStrings.ShowExtraTopRanksTitle; public override string Identifier => "top_ranks"; diff --git a/osu.Game/Overlays/Profile/Sections/RecentSection.cs b/osu.Game/Overlays/Profile/Sections/RecentSection.cs index 1e6cfcc9fd..7a6536c3af 100644 --- a/osu.Game/Overlays/Profile/Sections/RecentSection.cs +++ b/osu.Game/Overlays/Profile/Sections/RecentSection.cs @@ -1,13 +1,15 @@ // 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.Localisation; using osu.Game.Overlays.Profile.Sections.Recent; +using osu.Game.Resources.Localisation.Web; namespace osu.Game.Overlays.Profile.Sections { public class RecentSection : ProfileSection { - public override string Title => "Recent"; + public override LocalisableString Title => UsersStrings.ShowExtraRecentActivityTitle; public override string Identifier => "recent_activity"; From 4d276b114bbacdd9df278aeaa23ef59a956525ff Mon Sep 17 00:00:00 2001 From: Lucas A Date: Sat, 17 Jul 2021 15:18:45 +0200 Subject: [PATCH 051/106] Localise profile header. --- .../Overlays/Profile/Header/Components/ExpandDetailsButton.cs | 3 ++- osu.Game/Overlays/Profile/Header/Components/FollowersButton.cs | 3 ++- .../Overlays/Profile/Header/Components/LevelProgressBar.cs | 3 ++- .../Profile/Header/Components/MappingSubscribersButton.cs | 3 ++- .../Overlays/Profile/Header/Components/MessageUserButton.cs | 3 ++- .../Profile/Header/Components/OverlinedInfoContainer.cs | 3 ++- .../Profile/Header/Components/OverlinedTotalPlayTime.cs | 3 ++- .../Overlays/Profile/Header/Components/PreviousUsernames.cs | 3 ++- 8 files changed, 16 insertions(+), 8 deletions(-) diff --git a/osu.Game/Overlays/Profile/Header/Components/ExpandDetailsButton.cs b/osu.Game/Overlays/Profile/Header/Components/ExpandDetailsButton.cs index 16b443875e..b4a5d5e31b 100644 --- a/osu.Game/Overlays/Profile/Header/Components/ExpandDetailsButton.cs +++ b/osu.Game/Overlays/Profile/Header/Components/ExpandDetailsButton.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Game.Graphics.UserInterface; +using osu.Game.Resources.Localisation.Web; using osuTK; namespace osu.Game.Overlays.Profile.Header.Components @@ -18,7 +19,7 @@ namespace osu.Game.Overlays.Profile.Header.Components { public readonly BindableBool DetailsVisible = new BindableBool(); - public override LocalisableString TooltipText => DetailsVisible.Value ? "collapse" : "expand"; + public override LocalisableString TooltipText => DetailsVisible.Value ? CommonStrings.ButtonsCollapse : CommonStrings.ButtonsExpand; private SpriteIcon icon; private Sample sampleOpen; diff --git a/osu.Game/Overlays/Profile/Header/Components/FollowersButton.cs b/osu.Game/Overlays/Profile/Header/Components/FollowersButton.cs index db94762efd..8f66120055 100644 --- a/osu.Game/Overlays/Profile/Header/Components/FollowersButton.cs +++ b/osu.Game/Overlays/Profile/Header/Components/FollowersButton.cs @@ -5,6 +5,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; +using osu.Game.Resources.Localisation.Web; using osu.Game.Users; namespace osu.Game.Overlays.Profile.Header.Components @@ -13,7 +14,7 @@ namespace osu.Game.Overlays.Profile.Header.Components { public readonly Bindable User = new Bindable(); - public override LocalisableString TooltipText => "followers"; + public override LocalisableString TooltipText => FriendsStrings.ButtonsDisabled; protected override IconUsage Icon => FontAwesome.Solid.User; diff --git a/osu.Game/Overlays/Profile/Header/Components/LevelProgressBar.cs b/osu.Game/Overlays/Profile/Header/Components/LevelProgressBar.cs index 528b05a80a..ed89d78a10 100644 --- a/osu.Game/Overlays/Profile/Header/Components/LevelProgressBar.cs +++ b/osu.Game/Overlays/Profile/Header/Components/LevelProgressBar.cs @@ -10,6 +10,7 @@ using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; +using osu.Game.Resources.Localisation.Web; using osu.Game.Users; using osuTK.Graphics; @@ -26,7 +27,7 @@ namespace osu.Game.Overlays.Profile.Header.Components public LevelProgressBar() { - TooltipText = "progress to next level"; + TooltipText = UsersStrings.ShowStatsLevelProgress; } [BackgroundDependencyLoader] diff --git a/osu.Game/Overlays/Profile/Header/Components/MappingSubscribersButton.cs b/osu.Game/Overlays/Profile/Header/Components/MappingSubscribersButton.cs index ae3d024fbf..5cdf3a5ef9 100644 --- a/osu.Game/Overlays/Profile/Header/Components/MappingSubscribersButton.cs +++ b/osu.Game/Overlays/Profile/Header/Components/MappingSubscribersButton.cs @@ -5,6 +5,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; +using osu.Game.Resources.Localisation.Web; using osu.Game.Users; namespace osu.Game.Overlays.Profile.Header.Components @@ -13,7 +14,7 @@ namespace osu.Game.Overlays.Profile.Header.Components { public readonly Bindable User = new Bindable(); - public override LocalisableString TooltipText => "mapping subscribers"; + public override LocalisableString TooltipText => FollowsStrings.MappingFollowers; protected override IconUsage Icon => FontAwesome.Solid.Bell; diff --git a/osu.Game/Overlays/Profile/Header/Components/MessageUserButton.cs b/osu.Game/Overlays/Profile/Header/Components/MessageUserButton.cs index 4c2cc768ce..07f1f1c3ed 100644 --- a/osu.Game/Overlays/Profile/Header/Components/MessageUserButton.cs +++ b/osu.Game/Overlays/Profile/Header/Components/MessageUserButton.cs @@ -8,6 +8,7 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Game.Online.API; using osu.Game.Online.Chat; +using osu.Game.Resources.Localisation.Web; using osu.Game.Users; using osuTK; @@ -17,7 +18,7 @@ namespace osu.Game.Overlays.Profile.Header.Components { public readonly Bindable User = new Bindable(); - public override LocalisableString TooltipText => "send message"; + public override LocalisableString TooltipText => UsersStrings.CardSendMessage; [Resolved(CanBeNull = true)] private ChannelManager channelManager { get; set; } diff --git a/osu.Game/Overlays/Profile/Header/Components/OverlinedInfoContainer.cs b/osu.Game/Overlays/Profile/Header/Components/OverlinedInfoContainer.cs index 9f56a34aa6..8f1bbc4097 100644 --- a/osu.Game/Overlays/Profile/Header/Components/OverlinedInfoContainer.cs +++ b/osu.Game/Overlays/Profile/Header/Components/OverlinedInfoContainer.cs @@ -4,6 +4,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osuTK.Graphics; @@ -16,7 +17,7 @@ namespace osu.Game.Overlays.Profile.Header.Components private readonly OsuSpriteText title; private readonly OsuSpriteText content; - public string Title + public LocalisableString Title { set => title.Text = value; } diff --git a/osu.Game/Overlays/Profile/Header/Components/OverlinedTotalPlayTime.cs b/osu.Game/Overlays/Profile/Header/Components/OverlinedTotalPlayTime.cs index aa7cb8636a..1a40944632 100644 --- a/osu.Game/Overlays/Profile/Header/Components/OverlinedTotalPlayTime.cs +++ b/osu.Game/Overlays/Profile/Header/Components/OverlinedTotalPlayTime.cs @@ -7,6 +7,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Localisation; +using osu.Game.Resources.Localisation.Web; using osu.Game.Users; namespace osu.Game.Overlays.Profile.Header.Components @@ -31,7 +32,7 @@ namespace osu.Game.Overlays.Profile.Header.Components { InternalChild = info = new OverlinedInfoContainer { - Title = "Total Play Time", + Title = UsersStrings.ShowStatsPlayTime, LineColour = colourProvider.Highlight1, }; diff --git a/osu.Game/Overlays/Profile/Header/Components/PreviousUsernames.cs b/osu.Game/Overlays/Profile/Header/Components/PreviousUsernames.cs index 3cdf110090..14eeb4e5f0 100644 --- a/osu.Game/Overlays/Profile/Header/Components/PreviousUsernames.cs +++ b/osu.Game/Overlays/Profile/Header/Components/PreviousUsernames.cs @@ -12,6 +12,7 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osu.Game.Resources.Localisation.Web; using osu.Game.Users; using osuTK; @@ -68,7 +69,7 @@ namespace osu.Game.Overlays.Profile.Header.Components { Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, - Text = @"formerly known as", + Text = UsersStrings.ShowPreviousUsernames, Font = OsuFont.GetFont(size: 10, italics: true) } }, From 306a34a80280dc608dff653666c2e7f5a2caaddf Mon Sep 17 00:00:00 2001 From: Lucas A Date: Sat, 17 Jul 2021 15:21:30 +0200 Subject: [PATCH 052/106] Localise level badge tooltip. --- osu.Game/Overlays/Profile/Header/Components/LevelBadge.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Profile/Header/Components/LevelBadge.cs b/osu.Game/Overlays/Profile/Header/Components/LevelBadge.cs index a0b8ef0f11..1deed1a748 100644 --- a/osu.Game/Overlays/Profile/Header/Components/LevelBadge.cs +++ b/osu.Game/Overlays/Profile/Header/Components/LevelBadge.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics.Textures; using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osu.Game.Resources.Localisation.Web; using osu.Game.Users; namespace osu.Game.Overlays.Profile.Header.Components @@ -19,13 +20,13 @@ namespace osu.Game.Overlays.Profile.Header.Components { public readonly Bindable User = new Bindable(); - public LocalisableString TooltipText { get; } + public LocalisableString TooltipText { get; private set; } private OsuSpriteText levelText; public LevelBadge() { - TooltipText = "level"; + TooltipText = UsersStrings.ShowStatsLevel("0"); } [BackgroundDependencyLoader] @@ -53,6 +54,7 @@ namespace osu.Game.Overlays.Profile.Header.Components private void updateLevel(User user) { levelText.Text = user?.Statistics?.Level.Current.ToString() ?? "0"; + TooltipText = UsersStrings.ShowStatsLevel(user?.Statistics?.Level.Current.ToString()); } } } From a0c6945f8fd320e3d23ab8b07706dc66b01fcaf6 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Sat, 17 Jul 2021 15:25:45 +0200 Subject: [PATCH 053/106] Localise user graph. --- osu.Game/Overlays/Profile/Header/Components/RankGraph.cs | 5 +++-- osu.Game/Overlays/Profile/UserGraph.cs | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs b/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs index ad91e491ef..6bf356c0ff 100644 --- a/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs +++ b/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs @@ -9,6 +9,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osu.Game.Resources.Localisation.Web; using osu.Game.Users; namespace osu.Game.Overlays.Profile.Header.Components @@ -27,7 +28,7 @@ namespace osu.Game.Overlays.Profile.Header.Components { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Text = "No recent plays", + Text = UsersStrings.ShowExtraUnranked, Font = OsuFont.GetFont(size: 12, weight: FontWeight.Regular) }); } @@ -74,7 +75,7 @@ namespace osu.Game.Overlays.Profile.Header.Components private class RankGraphTooltip : UserGraphTooltip { public RankGraphTooltip() - : base("Global Ranking") + : base(UsersStrings.ShowRankGlobalSimple) { } diff --git a/osu.Game/Overlays/Profile/UserGraph.cs b/osu.Game/Overlays/Profile/UserGraph.cs index cdfd722d68..b7a08b6c5e 100644 --- a/osu.Game/Overlays/Profile/UserGraph.cs +++ b/osu.Game/Overlays/Profile/UserGraph.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; +using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; @@ -211,7 +212,7 @@ namespace osu.Game.Overlays.Profile protected readonly OsuSpriteText Counter, BottomText; private readonly Box background; - protected UserGraphTooltip(string tooltipCounterName) + protected UserGraphTooltip(LocalisableString tooltipCounterName) { AutoSizeAxes = Axes.Both; Masking = true; From 2cfec1dc326af464ccb324a3092ac54673b0dad2 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Sat, 17 Jul 2021 15:26:18 +0200 Subject: [PATCH 054/106] Localise osu!supporter badge. --- osu.Game/Overlays/Profile/Header/Components/SupporterIcon.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Profile/Header/Components/SupporterIcon.cs b/osu.Game/Overlays/Profile/Header/Components/SupporterIcon.cs index 9a43997030..77f0378762 100644 --- a/osu.Game/Overlays/Profile/Header/Components/SupporterIcon.cs +++ b/osu.Game/Overlays/Profile/Header/Components/SupporterIcon.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Game.Graphics; +using osu.Game.Resources.Localisation.Web; namespace osu.Game.Overlays.Profile.Header.Components { @@ -19,7 +20,7 @@ namespace osu.Game.Overlays.Profile.Header.Components private readonly FillFlowContainer iconContainer; private readonly CircularContainer content; - public LocalisableString TooltipText => "osu!supporter"; + public LocalisableString TooltipText => UsersStrings.ShowIsSupporter; public int SupportLevel { From c6a27e4baaf84cf9e07df687bbd83b6a90c5bb36 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Sat, 17 Jul 2021 15:27:40 +0200 Subject: [PATCH 055/106] Localise `CentreHeaderContainer`. --- osu.Game/Overlays/Profile/Header/CentreHeaderContainer.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Profile/Header/CentreHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/CentreHeaderContainer.cs index 62ebee7677..4195b0b2f1 100644 --- a/osu.Game/Overlays/Profile/Header/CentreHeaderContainer.cs +++ b/osu.Game/Overlays/Profile/Header/CentreHeaderContainer.cs @@ -8,6 +8,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Textures; using osu.Game.Overlays.Profile.Header.Components; +using osu.Game.Resources.Localisation.Web; using osu.Game.Users; using osuTK; @@ -119,12 +120,12 @@ namespace osu.Game.Overlays.Profile.Header { hiddenDetailGlobal = new OverlinedInfoContainer { - Title = "Global Ranking", + Title = UsersStrings.ShowRankGlobalSimple, LineColour = colourProvider.Highlight1 }, hiddenDetailCountry = new OverlinedInfoContainer { - Title = "Country Ranking", + Title = UsersStrings.ShowRankCountrySimple, LineColour = colourProvider.Highlight1 }, } From 213e3c0716e8706565489a4cd3896e08ca0a5383 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Sat, 17 Jul 2021 15:29:34 +0200 Subject: [PATCH 056/106] Localise `DetailHeaderContainer` --- osu.Game/Overlays/Profile/Header/DetailHeaderContainer.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Profile/Header/DetailHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/DetailHeaderContainer.cs index 574aef02fd..6214e504b0 100644 --- a/osu.Game/Overlays/Profile/Header/DetailHeaderContainer.cs +++ b/osu.Game/Overlays/Profile/Header/DetailHeaderContainer.cs @@ -11,6 +11,7 @@ using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Online.Leaderboards; using osu.Game.Overlays.Profile.Header.Components; +using osu.Game.Resources.Localisation.Web; using osu.Game.Scoring; using osu.Game.Users; using osuTK; @@ -100,7 +101,7 @@ namespace osu.Game.Overlays.Profile.Header }, medalInfo = new OverlinedInfoContainer { - Title = "Medals", + Title = UsersStrings.ShowStatsMedals, LineColour = colours.GreenLight, }, ppInfo = new OverlinedInfoContainer @@ -151,12 +152,12 @@ namespace osu.Game.Overlays.Profile.Header { detailGlobalRank = new OverlinedInfoContainer(true, 110) { - Title = "Global Ranking", + Title = UsersStrings.ShowRankGlobalSimple, LineColour = colourProvider.Highlight1, }, detailCountryRank = new OverlinedInfoContainer(false, 110) { - Title = "Country Ranking", + Title = UsersStrings.ShowRankCountrySimple, LineColour = colourProvider.Highlight1, }, } From c6bc95767dc5e2ac3e24596a6d99fd06e3d53ed1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 17 Jul 2021 22:31:47 +0900 Subject: [PATCH 057/106] Simplify popover hide logic and add test coverage --- .../TestSceneMultiplayerLoungeSubScreen.cs | 12 +++++++++++ .../Lounge/Components/RoomsContainer.cs | 7 ------- .../OnlinePlay/Lounge/LoungeSubScreen.cs | 20 ++++++++++++------- 3 files changed, 25 insertions(+), 14 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs index bde9ea89ed..de46d9e25a 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs @@ -52,6 +52,18 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("room join password correct", () => lastJoinedPassword == null); } + [Test] + public void TestPopoverHidesOnLeavingScreen() + { + AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true)); + AddStep("select room", () => InputManager.Key(Key.Down)); + AddStep("attempt join room", () => InputManager.Key(Key.Enter)); + + AddUntilStep("password prompt appeared", () => InputManager.ChildrenOfType().Any()); + AddStep("exit screen", () => Stack.Exit()); + AddUntilStep("password prompt hidden", () => !InputManager.ChildrenOfType().Any()); + } + [Test] public void TestJoinRoomWithPassword() { diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs index 0910ff1a7a..07e412ee75 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs @@ -7,7 +7,6 @@ using System.Collections.Specialized; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Extensions; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -239,11 +238,5 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components if (roomManager != null) roomManager.RoomsUpdated -= updateSorting; } - - public void HideAnyPopovers() - { - // must be called on a child of the PopoverContainer due to parent traversal not considering self. - roomFlow.HidePopover(); - } } } diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs index dd6106b868..f43109c4fa 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs @@ -6,6 +6,7 @@ using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; @@ -151,24 +152,29 @@ namespace osu.Game.Screens.OnlinePlay.Lounge onReturning(); } - private void onReturning() - { - filter.HoldFocus = true; - } - public override bool OnExiting(IScreen next) { - filter.HoldFocus = false; + onLeaving(); return base.OnExiting(next); } public override void OnSuspending(IScreen next) { + onLeaving(); base.OnSuspending(next); + } + + private void onReturning() + { + filter.HoldFocus = true; + } + + private void onLeaving() + { filter.HoldFocus = false; // ensure any password prompt is dismissed. - roomsContainer.HideAnyPopovers(); + this.HidePopover(); } public void Join(Room room, string password) From d36842aa15a47a5dd252802970ed9e465fb4552a Mon Sep 17 00:00:00 2001 From: Lucas A Date: Sat, 17 Jul 2021 15:32:28 +0200 Subject: [PATCH 058/106] Localise `TopHeaderContainer` --- .../Profile/Header/TopHeaderContainer.cs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs index d751424367..b64dba62e3 100644 --- a/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs +++ b/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs @@ -7,11 +7,13 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Overlays.Profile.Header.Components; +using osu.Game.Resources.Localisation.Web; using osu.Game.Users; using osu.Game.Users.Drawables; using osuTK; @@ -179,19 +181,19 @@ namespace osu.Game.Overlays.Profile.Header if (user?.Statistics != null) { - userStats.Add(new UserStatsLine("Ranked Score", user.Statistics.RankedScore.ToString("#,##0"))); - userStats.Add(new UserStatsLine("Hit Accuracy", user.Statistics.DisplayAccuracy)); - userStats.Add(new UserStatsLine("Play Count", user.Statistics.PlayCount.ToString("#,##0"))); - userStats.Add(new UserStatsLine("Total Score", user.Statistics.TotalScore.ToString("#,##0"))); - userStats.Add(new UserStatsLine("Total Hits", user.Statistics.TotalHits.ToString("#,##0"))); - userStats.Add(new UserStatsLine("Maximum Combo", user.Statistics.MaxCombo.ToString("#,##0"))); - userStats.Add(new UserStatsLine("Replays Watched by Others", user.Statistics.ReplaysWatched.ToString("#,##0"))); + userStats.Add(new UserStatsLine(UsersStrings.ShowStatsRankedScore, user.Statistics.RankedScore.ToString("#,##0"))); + userStats.Add(new UserStatsLine(UsersStrings.ShowStatsHitAccuracy, user.Statistics.DisplayAccuracy)); + userStats.Add(new UserStatsLine(UsersStrings.ShowStatsPlayCount, user.Statistics.PlayCount.ToString("#,##0"))); + userStats.Add(new UserStatsLine(UsersStrings.ShowStatsTotalScore, user.Statistics.TotalScore.ToString("#,##0"))); + userStats.Add(new UserStatsLine(UsersStrings.ShowStatsTotalHits, user.Statistics.TotalHits.ToString("#,##0"))); + userStats.Add(new UserStatsLine(UsersStrings.ShowStatsMaximumCombo, user.Statistics.MaxCombo.ToString("#,##0"))); + userStats.Add(new UserStatsLine(UsersStrings.ShowStatsReplaysWatchedByOthers, user.Statistics.ReplaysWatched.ToString("#,##0"))); } } private class UserStatsLine : Container { - public UserStatsLine(string left, string right) + public UserStatsLine(LocalisableString left, string right) { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; From 5bb45c7f84fb4b30e310a1723fb2e8dfc0753ce1 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Sat, 17 Jul 2021 15:45:17 +0200 Subject: [PATCH 059/106] Localise beatmap section. --- .../Sections/Beatmaps/PaginatedBeatmapContainer.cs | 5 +++-- osu.Game/Overlays/Profile/Sections/BeatmapsSection.cs | 10 +++++----- .../Profile/Sections/PaginatedProfileSubsection.cs | 9 +++++---- .../Overlays/Profile/Sections/ProfileSubsection.cs | 5 +++-- .../Profile/Sections/ProfileSubsectionHeader.cs | 5 +++-- 5 files changed, 19 insertions(+), 15 deletions(-) diff --git a/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs b/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs index fe9c710bcc..af1ed9d529 100644 --- a/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Localisation; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; @@ -19,8 +20,8 @@ namespace osu.Game.Overlays.Profile.Sections.Beatmaps private const float panel_padding = 10f; private readonly BeatmapSetType type; - public PaginatedBeatmapContainer(BeatmapSetType type, Bindable user, string headerText) - : base(user, headerText) + public PaginatedBeatmapContainer(BeatmapSetType type, Bindable user, LocalisableString headerText) + : base(user, headerText, null) { this.type = type; ItemsPerPage = 6; diff --git a/osu.Game/Overlays/Profile/Sections/BeatmapsSection.cs b/osu.Game/Overlays/Profile/Sections/BeatmapsSection.cs index 67aaf7d41e..b8fbe73c0c 100644 --- a/osu.Game/Overlays/Profile/Sections/BeatmapsSection.cs +++ b/osu.Game/Overlays/Profile/Sections/BeatmapsSection.cs @@ -18,11 +18,11 @@ namespace osu.Game.Overlays.Profile.Sections { Children = new[] { - new PaginatedBeatmapContainer(BeatmapSetType.Favourite, User, "Favourite Beatmaps"), - new PaginatedBeatmapContainer(BeatmapSetType.RankedAndApproved, User, "Ranked & Approved Beatmaps"), - new PaginatedBeatmapContainer(BeatmapSetType.Loved, User, "Loved Beatmaps"), - new PaginatedBeatmapContainer(BeatmapSetType.Unranked, User, "Pending Beatmaps"), - new PaginatedBeatmapContainer(BeatmapSetType.Graveyard, User, "Graveyarded Beatmaps") + new PaginatedBeatmapContainer(BeatmapSetType.Favourite, User, UsersStrings.ShowExtraBeatmapsFavouriteTitle), + new PaginatedBeatmapContainer(BeatmapSetType.RankedAndApproved, User, UsersStrings.ShowExtraBeatmapsRankedTitle), + new PaginatedBeatmapContainer(BeatmapSetType.Loved, User, UsersStrings.ShowExtraBeatmapsLovedTitle), + new PaginatedBeatmapContainer(BeatmapSetType.Unranked, User, UsersStrings.ShowExtraBeatmapsPendingTitle), + new PaginatedBeatmapContainer(BeatmapSetType.Graveyard, User, UsersStrings.ShowExtraBeatmapsGraveyardTitle) }; } } diff --git a/osu.Game/Overlays/Profile/Sections/PaginatedProfileSubsection.cs b/osu.Game/Overlays/Profile/Sections/PaginatedProfileSubsection.cs index e237b43b2e..25c72a24da 100644 --- a/osu.Game/Overlays/Profile/Sections/PaginatedProfileSubsection.cs +++ b/osu.Game/Overlays/Profile/Sections/PaginatedProfileSubsection.cs @@ -15,6 +15,7 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets; using osu.Game.Graphics.Sprites; using osu.Game.Graphics; +using osu.Framework.Localisation; namespace osu.Game.Overlays.Profile.Sections { @@ -36,9 +37,9 @@ namespace osu.Game.Overlays.Profile.Sections private ShowMoreButton moreButton; private OsuSpriteText missing; - private readonly string missingText; + private readonly LocalisableString? missingText; - protected PaginatedProfileSubsection(Bindable user, string headerText = "", string missingText = "") + protected PaginatedProfileSubsection(Bindable user, LocalisableString headerText, LocalisableString? missingText) : base(user, headerText, CounterVisibilityState.AlwaysVisible) { this.missingText = missingText; @@ -68,7 +69,7 @@ namespace osu.Game.Overlays.Profile.Sections missing = new OsuSpriteText { Font = OsuFont.GetFont(size: 15), - Text = missingText, + Text = missingText ?? string.Empty, Alpha = 0, } } @@ -114,7 +115,7 @@ namespace osu.Game.Overlays.Profile.Sections moreButton.Hide(); moreButton.IsLoading = false; - if (!string.IsNullOrEmpty(missingText)) + if (missingText.HasValue) missing.Show(); return; diff --git a/osu.Game/Overlays/Profile/Sections/ProfileSubsection.cs b/osu.Game/Overlays/Profile/Sections/ProfileSubsection.cs index 3e331f85e9..a9d2448abf 100644 --- a/osu.Game/Overlays/Profile/Sections/ProfileSubsection.cs +++ b/osu.Game/Overlays/Profile/Sections/ProfileSubsection.cs @@ -7,6 +7,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Users; using JetBrains.Annotations; +using osu.Framework.Localisation; namespace osu.Game.Overlays.Profile.Sections { @@ -14,12 +15,12 @@ namespace osu.Game.Overlays.Profile.Sections { protected readonly Bindable User = new Bindable(); - private readonly string headerText; + private readonly LocalisableString headerText; private readonly CounterVisibilityState counterVisibilityState; private ProfileSubsectionHeader header; - protected ProfileSubsection(Bindable user, string headerText = "", CounterVisibilityState counterVisibilityState = CounterVisibilityState.AlwaysHidden) + protected ProfileSubsection(Bindable user, LocalisableString headerText, CounterVisibilityState counterVisibilityState = CounterVisibilityState.AlwaysHidden) { this.headerText = headerText; this.counterVisibilityState = counterVisibilityState; diff --git a/osu.Game/Overlays/Profile/Sections/ProfileSubsectionHeader.cs b/osu.Game/Overlays/Profile/Sections/ProfileSubsectionHeader.cs index 5858cebe89..408cb00770 100644 --- a/osu.Game/Overlays/Profile/Sections/ProfileSubsectionHeader.cs +++ b/osu.Game/Overlays/Profile/Sections/ProfileSubsectionHeader.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics.Shapes; using osuTK; using osu.Game.Graphics.Sprites; using osu.Game.Graphics; +using osu.Framework.Localisation; namespace osu.Game.Overlays.Profile.Sections { @@ -24,12 +25,12 @@ namespace osu.Game.Overlays.Profile.Sections set => current.Current = value; } - private readonly string text; + private readonly LocalisableString text; private readonly CounterVisibilityState counterState; private CounterPill counterPill; - public ProfileSubsectionHeader(string text, CounterVisibilityState counterState) + public ProfileSubsectionHeader(LocalisableString text, CounterVisibilityState counterState) { this.text = text; this.counterState = counterState; From d17f6589857a1448bdb445dccb3cb0d916a137ff Mon Sep 17 00:00:00 2001 From: Lucas A Date: Sat, 17 Jul 2021 15:50:56 +0200 Subject: [PATCH 060/106] Localise Recent section. --- .../Sections/Recent/PaginatedRecentActivityContainer.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Profile/Sections/Recent/PaginatedRecentActivityContainer.cs b/osu.Game/Overlays/Profile/Sections/Recent/PaginatedRecentActivityContainer.cs index d7101a8147..04c8d7483b 100644 --- a/osu.Game/Overlays/Profile/Sections/Recent/PaginatedRecentActivityContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/Recent/PaginatedRecentActivityContainer.cs @@ -10,13 +10,14 @@ using osu.Game.Online.API; using System.Collections.Generic; using osuTK; using osu.Framework.Allocation; +using osu.Game.Resources.Localisation.Web; namespace osu.Game.Overlays.Profile.Sections.Recent { public class PaginatedRecentActivityContainer : PaginatedProfileSubsection { public PaginatedRecentActivityContainer(Bindable user) - : base(user, missingText: "This user hasn't done anything notable recently!") + : base(user, string.Empty, EventsStrings.Empty) { ItemsPerPage = 10; } From fbbf8ce5a3950f7a3da6a08ec61c64c4f5a2b1f7 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Sat, 17 Jul 2021 15:53:24 +0200 Subject: [PATCH 061/106] Localise Ranks section. --- .../Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs | 2 +- .../Overlays/Profile/Sections/PaginatedProfileSubsection.cs | 2 +- .../Profile/Sections/Ranks/PaginatedScoreContainer.cs | 3 ++- osu.Game/Overlays/Profile/Sections/RanksSection.cs | 4 ++-- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs b/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs index af1ed9d529..ec64371a5d 100644 --- a/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs @@ -21,7 +21,7 @@ namespace osu.Game.Overlays.Profile.Sections.Beatmaps private readonly BeatmapSetType type; public PaginatedBeatmapContainer(BeatmapSetType type, Bindable user, LocalisableString headerText) - : base(user, headerText, null) + : base(user, headerText) { this.type = type; ItemsPerPage = 6; diff --git a/osu.Game/Overlays/Profile/Sections/PaginatedProfileSubsection.cs b/osu.Game/Overlays/Profile/Sections/PaginatedProfileSubsection.cs index 25c72a24da..4a37c94abe 100644 --- a/osu.Game/Overlays/Profile/Sections/PaginatedProfileSubsection.cs +++ b/osu.Game/Overlays/Profile/Sections/PaginatedProfileSubsection.cs @@ -39,7 +39,7 @@ namespace osu.Game.Overlays.Profile.Sections private OsuSpriteText missing; private readonly LocalisableString? missingText; - protected PaginatedProfileSubsection(Bindable user, LocalisableString headerText, LocalisableString? missingText) + protected PaginatedProfileSubsection(Bindable user, LocalisableString headerText, LocalisableString? missingText = null) : base(user, headerText, CounterVisibilityState.AlwaysVisible) { this.missingText = missingText; diff --git a/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs b/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs index 720cd4a3db..7c04b331c2 100644 --- a/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs @@ -11,6 +11,7 @@ using osu.Game.Online.API.Requests.Responses; using System.Collections.Generic; using osu.Game.Online.API; using osu.Framework.Allocation; +using osu.Framework.Localisation; namespace osu.Game.Overlays.Profile.Sections.Ranks { @@ -18,7 +19,7 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks { private readonly ScoreType type; - public PaginatedScoreContainer(ScoreType type, Bindable user, string headerText) + public PaginatedScoreContainer(ScoreType type, Bindable user, LocalisableString headerText) : base(user, headerText) { this.type = type; diff --git a/osu.Game/Overlays/Profile/Sections/RanksSection.cs b/osu.Game/Overlays/Profile/Sections/RanksSection.cs index a7931c8675..5e0648dbb1 100644 --- a/osu.Game/Overlays/Profile/Sections/RanksSection.cs +++ b/osu.Game/Overlays/Profile/Sections/RanksSection.cs @@ -18,8 +18,8 @@ namespace osu.Game.Overlays.Profile.Sections { Children = new[] { - new PaginatedScoreContainer(ScoreType.Best, User, "Best Performance"), - new PaginatedScoreContainer(ScoreType.Firsts, User, "First Place Ranks") + new PaginatedScoreContainer(ScoreType.Best, User, UsersStrings.ShowExtraTopRanksBestTitle), + new PaginatedScoreContainer(ScoreType.Firsts, User, UsersStrings.ShowExtraTopRanksFirstTitle) }; } } From 2f3ed4a4ab080d7758afc6d2786a7d1118b4f736 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Sat, 17 Jul 2021 16:13:33 +0200 Subject: [PATCH 062/106] Fix `PaginatedProfileSubsection` ctor arguments --- .../Overlays/Profile/Sections/PaginatedProfileSubsection.cs | 2 +- osu.Game/Overlays/Profile/Sections/ProfileSubsection.cs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/Profile/Sections/PaginatedProfileSubsection.cs b/osu.Game/Overlays/Profile/Sections/PaginatedProfileSubsection.cs index 4a37c94abe..d60243cd0a 100644 --- a/osu.Game/Overlays/Profile/Sections/PaginatedProfileSubsection.cs +++ b/osu.Game/Overlays/Profile/Sections/PaginatedProfileSubsection.cs @@ -39,7 +39,7 @@ namespace osu.Game.Overlays.Profile.Sections private OsuSpriteText missing; private readonly LocalisableString? missingText; - protected PaginatedProfileSubsection(Bindable user, LocalisableString headerText, LocalisableString? missingText = null) + protected PaginatedProfileSubsection(Bindable user, LocalisableString? headerText = null, LocalisableString? missingText = null) : base(user, headerText, CounterVisibilityState.AlwaysVisible) { this.missingText = missingText; diff --git a/osu.Game/Overlays/Profile/Sections/ProfileSubsection.cs b/osu.Game/Overlays/Profile/Sections/ProfileSubsection.cs index a9d2448abf..5a17f0d8bb 100644 --- a/osu.Game/Overlays/Profile/Sections/ProfileSubsection.cs +++ b/osu.Game/Overlays/Profile/Sections/ProfileSubsection.cs @@ -20,9 +20,9 @@ namespace osu.Game.Overlays.Profile.Sections private ProfileSubsectionHeader header; - protected ProfileSubsection(Bindable user, LocalisableString headerText, CounterVisibilityState counterVisibilityState = CounterVisibilityState.AlwaysHidden) + protected ProfileSubsection(Bindable user, LocalisableString? headerText = null, CounterVisibilityState counterVisibilityState = CounterVisibilityState.AlwaysHidden) { - this.headerText = headerText; + this.headerText = headerText ?? string.Empty; this.counterVisibilityState = counterVisibilityState; User.BindTo(user); } @@ -38,7 +38,7 @@ namespace osu.Game.Overlays.Profile.Sections { header = new ProfileSubsectionHeader(headerText, counterVisibilityState) { - Alpha = string.IsNullOrEmpty(headerText) ? 0 : 1 + Alpha = string.IsNullOrEmpty(headerText.ToString()) ? 0 : 1 }, CreateContent() }; From 148eb890fff5707f28d586121db754561943d81d Mon Sep 17 00:00:00 2001 From: Lucas A Date: Sat, 17 Jul 2021 16:20:37 +0200 Subject: [PATCH 063/106] Localise Historical section. --- .../Sections/Historical/ChartProfileSubsection.cs | 5 +++-- .../Sections/Historical/DrawableMostPlayedBeatmap.cs | 3 ++- .../Historical/PaginatedMostPlayedBeatmapContainer.cs | 3 ++- .../Sections/Historical/PlayHistorySubsection.cs | 6 ++++-- .../Profile/Sections/Historical/ProfileLineChart.cs | 3 ++- .../Profile/Sections/Historical/ReplaysSubsection.cs | 6 ++++-- .../Profile/Sections/Historical/UserHistoryGraph.cs | 11 ++++++----- .../Overlays/Profile/Sections/HistoricalSection.cs | 2 +- 8 files changed, 24 insertions(+), 15 deletions(-) diff --git a/osu.Game/Overlays/Profile/Sections/Historical/ChartProfileSubsection.cs b/osu.Game/Overlays/Profile/Sections/Historical/ChartProfileSubsection.cs index a48036dcbb..986b3d9874 100644 --- a/osu.Game/Overlays/Profile/Sections/Historical/ChartProfileSubsection.cs +++ b/osu.Game/Overlays/Profile/Sections/Historical/ChartProfileSubsection.cs @@ -6,6 +6,7 @@ using System.Linq; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Localisation; using osu.Game.Users; using static osu.Game.Users.User; @@ -18,9 +19,9 @@ namespace osu.Game.Overlays.Profile.Sections.Historical /// /// Text describing the value being plotted on the graph, which will be displayed as a prefix to the value in the history graph tooltip. /// - protected abstract string GraphCounterName { get; } + protected abstract LocalisableString GraphCounterName { get; } - protected ChartProfileSubsection(Bindable user, string headerText) + protected ChartProfileSubsection(Bindable user, LocalisableString headerText) : base(user, headerText) { } diff --git a/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedBeatmap.cs b/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedBeatmap.cs index 6f1869966a..a419bef233 100644 --- a/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedBeatmap.cs +++ b/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedBeatmap.cs @@ -13,6 +13,7 @@ using osu.Game.Graphics.Sprites; using osuTK; using osu.Framework.Graphics.Cursor; using osu.Framework.Localisation; +using osu.Game.Resources.Localisation.Web; namespace osu.Game.Overlays.Profile.Sections.Historical { @@ -143,7 +144,7 @@ namespace osu.Game.Overlays.Profile.Sections.Historical private class PlayCountText : CompositeDrawable, IHasTooltip { - public LocalisableString TooltipText => "times played"; + public LocalisableString TooltipText => UsersStrings.ShowExtraHistoricalMostPlayedCount; public PlayCountText(int playCount) { diff --git a/osu.Game/Overlays/Profile/Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs b/osu.Game/Overlays/Profile/Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs index eeb14e5e4f..d0979526da 100644 --- a/osu.Game/Overlays/Profile/Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics.Containers; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Resources.Localisation.Web; using osu.Game.Users; namespace osu.Game.Overlays.Profile.Sections.Historical @@ -16,7 +17,7 @@ namespace osu.Game.Overlays.Profile.Sections.Historical public class PaginatedMostPlayedBeatmapContainer : PaginatedProfileSubsection { public PaginatedMostPlayedBeatmapContainer(Bindable user) - : base(user, "Most Played Beatmaps") + : base(user, UsersStrings.ShowExtraHistoricalMostPlayedTitle) { ItemsPerPage = 5; } diff --git a/osu.Game/Overlays/Profile/Sections/Historical/PlayHistorySubsection.cs b/osu.Game/Overlays/Profile/Sections/Historical/PlayHistorySubsection.cs index dfd29db693..83c005970e 100644 --- a/osu.Game/Overlays/Profile/Sections/Historical/PlayHistorySubsection.cs +++ b/osu.Game/Overlays/Profile/Sections/Historical/PlayHistorySubsection.cs @@ -2,6 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Bindables; +using osu.Framework.Localisation; +using osu.Game.Resources.Localisation.Web; using osu.Game.Users; using static osu.Game.Users.User; @@ -9,10 +11,10 @@ namespace osu.Game.Overlays.Profile.Sections.Historical { public class PlayHistorySubsection : ChartProfileSubsection { - protected override string GraphCounterName => "Plays"; + protected override LocalisableString GraphCounterName => UsersStrings.ShowExtraHistoricalMonthlyPlaycountsCountLabel; public PlayHistorySubsection(Bindable user) - : base(user, "Play History") + : base(user, UsersStrings.ShowExtraHistoricalMonthlyPlaycountsTitle) { } diff --git a/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs b/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs index eb5deb2802..af39251781 100644 --- a/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs +++ b/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs @@ -13,6 +13,7 @@ using osu.Game.Graphics; using osu.Framework.Graphics.Shapes; using osuTK; using static osu.Game.Users.User; +using osu.Framework.Localisation; namespace osu.Game.Overlays.Profile.Sections.Historical { @@ -42,7 +43,7 @@ namespace osu.Game.Overlays.Profile.Sections.Historical private readonly Container rowLinesContainer; private readonly Container columnLinesContainer; - public ProfileLineChart(string graphCounterName) + public ProfileLineChart(LocalisableString graphCounterName) { RelativeSizeAxes = Axes.X; Height = 250; diff --git a/osu.Game/Overlays/Profile/Sections/Historical/ReplaysSubsection.cs b/osu.Game/Overlays/Profile/Sections/Historical/ReplaysSubsection.cs index 1c28306f17..76d5f73bd7 100644 --- a/osu.Game/Overlays/Profile/Sections/Historical/ReplaysSubsection.cs +++ b/osu.Game/Overlays/Profile/Sections/Historical/ReplaysSubsection.cs @@ -2,6 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Bindables; +using osu.Framework.Localisation; +using osu.Game.Resources.Localisation.Web; using osu.Game.Users; using static osu.Game.Users.User; @@ -9,10 +11,10 @@ namespace osu.Game.Overlays.Profile.Sections.Historical { public class ReplaysSubsection : ChartProfileSubsection { - protected override string GraphCounterName => "Replays Watched"; + protected override LocalisableString GraphCounterName => UsersStrings.ShowExtraHistoricalReplaysWatchedCountsCountLabel; public ReplaysSubsection(Bindable user) - : base(user, "Replays Watched History") + : base(user, UsersStrings.ShowExtraHistoricalReplaysWatchedCountsTitle) { } diff --git a/osu.Game/Overlays/Profile/Sections/Historical/UserHistoryGraph.cs b/osu.Game/Overlays/Profile/Sections/Historical/UserHistoryGraph.cs index 52831b4243..d626c63fed 100644 --- a/osu.Game/Overlays/Profile/Sections/Historical/UserHistoryGraph.cs +++ b/osu.Game/Overlays/Profile/Sections/Historical/UserHistoryGraph.cs @@ -5,13 +5,14 @@ using System; using System.Collections.Generic; using System.Linq; using JetBrains.Annotations; +using osu.Framework.Localisation; using static osu.Game.Users.User; namespace osu.Game.Overlays.Profile.Sections.Historical { public class UserHistoryGraph : UserGraph { - private readonly string tooltipCounterName; + private readonly LocalisableString tooltipCounterName; [CanBeNull] public UserHistoryCount[] Values @@ -19,7 +20,7 @@ namespace osu.Game.Overlays.Profile.Sections.Historical set => Data = value?.Select(v => new KeyValuePair(v.Date, v.Count)).ToArray(); } - public UserHistoryGraph(string tooltipCounterName) + public UserHistoryGraph(LocalisableString tooltipCounterName) { this.tooltipCounterName = tooltipCounterName; } @@ -40,9 +41,9 @@ namespace osu.Game.Overlays.Profile.Sections.Historical protected class HistoryGraphTooltip : UserGraphTooltip { - private readonly string tooltipCounterName; + private readonly LocalisableString tooltipCounterName; - public HistoryGraphTooltip(string tooltipCounterName) + public HistoryGraphTooltip(LocalisableString tooltipCounterName) : base(tooltipCounterName) { this.tooltipCounterName = tooltipCounterName; @@ -61,7 +62,7 @@ namespace osu.Game.Overlays.Profile.Sections.Historical private class TooltipDisplayContent { - public string Name; + public LocalisableString Name; public string Count; public string Date; } diff --git a/osu.Game/Overlays/Profile/Sections/HistoricalSection.cs b/osu.Game/Overlays/Profile/Sections/HistoricalSection.cs index 09ca492aa9..cba25c0a8b 100644 --- a/osu.Game/Overlays/Profile/Sections/HistoricalSection.cs +++ b/osu.Game/Overlays/Profile/Sections/HistoricalSection.cs @@ -22,7 +22,7 @@ namespace osu.Game.Overlays.Profile.Sections { new PlayHistorySubsection(User), new PaginatedMostPlayedBeatmapContainer(User), - new PaginatedScoreContainer(ScoreType.Recent, User, "Recent Plays (24h)"), + new PaginatedScoreContainer(ScoreType.Recent, User, UsersStrings.ShowExtraHistoricalRecentPlaysTitle), new ReplaysSubsection(User) }; } From 2545275f7102e0607d81eaced1c00db66efb08c1 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Sat, 17 Jul 2021 16:29:09 +0200 Subject: [PATCH 064/106] Partly localise Kudosu section. --- osu.Game/Overlays/Profile/Sections/Kudosu/KudosuInfo.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Profile/Sections/Kudosu/KudosuInfo.cs b/osu.Game/Overlays/Profile/Sections/Kudosu/KudosuInfo.cs index cdb24b784c..37de669b3b 100644 --- a/osu.Game/Overlays/Profile/Sections/Kudosu/KudosuInfo.cs +++ b/osu.Game/Overlays/Profile/Sections/Kudosu/KudosuInfo.cs @@ -12,6 +12,8 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Users; using osu.Framework.Allocation; +using osu.Game.Resources.Localisation.Web; +using osu.Framework.Localisation; namespace osu.Game.Overlays.Profile.Sections.Kudosu { @@ -37,7 +39,7 @@ namespace osu.Game.Overlays.Profile.Sections.Kudosu private class CountTotal : CountSection { public CountTotal() - : base("Total Kudosu Earned") + : base(UsersStrings.ShowExtraKudosuTotal) { DescriptionText.AddText("Based on how much of a contribution the user has made to beatmap moderation. See "); DescriptionText.AddLink("this page", "https://osu.ppy.sh/wiki/Kudosu"); @@ -56,7 +58,7 @@ namespace osu.Game.Overlays.Profile.Sections.Kudosu set => valueText.Text = value.ToString("N0"); } - public CountSection(string header) + public CountSection(LocalisableString header) { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; From a7c280508f9480141da9b324edddf613e7c7c022 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Sat, 17 Jul 2021 16:40:37 +0200 Subject: [PATCH 065/106] Throw instead of silently returning. --- osu.Game/Overlays/Profile/ProfileHeader.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Profile/ProfileHeader.cs b/osu.Game/Overlays/Profile/ProfileHeader.cs index 32bca68f0e..083725418e 100644 --- a/osu.Game/Overlays/Profile/ProfileHeader.cs +++ b/osu.Game/Overlays/Profile/ProfileHeader.cs @@ -1,6 +1,7 @@ // 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.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -127,7 +128,7 @@ namespace osu.Game.Overlays.Profile return LayoutStrings.HeaderUsersModding; default: - return string.Empty; + throw new ArgumentOutOfRangeException(nameof(value), value, null); } } } From 77d8f240f8e33d75f2f92caf0c6d7e12b7465463 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Sat, 17 Jul 2021 16:41:42 +0200 Subject: [PATCH 066/106] Use ctor default values. --- .../Profile/Sections/Recent/PaginatedRecentActivityContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Profile/Sections/Recent/PaginatedRecentActivityContainer.cs b/osu.Game/Overlays/Profile/Sections/Recent/PaginatedRecentActivityContainer.cs index 04c8d7483b..db2e6bc1e0 100644 --- a/osu.Game/Overlays/Profile/Sections/Recent/PaginatedRecentActivityContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/Recent/PaginatedRecentActivityContainer.cs @@ -17,7 +17,7 @@ namespace osu.Game.Overlays.Profile.Sections.Recent public class PaginatedRecentActivityContainer : PaginatedProfileSubsection { public PaginatedRecentActivityContainer(Bindable user) - : base(user, string.Empty, EventsStrings.Empty) + : base(user, missingText: EventsStrings.Empty) { ItemsPerPage = 10; } From 28845364a3380980a101f984f38312d792f9f376 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Sat, 17 Jul 2021 16:52:35 +0200 Subject: [PATCH 067/106] Localise score weighting. --- .../Profile/Sections/Ranks/DrawableProfileWeightedScore.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileWeightedScore.cs b/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileWeightedScore.cs index 3afa79e59e..63305d004c 100644 --- a/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileWeightedScore.cs +++ b/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileWeightedScore.cs @@ -5,6 +5,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osu.Game.Resources.Localisation.Web; using osu.Game.Scoring; using osuTK; @@ -51,7 +52,7 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks new OsuSpriteText { Font = OsuFont.GetFont(size: 12), - Text = $@"weighted {weight:0%}" + Text = UsersStrings.ShowExtraTopRanksPpWeight(weight.ToString("0%")) } } }; From 2c26248042e58a6db18531fbbe00980e7fdada8f Mon Sep 17 00:00:00 2001 From: Lucas A Date: Sat, 17 Jul 2021 17:36:49 +0200 Subject: [PATCH 068/106] Localise missing text of `PaginatedKudosuHistoryContainer`. --- .../Profile/Sections/Kudosu/PaginatedKudosuHistoryContainer.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Profile/Sections/Kudosu/PaginatedKudosuHistoryContainer.cs b/osu.Game/Overlays/Profile/Sections/Kudosu/PaginatedKudosuHistoryContainer.cs index 008d89d881..76cd7ed722 100644 --- a/osu.Game/Overlays/Profile/Sections/Kudosu/PaginatedKudosuHistoryContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/Kudosu/PaginatedKudosuHistoryContainer.cs @@ -8,13 +8,14 @@ using osu.Framework.Bindables; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API; using System.Collections.Generic; +using osu.Game.Resources.Localisation.Web; namespace osu.Game.Overlays.Profile.Sections.Kudosu { public class PaginatedKudosuHistoryContainer : PaginatedProfileSubsection { public PaginatedKudosuHistoryContainer(Bindable user) - : base(user, missingText: "This user hasn't received any kudosu!") + : base(user, missingText: UsersStrings.ShowExtraKudosuEntryEmpty) { ItemsPerPage = 5; } From 9257cd7fad4c3883389d8620daffd78b1e18cee7 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Sun, 18 Jul 2021 19:18:06 +0200 Subject: [PATCH 069/106] Revert the use of an enum in `ProfileHeader`. --- osu.Game/Overlays/Profile/ProfileHeader.cs | 31 +++------------------- 1 file changed, 4 insertions(+), 27 deletions(-) diff --git a/osu.Game/Overlays/Profile/ProfileHeader.cs b/osu.Game/Overlays/Profile/ProfileHeader.cs index 083725418e..88c785d4e1 100644 --- a/osu.Game/Overlays/Profile/ProfileHeader.cs +++ b/osu.Game/Overlays/Profile/ProfileHeader.cs @@ -1,7 +1,6 @@ // 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.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -15,7 +14,7 @@ using osu.Game.Users; namespace osu.Game.Overlays.Profile { - public class ProfileHeader : TabControlOverlayHeader + public class ProfileHeader : TabControlOverlayHeader { private UserCoverBackground coverContainer; @@ -30,6 +29,9 @@ namespace osu.Game.Overlays.Profile User.ValueChanged += e => updateDisplay(e.NewValue); + TabControl.AddItem(LayoutStrings.HeaderUsersShow); + TabControl.AddItem(LayoutStrings.HeaderUsersModding); + centreHeaderContainer.DetailsVisible.BindValueChanged(visible => detailHeaderContainer.Expanded = visible.NewValue, true); } @@ -107,29 +109,4 @@ namespace osu.Game.Overlays.Profile protected override double LoadDelay => 0; } } - - [LocalisableEnum(typeof(ProfileHeaderTabEnumLocalisationMapper))] - public enum ProfileHeaderTab - { - Info, - Modding, - } - - public class ProfileHeaderTabEnumLocalisationMapper : EnumLocalisationMapper - { - public override LocalisableString Map(ProfileHeaderTab value) - { - switch (value) - { - case ProfileHeaderTab.Info: - return LayoutStrings.HeaderUsersShow; - - case ProfileHeaderTab.Modding: - return LayoutStrings.HeaderUsersModding; - - default: - throw new ArgumentOutOfRangeException(nameof(value), value, null); - } - } - } } From 80885301a391e57d5d413337e87b1495da7624f1 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Sun, 18 Jul 2021 19:36:34 +0200 Subject: [PATCH 070/106] Fix codefactor issues. --- osu.Game/Overlays/Profile/ProfileHeader.cs | 1 - .../Overlays/Profile/Sections/Historical/ProfileLineChart.cs | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Profile/ProfileHeader.cs b/osu.Game/Overlays/Profile/ProfileHeader.cs index 88c785d4e1..815f9fdafc 100644 --- a/osu.Game/Overlays/Profile/ProfileHeader.cs +++ b/osu.Game/Overlays/Profile/ProfileHeader.cs @@ -32,7 +32,6 @@ namespace osu.Game.Overlays.Profile TabControl.AddItem(LayoutStrings.HeaderUsersShow); TabControl.AddItem(LayoutStrings.HeaderUsersModding); - centreHeaderContainer.DetailsVisible.BindValueChanged(visible => detailHeaderContainer.Expanded = visible.NewValue, true); } diff --git a/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs b/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs index af39251781..e6fd09301e 100644 --- a/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs +++ b/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs @@ -12,8 +12,8 @@ using osu.Framework.Allocation; using osu.Game.Graphics; using osu.Framework.Graphics.Shapes; using osuTK; -using static osu.Game.Users.User; using osu.Framework.Localisation; +using static osu.Game.Users.User; namespace osu.Game.Overlays.Profile.Sections.Historical { From 3c028ce05cd619c58488793e9e26548ae07aeff3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 19 Jul 2021 12:38:22 +0900 Subject: [PATCH 071/106] Add `IDeepCloneable` interface and update existing `CreateCopy` methods to use it --- CodeAnalysis/BannedSymbols.txt | 1 + osu.Game.Tests/NonVisual/ControlPointInfoTest.cs | 4 ++-- .../Visual/UserInterface/TestSceneModSettings.cs | 4 ++-- osu.Game/Beatmaps/ControlPoints/ControlPoint.cs | 5 +++-- .../Beatmaps/ControlPoints/ControlPointInfo.cs | 7 ++++--- osu.Game/Online/Rooms/Room.cs | 5 +++-- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 2 +- .../Rulesets/Difficulty/DifficultyCalculator.cs | 2 +- osu.Game/Rulesets/Mods/Mod.cs | 4 ++-- osu.Game/Rulesets/Mods/MultiMod.cs | 2 +- osu.Game/Screens/Edit/Editor.cs | 2 +- .../OnlinePlay/Lounge/Components/DrawableRoom.cs | 2 +- .../Screens/OnlinePlay/OnlinePlaySongSelect.cs | 8 ++++---- .../OnlinePlay/Playlists/PlaylistsSongSelect.cs | 4 ++-- osu.Game/Screens/Play/Player.cs | 2 +- osu.Game/Utils/IDeepCloneable.cs | 16 ++++++++++++++++ 16 files changed, 45 insertions(+), 25 deletions(-) create mode 100644 osu.Game/Utils/IDeepCloneable.cs diff --git a/CodeAnalysis/BannedSymbols.txt b/CodeAnalysis/BannedSymbols.txt index 46c50dbfa2..ea3e25142c 100644 --- a/CodeAnalysis/BannedSymbols.txt +++ b/CodeAnalysis/BannedSymbols.txt @@ -3,6 +3,7 @@ M:System.Object.Equals(System.Object)~System.Boolean;Don't use object.Equals. Us M:System.ValueType.Equals(System.Object)~System.Boolean;Don't use object.Equals(Fallbacks to ValueType). Use IEquatable or EqualityComparer.Default instead. M:System.Nullable`1.Equals(System.Object)~System.Boolean;Use == instead. T:System.IComparable;Don't use non-generic IComparable. Use generic version instead. +T:SixLabors.ImageSharp.IDeepCloneable`1;Use osu.Game.Utils.IDeepCloneable instead. M:osu.Framework.Graphics.Sprites.SpriteText.#ctor;Use OsuSpriteText. M:osu.Framework.Bindables.IBindableList`1.GetBoundCopy();Fails on iOS. Use manual ctor + BindTo instead. (see https://github.com/mono/mono/issues/19900) T:Microsoft.EntityFrameworkCore.Internal.EnumerableExtensions;Don't use internal extension methods. diff --git a/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs b/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs index b27c257795..240ae4a90c 100644 --- a/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs +++ b/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs @@ -248,13 +248,13 @@ namespace osu.Game.Tests.NonVisual } [Test] - public void TestCreateCopyIsDeepClone() + public void TestDeepClone() { var cpi = new ControlPointInfo(); cpi.Add(1000, new TimingControlPoint { BeatLength = 500 }); - var cpiCopy = cpi.CreateCopy(); + var cpiCopy = cpi.DeepClone(); cpiCopy.Add(2000, new TimingControlPoint { BeatLength = 500 }); diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSettings.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSettings.cs index bda1973354..65db2e9644 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSettings.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSettings.cs @@ -88,7 +88,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("create mods", () => { original = new OsuModDoubleTime(); - copy = (OsuModDoubleTime)original.CreateCopy(); + copy = (OsuModDoubleTime)original.DeepClone(); }); AddStep("change property", () => original.SpeedChange.Value = 2); @@ -106,7 +106,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("create mods", () => { original = new MultiMod(new OsuModDoubleTime()); - copy = (MultiMod)original.CreateCopy(); + copy = (MultiMod)original.DeepClone(); }); AddStep("change property", () => ((OsuModDoubleTime)original.Mods[0]).SpeedChange.Value = 2); diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs index e8dc623ddb..643c5d9adb 100644 --- a/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs @@ -3,11 +3,12 @@ using System; using osu.Game.Graphics; +using osu.Game.Utils; using osuTK.Graphics; namespace osu.Game.Beatmaps.ControlPoints { - public abstract class ControlPoint : IComparable + public abstract class ControlPoint : IComparable, IDeepCloneable { /// /// The time at which the control point takes effect. @@ -32,7 +33,7 @@ namespace osu.Game.Beatmaps.ControlPoints /// /// Create an unbound copy of this control point. /// - public ControlPoint CreateCopy() + public ControlPoint DeepClone() { var copy = (ControlPoint)Activator.CreateInstance(GetType()); diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs index 25d0843a71..2d0fc17a7b 100644 --- a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs +++ b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs @@ -10,11 +10,12 @@ using osu.Framework.Bindables; using osu.Framework.Lists; using osu.Framework.Utils; using osu.Game.Screens.Edit; +using osu.Game.Utils; namespace osu.Game.Beatmaps.ControlPoints { [Serializable] - public class ControlPointInfo + public class ControlPointInfo : IDeepCloneable { /// /// All control points grouped by time. @@ -350,12 +351,12 @@ namespace osu.Game.Beatmaps.ControlPoints } } - public ControlPointInfo CreateCopy() + public ControlPointInfo DeepClone() { var controlPointInfo = new ControlPointInfo(); foreach (var point in AllControlPoints) - controlPointInfo.Add(point.Time, point.CreateCopy()); + controlPointInfo.Add(point.Time, point.DeepClone()); return controlPointInfo; } diff --git a/osu.Game/Online/Rooms/Room.cs b/osu.Game/Online/Rooms/Room.cs index b28680ffef..863fefd55c 100644 --- a/osu.Game/Online/Rooms/Room.cs +++ b/osu.Game/Online/Rooms/Room.cs @@ -10,10 +10,11 @@ using osu.Game.IO.Serialization.Converters; using osu.Game.Online.Rooms.GameTypes; using osu.Game.Online.Rooms.RoomStatuses; using osu.Game.Users; +using osu.Game.Utils; namespace osu.Game.Online.Rooms { - public class Room + public class Room : IDeepCloneable { [Cached] [JsonProperty("id")] @@ -120,7 +121,7 @@ namespace osu.Game.Online.Rooms /// Create a copy of this room without online information. /// Should be used to create a local copy of a room for submitting in the future. /// - public Room CreateCopy() + public Room DeepClone() { var copy = new Room(); diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 98a79a62c8..793bb79318 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -429,7 +429,7 @@ namespace osu.Game.Overlays.Mods if (!Stacked) modEnumeration = ModUtils.FlattenMods(modEnumeration); - section.Mods = modEnumeration.Select(getValidModOrNull).Where(m => m != null).Select(m => m.CreateCopy()); + section.Mods = modEnumeration.Select(getValidModOrNull).Where(m => m != null).Select(m => m.DeepClone()); } updateSelectedButtons(); diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs index 3cc69bd85b..224c9178ae 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs @@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Difficulty /// A structure describing the difficulty of the beatmap. public DifficultyAttributes Calculate(params Mod[] mods) { - mods = mods.Select(m => m.CreateCopy()).ToArray(); + mods = mods.Select(m => m.DeepClone()).ToArray(); IBeatmap playableBeatmap = beatmap.GetPlayableBeatmap(ruleset.RulesetInfo, mods); diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index 6f00bb6c75..f2fd02c652 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Mods /// The base class for gameplay modifiers. /// [ExcludeFromDynamicCompile] - public abstract class Mod : IMod, IEquatable, IJsonSerializable + public abstract class Mod : IMod, IEquatable, IJsonSerializable, IDeepCloneable { /// /// The name of this mod. @@ -132,7 +132,7 @@ namespace osu.Game.Rulesets.Mods /// /// Creates a copy of this initialised to a default state. /// - public virtual Mod CreateCopy() + public virtual Mod DeepClone() { var result = (Mod)Activator.CreateInstance(GetType()); result.CopyFrom(this); diff --git a/osu.Game/Rulesets/Mods/MultiMod.cs b/osu.Game/Rulesets/Mods/MultiMod.cs index 2107009dbb..1c41c6b8b3 100644 --- a/osu.Game/Rulesets/Mods/MultiMod.cs +++ b/osu.Game/Rulesets/Mods/MultiMod.cs @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Mods Mods = mods; } - public override Mod CreateCopy() => new MultiMod(Mods.Select(m => m.CreateCopy()).ToArray()); + public override Mod DeepClone() => new MultiMod(Mods.Select(m => m.DeepClone()).ToArray()); public override Type[] IncompatibleMods => Mods.SelectMany(m => m.IncompatibleMods).ToArray(); } diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 986a4efb28..71dd47b058 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -128,7 +128,7 @@ namespace osu.Game.Screens.Edit // clone these locally for now to avoid incurring overhead on GetPlayableBeatmap usages. // eventually we will want to improve how/where this is done as there are issues with *not* cloning it in all cases. - playableBeatmap.ControlPointInfo = playableBeatmap.ControlPointInfo.CreateCopy(); + playableBeatmap.ControlPointInfo = playableBeatmap.ControlPointInfo.DeepClone(); } catch (Exception e) { diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs index 35782c6104..b4b1a09c4f 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs @@ -242,7 +242,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components { new OsuMenuItem("Create copy", MenuItemType.Standard, () => { - parentScreen?.OpenNewRoom(Room.CreateCopy()); + parentScreen?.OpenNewRoom(Room.DeepClone()); }) }; } diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs index 2c46f76737..be28de5c43 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs @@ -72,8 +72,8 @@ namespace osu.Game.Screens.OnlinePlay // At this point, Mods contains both the required and allowed mods. For selection purposes, it should only contain the required mods. // Similarly, freeMods is currently empty but should only contain the allowed mods. - Mods.Value = selectedItem?.Value?.RequiredMods.Select(m => m.CreateCopy()).ToArray() ?? Array.Empty(); - FreeMods.Value = selectedItem?.Value?.AllowedMods.Select(m => m.CreateCopy()).ToArray() ?? Array.Empty(); + Mods.Value = selectedItem?.Value?.RequiredMods.Select(m => m.DeepClone()).ToArray() ?? Array.Empty(); + FreeMods.Value = selectedItem?.Value?.AllowedMods.Select(m => m.DeepClone()).ToArray() ?? Array.Empty(); Mods.BindValueChanged(onModsChanged); Ruleset.BindValueChanged(onRulesetChanged); @@ -108,8 +108,8 @@ namespace osu.Game.Screens.OnlinePlay } }; - item.RequiredMods.AddRange(Mods.Value.Select(m => m.CreateCopy())); - item.AllowedMods.AddRange(FreeMods.Value.Select(m => m.CreateCopy())); + item.RequiredMods.AddRange(Mods.Value.Select(m => m.DeepClone())); + item.AllowedMods.AddRange(FreeMods.Value.Select(m => m.DeepClone())); SelectItem(item); return true; diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs index 21335fc90c..076fa77336 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs @@ -55,10 +55,10 @@ namespace osu.Game.Screens.OnlinePlay.Playlists item.Ruleset.Value = Ruleset.Value; item.RequiredMods.Clear(); - item.RequiredMods.AddRange(Mods.Value.Select(m => m.CreateCopy())); + item.RequiredMods.AddRange(Mods.Value.Select(m => m.DeepClone())); item.AllowedMods.Clear(); - item.AllowedMods.AddRange(FreeMods.Value.Select(m => m.CreateCopy())); + item.AllowedMods.AddRange(FreeMods.Value.Select(m => m.DeepClone())); } } } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 97854ee12f..51f1dbd121 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -184,7 +184,7 @@ namespace osu.Game.Screens.Play [BackgroundDependencyLoader(true)] private void load(AudioManager audio, OsuConfigManager config, OsuGameBase game) { - Mods.Value = base.Mods.Value.Select(m => m.CreateCopy()).ToArray(); + Mods.Value = base.Mods.Value.Select(m => m.DeepClone()).ToArray(); if (Beatmap.Value is DummyWorkingBeatmap) return; diff --git a/osu.Game/Utils/IDeepCloneable.cs b/osu.Game/Utils/IDeepCloneable.cs new file mode 100644 index 0000000000..6877f346c4 --- /dev/null +++ b/osu.Game/Utils/IDeepCloneable.cs @@ -0,0 +1,16 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Utils +{ + /// A generic interface for a deeply cloneable type. + /// The type of object to clone. + public interface IDeepCloneable where T : class + { + /// + /// Creates a new that is a deep copy of the current instance. + /// + /// The . + T DeepClone(); + } +} From be495a74887f12011df92911f1a72ff6d8b1b59e Mon Sep 17 00:00:00 2001 From: ekrctb Date: Mon, 19 Jul 2021 13:03:53 +0900 Subject: [PATCH 072/106] Add `JuiceStreamPath` to allow editing `JuiceStream` in catch editor A `JuiceStream` holds a legacy `SliderPath` as the representation of the path. However, the `SliderPath` representation is difficult to work with because `SliderPath` works in 2D position, while osu!catch works in `(time, x)` coordinates. This `JuiceStreamPath` represents the path in a more convenient way, a polyline connecting list of `(distance, x)` vertices. --- .../Objects/JuiceStreamPath.cs | 340 ++++++++++++++++++ .../Objects/JuiceStreamPathVertex.cs | 33 ++ 2 files changed, 373 insertions(+) create mode 100644 osu.Game.Rulesets.Catch/Objects/JuiceStreamPath.cs create mode 100644 osu.Game.Rulesets.Catch/Objects/JuiceStreamPathVertex.cs diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStreamPath.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStreamPath.cs new file mode 100644 index 0000000000..ac11bd9918 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Objects/JuiceStreamPath.cs @@ -0,0 +1,340 @@ +// 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.Utils; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osuTK; + +#nullable enable + +namespace osu.Game.Rulesets.Catch.Objects +{ + /// + /// Represents the path of a juice stream. + /// + /// A holds a legacy as the representation of the path. + /// However, the representation is difficult to work with. + /// This represents the path in a more convenient way, a polyline connecting list of s. + /// + /// + /// The path can be regarded as a function from the closed interval [Vertices[0].Distance, Vertices[^1].Distance] to the x position, given by . + /// To ensure the path is convertible to a , the slope of the function must not be more than 1 everywhere, + /// and this slope condition is always maintained as an invariant. + /// + /// + public class JuiceStreamPath + { + /// + /// The list of vertices of the path, which is represented as a polyline connecting the vertices. + /// + public IReadOnlyList Vertices => vertices; + + /// + /// The current version number. + /// This starts from 1 and incremented whenever this is modified. + /// + public int InvalidationID { get; private set; } = 1; + + /// + /// The difference between first vertex's and last vertex's . + /// + public double Distance => vertices[^1].Distance - vertices[0].Distance; + + /// + /// This list should always be non-empty. + /// + private readonly List vertices = new List + { + new JuiceStreamPathVertex() + }; + + /// + /// Compute the x-position of the path at the given . + /// + /// + /// When the given distance is outside of the path, the x position at the corresponding endpoint is returned, + /// + public float PositionAtDistance(double distance) + { + int index = vertexIndexAtDistance(distance); + return positionAtDistance(distance, index); + } + + /// + /// Remove all vertices of this path, then add a new vertex (0, 0). + /// + public void Clear() + { + vertices.Clear(); + vertices.Add(new JuiceStreamPathVertex()); + invalidate(); + } + + /// + /// Insert a vertex at given . + /// The is used as the position of the new vertex. + /// Thus, the set of points of the path is not changed (up to floating-point precision). + /// + /// The index of the new vertex. + public int InsertVertex(double distance) + { + if (!double.IsFinite(distance)) + throw new ArgumentOutOfRangeException(nameof(distance)); + + int index = vertexIndexAtDistance(distance); + float x = positionAtDistance(distance, index); + vertices.Insert(index, new JuiceStreamPathVertex(distance, x)); + + invalidate(); + return index; + } + + /// + /// Move the vertex of given to the given position . + /// When the distances between vertices are too small for the new vertex positions, the adjacent vertices are moved towards . + /// + public void SetVertexPosition(int index, float newX) + { + if (index < 0 || index >= vertices.Count) + throw new ArgumentOutOfRangeException(nameof(index)); + + if (!float.IsFinite(newX)) + throw new ArgumentOutOfRangeException(nameof(newX)); + + var newVertex = new JuiceStreamPathVertex(vertices[index].Distance, newX); + + for (int i = index - 1; i >= 0 && !canConnect(vertices[i], newVertex); i--) + { + float clampedX = clampToConnectablePosition(newVertex, vertices[i]); + vertices[i] = new JuiceStreamPathVertex(vertices[i].Distance, clampedX); + } + + for (int i = index + 1; i < vertices.Count; i++) + { + float clampedX = clampToConnectablePosition(newVertex, vertices[i]); + vertices[i] = new JuiceStreamPathVertex(vertices[i].Distance, clampedX); + } + + vertices[index] = newVertex; + + invalidate(); + } + + /// + /// Add a new vertex at given and position. + /// Adjacent vertices are moved when necessary in the same way as . + /// + public void Add(double distance, float x) + { + int index = InsertVertex(distance); + SetVertexPosition(index, x); + } + + /// + /// Remove all vertices that satisfy the given . + /// + /// + /// If all vertices are removed, a new vertex (0, 0) is added. + /// + /// The predicate to determine whether a vertex should be removed given the vertex and its index in the path. + /// The number of removed vertices. + public int RemoveVertices(Func predicate) + { + int index = 0; + int removeCount = vertices.RemoveAll(vertex => predicate(vertex, index++)); + + if (vertices.Count == 0) + vertices.Add(new JuiceStreamPathVertex()); + + if (removeCount != 0) + invalidate(); + + return removeCount; + } + + /// + /// Recreate this path by using difference set of vertices at given distances. + /// In addition to the given , the first vertex and the last vertex are always added to the new path. + /// New vertices use the positions on the original path. Thus, s at are preserved. + /// + public void ResampleVertices(IEnumerable sampleDistances) + { + var sampledVertices = new List(); + + foreach (double distance in sampleDistances) + { + if (!double.IsFinite(distance)) + throw new ArgumentOutOfRangeException(nameof(sampleDistances)); + + double clampedDistance = Math.Clamp(distance, vertices[0].Distance, vertices[^1].Distance); + float x = PositionAtDistance(clampedDistance); + sampledVertices.Add(new JuiceStreamPathVertex(clampedDistance, x)); + } + + sampledVertices.Sort(); + + // The first vertex and the last vertex are always used in the result. + vertices.RemoveRange(1, vertices.Count - (vertices.Count == 1 ? 1 : 2)); + vertices.InsertRange(1, sampledVertices); + + invalidate(); + } + + /// + /// Convert a to list of vertices and write the result to this . + /// + /// + /// Duplicated vertices are automatically removed. + /// + public void ConvertFromSliderPath(SliderPath sliderPath) + { + var sliderPathVertices = new List(); + sliderPath.GetPathToProgress(sliderPathVertices, 0, 1); + + double distance = 0; + + vertices.Clear(); + vertices.Add(new JuiceStreamPathVertex(0, sliderPathVertices.FirstOrDefault().X)); + + for (int i = 1; i < sliderPathVertices.Count; i++) + { + distance += Vector2.Distance(sliderPathVertices[i - 1], sliderPathVertices[i]); + + if (!Precision.AlmostEquals(vertices[^1].Distance, distance)) + vertices.Add(new JuiceStreamPathVertex(distance, sliderPathVertices[i].X)); + } + + invalidate(); + } + + /// + /// The height of legacy osu!standard playfield. + /// The sliders converted by are vertically contained in this height. + /// + public const float OSU_PLAYFIELD_HEIGHT = 384; + + /// + /// Convert the path of this to a and write the result to . + /// The resulting slider is "folded" to make it vertically contained in the playfield `(0..)` assuming the slider start position is . + /// + public void ConvertToSliderPath(SliderPath sliderPath, float sliderStartY) + { + const float margin = 1; + + // Note: these two variables and `sliderPath` are modified by the local functions. + double currentDistance = 0; + Vector2 lastPosition = new Vector2(vertices[0].X, 0); + + sliderPath.ControlPoints.Clear(); + sliderPath.ControlPoints.Add(new PathControlPoint(lastPosition)); + + for (int i = 1; i < vertices.Count; i++) + { + sliderPath.ControlPoints[^1].Type.Value = PathType.Linear; + + float deltaX = vertices[i].X - lastPosition.X; + double length = vertices[i].Distance - currentDistance; + + // Should satisfy `deltaX^2 + deltaY^2 = length^2`. + // By invariants, the expression inside the `sqrt` is (almost) non-negative. + double deltaY = Math.Sqrt(Math.Max(0, length * length - (double)deltaX * deltaX)); + + // When `deltaY` is small, one segment is always enough. + // This case is handled separately to prevent divide-by-zero. + if (deltaY <= OSU_PLAYFIELD_HEIGHT / 2 - margin) + { + float nextX = vertices[i].X; + float nextY = (float)(lastPosition.Y + getYDirection() * deltaY); + addControlPoint(nextX, nextY); + continue; + } + + // When `deltaY` is large or when the slider velocity is fast, the segment must be partitioned to subsegments to stay in bounds. + for (double currentProgress = 0; currentProgress < deltaY;) + { + double nextProgress = Math.Min(currentProgress + getMaxDeltaY(), deltaY); + float nextX = (float)(vertices[i - 1].X + nextProgress / deltaY * deltaX); + float nextY = (float)(lastPosition.Y + getYDirection() * (nextProgress - currentProgress)); + addControlPoint(nextX, nextY); + currentProgress = nextProgress; + } + } + + int getYDirection() + { + float lastSliderY = sliderStartY + lastPosition.Y; + return lastSliderY < OSU_PLAYFIELD_HEIGHT / 2 ? 1 : -1; + } + + float getMaxDeltaY() + { + float lastSliderY = sliderStartY + lastPosition.Y; + return Math.Max(lastSliderY, OSU_PLAYFIELD_HEIGHT - lastSliderY) - margin; + } + + void addControlPoint(float nextX, float nextY) + { + Vector2 nextPosition = new Vector2(nextX, nextY); + sliderPath.ControlPoints.Add(new PathControlPoint(nextPosition)); + currentDistance += Vector2.Distance(lastPosition, nextPosition); + lastPosition = nextPosition; + } + } + + /// + /// Find the index at which a new vertex with can be inserted. + /// + private int vertexIndexAtDistance(double distance) + { + // The position of `(distance, Infinity)` is uniquely determined because infinite positions are not allowed. + int i = vertices.BinarySearch(new JuiceStreamPathVertex(distance, float.PositiveInfinity)); + return i < 0 ? ~i : i; + } + + /// + /// Compute the position at the given , assuming is the vertex index returned by . + /// + private float positionAtDistance(double distance, int index) + { + if (index <= 0) + return vertices[0].X; + if (index >= vertices.Count) + return vertices[^1].X; + + double length = vertices[index].Distance - vertices[index - 1].Distance; + if (Precision.AlmostEquals(length, 0)) + return vertices[index].X; + + float deltaX = vertices[index].X - vertices[index - 1].X; + + return (float)(vertices[index - 1].X + deltaX * ((distance - vertices[index - 1].Distance) / length)); + } + + /// + /// Check the two vertices can connected directly while satisfying the slope condition. + /// + private bool canConnect(JuiceStreamPathVertex vertex1, JuiceStreamPathVertex vertex2, float allowance = 0) + { + double xDistance = Math.Abs((double)vertex2.X - vertex1.X); + float length = (float)Math.Abs(vertex2.Distance - vertex1.Distance); + return xDistance <= length + allowance; + } + + /// + /// Move the position of towards the position of + /// until the vertex pair satisfies the condition . + /// + /// The resulting position of . + private float clampToConnectablePosition(JuiceStreamPathVertex fixedVertex, JuiceStreamPathVertex movableVertex) + { + float length = (float)Math.Abs(movableVertex.Distance - fixedVertex.Distance); + return Math.Clamp(movableVertex.X, fixedVertex.X - length, fixedVertex.X + length); + } + + private void invalidate() => InvalidationID++; + } +} diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStreamPathVertex.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStreamPathVertex.cs new file mode 100644 index 0000000000..58c50603c4 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Objects/JuiceStreamPathVertex.cs @@ -0,0 +1,33 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; + +#nullable enable + +namespace osu.Game.Rulesets.Catch.Objects +{ + /// + /// A vertex of a . + /// + public readonly struct JuiceStreamPathVertex : IComparable + { + public readonly double Distance; + + public readonly float X; + + public JuiceStreamPathVertex(double distance, float x) + { + Distance = distance; + X = x; + } + + public int CompareTo(JuiceStreamPathVertex other) + { + int c = Distance.CompareTo(other.Distance); + return c != 0 ? c : X.CompareTo(other.X); + } + + public override string ToString() => $"({Distance}, {X})"; + } +} From fffe0d2e5735d68c7beef525b5c134fe5822ba69 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Mon, 19 Jul 2021 13:09:33 +0900 Subject: [PATCH 073/106] Add tests of `JuiceStreamPath`. "Property testing" is heavily used, tests that generates random cases and asserting properties. That gives high confidence of round-trip correctness of `SliderPath` conversion, for example. --- .../JuiceStreamPathTest.cs | 288 ++++++++++++++++++ 1 file changed, 288 insertions(+) create mode 100644 osu.Game.Rulesets.Catch.Tests/JuiceStreamPathTest.cs diff --git a/osu.Game.Rulesets.Catch.Tests/JuiceStreamPathTest.cs b/osu.Game.Rulesets.Catch.Tests/JuiceStreamPathTest.cs new file mode 100644 index 0000000000..5e4b6d9e1a --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/JuiceStreamPathTest.cs @@ -0,0 +1,288 @@ +// 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 NUnit.Framework; +using osu.Framework.Utils; +using osu.Game.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osuTK; + +namespace osu.Game.Rulesets.Catch.Tests +{ + [TestFixture] + public class JuiceStreamPathTest + { + [TestCase(1e3, true, false)] + // When the coordinates are large, the slope invariant fails within the specified absolute allowance due to the floating-number precision. + [TestCase(1e9, false, false)] + // Using discrete values sometimes discover more edge cases. + [TestCase(10, true, true)] + public void TestRandomInsertSetPosition(double scale, bool checkSlope, bool integralValues) + { + var rng = new Random(1); + var path = new JuiceStreamPath(); + + for (int iteration = 0; iteration < 100000; iteration++) + { + if (rng.Next(10) == 0) + path.Clear(); + + int vertexCount = path.Vertices.Count; + + switch (rng.Next(2)) + { + case 0: + { + double distance = rng.NextDouble() * scale * 2 - scale; + if (integralValues) + distance = Math.Round(distance); + + float oldX = path.PositionAtDistance(distance); + int index = path.InsertVertex(distance); + Assert.That(path.Vertices.Count, Is.EqualTo(vertexCount + 1)); + Assert.That(path.Vertices[index].Distance, Is.EqualTo(distance)); + Assert.That(path.Vertices[index].X, Is.EqualTo(oldX)); + break; + } + + case 1: + { + int index = rng.Next(path.Vertices.Count); + double distance = path.Vertices[index].Distance; + float newX = (float)(rng.NextDouble() * scale * 2 - scale); + if (integralValues) + newX = MathF.Round(newX); + + path.SetVertexPosition(index, newX); + Assert.That(path.Vertices.Count, Is.EqualTo(vertexCount)); + Assert.That(path.Vertices[index].Distance, Is.EqualTo(distance)); + Assert.That(path.Vertices[index].X, Is.EqualTo(newX)); + break; + } + } + + assertInvariants(path.Vertices, checkSlope); + } + } + + [Test] + public void TestRemoveVertices() + { + var path = new JuiceStreamPath(); + path.Add(10, 5); + path.Add(20, -5); + + int removeCount = path.RemoveVertices((v, i) => v.Distance == 10 && i == 1); + Assert.That(removeCount, Is.EqualTo(1)); + Assert.That(path.Vertices, Is.EqualTo(new[] + { + new JuiceStreamPathVertex(0, 0), + new JuiceStreamPathVertex(20, -5) + })); + + removeCount = path.RemoveVertices((_, i) => i == 0); + Assert.That(removeCount, Is.EqualTo(1)); + Assert.That(path.Vertices, Is.EqualTo(new[] + { + new JuiceStreamPathVertex(20, -5) + })); + + removeCount = path.RemoveVertices((_, i) => true); + Assert.That(removeCount, Is.EqualTo(1)); + Assert.That(path.Vertices, Is.EqualTo(new[] + { + new JuiceStreamPathVertex() + })); + } + + [Test] + public void TestResampleVertices() + { + var path = new JuiceStreamPath(); + path.Add(-100, -10); + path.Add(100, 50); + path.ResampleVertices(new double[] + { + -50, + 0, + 70, + 120 + }); + Assert.That(path.Vertices, Is.EqualTo(new[] + { + new JuiceStreamPathVertex(-100, -10), + new JuiceStreamPathVertex(-50, -5), + new JuiceStreamPathVertex(0, 0), + new JuiceStreamPathVertex(70, 35), + new JuiceStreamPathVertex(100, 50), + new JuiceStreamPathVertex(100, 50), + })); + + path.Clear(); + path.SetVertexPosition(0, 10); + path.ResampleVertices(Array.Empty()); + Assert.That(path.Vertices, Is.EqualTo(new[] + { + new JuiceStreamPathVertex(0, 10) + })); + } + + [Test] + public void TestRandomConvertFromSliderPath() + { + var rng = new Random(1); + var path = new JuiceStreamPath(); + var sliderPath = new SliderPath(); + + for (int iteration = 0; iteration < 10000; iteration++) + { + sliderPath.ControlPoints.Clear(); + + do + { + int start = sliderPath.ControlPoints.Count; + + do + { + float x = (float)(rng.NextDouble() * 1e3); + float y = (float)(rng.NextDouble() * 1e3); + sliderPath.ControlPoints.Add(new PathControlPoint(new Vector2(x, y))); + } while (rng.Next(2) != 0); + + int length = sliderPath.ControlPoints.Count - start + 1; + sliderPath.ControlPoints[start].Type.Value = length <= 2 ? PathType.Linear : length == 3 ? PathType.PerfectCurve : PathType.Bezier; + } while (rng.Next(3) != 0); + + if (rng.Next(5) == 0) + sliderPath.ExpectedDistance.Value = rng.NextDouble() * 3e3; + else + sliderPath.ExpectedDistance.Value = null; + + path.ConvertFromSliderPath(sliderPath); + Assert.That(path.Vertices[0].Distance, Is.EqualTo(0)); + Assert.That(path.Distance, Is.EqualTo(sliderPath.Distance).Within(1e-3)); + assertInvariants(path.Vertices, true); + + double[] sampleDistances = Enumerable.Range(0, 10) + .Select(_ => rng.NextDouble() * sliderPath.Distance) + .ToArray(); + + foreach (double distance in sampleDistances) + { + float expected = sliderPath.PositionAt(distance / sliderPath.Distance).X; + Assert.That(path.PositionAtDistance(distance), Is.EqualTo(expected).Within(1e-3)); + } + + path.ResampleVertices(sampleDistances); + assertInvariants(path.Vertices, true); + + foreach (double distance in sampleDistances) + { + float expected = sliderPath.PositionAt(distance / sliderPath.Distance).X; + Assert.That(path.PositionAtDistance(distance), Is.EqualTo(expected).Within(1e-3)); + } + } + } + + [Test] + public void TestRandomConvertToSliderPath() + { + var rng = new Random(1); + var path = new JuiceStreamPath(); + var sliderPath = new SliderPath(); + + for (int iteration = 0; iteration < 10000; iteration++) + { + path.Clear(); + + do + { + double distance = rng.NextDouble() * 1e3; + float x = (float)(rng.NextDouble() * 1e3); + path.Add(distance, x); + } while (rng.Next(5) != 0); + + float sliderStartY = (float)(rng.NextDouble() * JuiceStreamPath.OSU_PLAYFIELD_HEIGHT); + + path.ConvertToSliderPath(sliderPath, sliderStartY); + Assert.That(sliderPath.Distance, Is.EqualTo(path.Distance).Within(1e-3)); + Assert.That(sliderPath.ControlPoints[0].Position.Value.X, Is.EqualTo(path.Vertices[0].X)); + assertInvariants(path.Vertices, true); + + foreach (var point in sliderPath.ControlPoints) + { + Assert.That(point.Type.Value, Is.EqualTo(PathType.Linear).Or.Null); + Assert.That(sliderStartY + point.Position.Value.Y, Is.InRange(0, JuiceStreamPath.OSU_PLAYFIELD_HEIGHT)); + } + + for (int i = 0; i < 10; i++) + { + double distance = rng.NextDouble() * path.Distance; + float expected = path.PositionAtDistance(distance); + Assert.That(sliderPath.PositionAt(distance / sliderPath.Distance).X, Is.EqualTo(expected).Within(1e-3)); + } + } + } + + [Test] + public void TestInvalidation() + { + var path = new JuiceStreamPath(); + Assert.That(path.InvalidationID, Is.EqualTo(1)); + int previousId = path.InvalidationID; + + path.InsertVertex(10); + checkNewId(); + + path.SetVertexPosition(1, 5); + checkNewId(); + + path.Add(20, 0); + checkNewId(); + + path.RemoveVertices((v, _) => v.Distance == 20); + checkNewId(); + + path.ResampleVertices(new double[] { 5, 10, 15 }); + checkNewId(); + + path.Clear(); + checkNewId(); + + path.ConvertFromSliderPath(new SliderPath()); + checkNewId(); + + void checkNewId() + { + Assert.That(path.InvalidationID, Is.Not.EqualTo(previousId)); + previousId = path.InvalidationID; + } + } + + private void assertInvariants(IReadOnlyList vertices, bool checkSlope) + { + Assert.That(vertices, Is.Not.Empty); + + for (int i = 0; i < vertices.Count; i++) + { + Assert.That(double.IsFinite(vertices[i].Distance)); + Assert.That(float.IsFinite(vertices[i].X)); + } + + for (int i = 1; i < vertices.Count; i++) + { + Assert.That(vertices[i].Distance, Is.GreaterThanOrEqualTo(vertices[i - 1].Distance)); + + if (!checkSlope) continue; + + float xDiff = Math.Abs(vertices[i].X - vertices[i - 1].X); + double distanceDiff = vertices[i].Distance - vertices[i - 1].Distance; + Assert.That(xDiff, Is.LessThanOrEqualTo(distanceDiff).Within(Precision.FLOAT_EPSILON)); + } + } + } +} From e507faef294748c4aac5686aef06fa343e83069b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 19 Jul 2021 13:02:40 +0900 Subject: [PATCH 074/106] Add deep cloning support to `Score`/`ScoreInfo`/`Replay` --- osu.Game.Tests/NonVisual/ScoreInfoTest.cs | 33 +++++++++++++++++++++++ osu.Game/Replays/Replay.cs | 14 +++++++++- osu.Game/Scoring/Score.cs | 12 ++++++++- osu.Game/Scoring/ScoreInfo.cs | 11 +++++++- 4 files changed, 67 insertions(+), 3 deletions(-) create mode 100644 osu.Game.Tests/NonVisual/ScoreInfoTest.cs diff --git a/osu.Game.Tests/NonVisual/ScoreInfoTest.cs b/osu.Game.Tests/NonVisual/ScoreInfoTest.cs new file mode 100644 index 0000000000..6e5718cd4c --- /dev/null +++ b/osu.Game.Tests/NonVisual/ScoreInfoTest.cs @@ -0,0 +1,33 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; + +namespace osu.Game.Tests.NonVisual +{ + [TestFixture] + public class ScoreInfoTest + { + [Test] + public void TestDeepClone() + { + var score = new ScoreInfo(); + + score.Statistics.Add(HitResult.Good, 10); + score.Rank = ScoreRank.B; + + var scoreCopy = score.DeepClone(); + + score.Statistics[HitResult.Good]++; + score.Rank = ScoreRank.X; + + Assert.That(scoreCopy.Statistics[HitResult.Good], Is.EqualTo(10)); + Assert.That(score.Statistics[HitResult.Good], Is.EqualTo(11)); + + Assert.That(scoreCopy.Rank, Is.EqualTo(ScoreRank.B)); + Assert.That(score.Rank, Is.EqualTo(ScoreRank.X)); + } + } +} diff --git a/osu.Game/Replays/Replay.cs b/osu.Game/Replays/Replay.cs index 5430915394..30e176b5c7 100644 --- a/osu.Game/Replays/Replay.cs +++ b/osu.Game/Replays/Replay.cs @@ -2,11 +2,13 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using System.Linq; using osu.Game.Rulesets.Replays; +using osu.Game.Utils; namespace osu.Game.Replays { - public class Replay + public class Replay : IDeepCloneable { /// /// Whether all frames for this replay have been received. @@ -15,5 +17,15 @@ namespace osu.Game.Replays public bool HasReceivedAllFrames = true; public List Frames = new List(); + + public Replay DeepClone() + { + return new Replay + { + HasReceivedAllFrames = HasReceivedAllFrames, + // individual frames are mutable for now but hopefully this will not be a thing in the future. + Frames = Frames.ToList(), + }; + } } } diff --git a/osu.Game/Scoring/Score.cs b/osu.Game/Scoring/Score.cs index 4e82b1584e..83e4389dc8 100644 --- a/osu.Game/Scoring/Score.cs +++ b/osu.Game/Scoring/Score.cs @@ -2,12 +2,22 @@ // See the LICENCE file in the repository root for full licence text. using osu.Game.Replays; +using osu.Game.Utils; namespace osu.Game.Scoring { - public class Score + public class Score : IDeepCloneable { public ScoreInfo ScoreInfo = new ScoreInfo(); public Replay Replay = new Replay(); + + public Score DeepClone() + { + return new Score + { + ScoreInfo = ScoreInfo.DeepClone(), + Replay = Replay.DeepClone(), + }; + } } } diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index 1d22dab0af..a0c4d5a026 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -18,7 +18,7 @@ using osu.Game.Utils; namespace osu.Game.Scoring { - public class ScoreInfo : IHasFiles, IHasPrimaryKey, ISoftDelete, IEquatable + public class ScoreInfo : IHasFiles, IHasPrimaryKey, ISoftDelete, IEquatable, IDeepCloneable { public int ID { get; set; } @@ -242,6 +242,15 @@ namespace osu.Game.Scoring } } + public ScoreInfo DeepClone() + { + var clone = (ScoreInfo)MemberwiseClone(); + + clone.Statistics = new Dictionary(clone.Statistics); + + return clone; + } + public override string ToString() => $"{User} playing {Beatmap}"; public bool Equals(ScoreInfo other) From caba78cb5d0ebd67053537e634d613fae733933c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 19 Jul 2021 13:04:16 +0900 Subject: [PATCH 075/106] Copy score during submission process to ensure it isn't modified --- osu.Game/Screens/Play/SoloPlayer.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/SoloPlayer.cs b/osu.Game/Screens/Play/SoloPlayer.cs index d90e8e0168..7d15fcf49f 100644 --- a/osu.Game/Screens/Play/SoloPlayer.cs +++ b/osu.Game/Screens/Play/SoloPlayer.cs @@ -38,13 +38,15 @@ namespace osu.Game.Screens.Play protected override APIRequest CreateSubmissionRequest(Score score, long token) { - var beatmap = score.ScoreInfo.Beatmap; + var scoreCopy = score.DeepClone(); + + var beatmap = scoreCopy.ScoreInfo.Beatmap; Debug.Assert(beatmap.OnlineBeatmapID != null); int beatmapId = beatmap.OnlineBeatmapID.Value; - return new SubmitSoloScoreRequest(beatmapId, token, score.ScoreInfo); + return new SubmitSoloScoreRequest(beatmapId, token, scoreCopy.ScoreInfo); } } } From 87c39909c6c3ab7f872b33a3bb20714144022ac9 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Mon, 19 Jul 2021 14:37:19 +0900 Subject: [PATCH 076/106] Simplify `DependencyProvidingContainer` Using an array of tuple for dependencies instead of using children. --- .../TestSceneCatcher.cs | 7 +++--- .../Visual/DependencyProvidingContainer.cs | 24 ++++--------------- 2 files changed, 8 insertions(+), 23 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs index 0cf5e2b7a0..0a2dff6a21 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs @@ -47,15 +47,16 @@ namespace osu.Game.Rulesets.Catch.Tests { Anchor = Anchor.Centre, }; + droppedObjectContainer = new DroppedObjectContainer(); Child = new DependencyProvidingContainer { - Types = new[] + CachedDependencies = new (Type, object)[] { - typeof(DroppedObjectContainer), + (typeof(DroppedObjectContainer), droppedObjectContainer), }, Children = new Drawable[] { - droppedObjectContainer = new DroppedObjectContainer(), + droppedObjectContainer, catcher = new TestCatcher(trailContainer, difficulty), trailContainer }, diff --git a/osu.Game/Tests/Visual/DependencyProvidingContainer.cs b/osu.Game/Tests/Visual/DependencyProvidingContainer.cs index cd3ae1123b..c799cad61a 100644 --- a/osu.Game/Tests/Visual/DependencyProvidingContainer.cs +++ b/osu.Game/Tests/Visual/DependencyProvidingContainer.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics.Containers; @@ -10,39 +9,24 @@ namespace osu.Game.Tests.Visual { /// /// A which providing ad-hoc dependencies to the child drawables. - /// - /// To provide a dependency, specify the dependency type to , then specify the dependency value to either or . - /// For each type specified in , the first value compatible with the type is selected and provided to the children. - /// /// /// - /// The and values of the dependencies must be set while this is not loaded. + /// The must be set while this is not loaded. /// public class DependencyProvidingContainer : Container { /// - /// The types of the dependencies provided to the children. + /// The dependencies provided to the children. /// // TODO: should be an init-only property when C# 9 - public Type[] Types { get; set; } = Array.Empty(); - - /// - /// The dependency values provided to the children. - /// - public object[] Values { get; set; } = Array.Empty(); + public (Type, object)[] CachedDependencies { get; set; } = Array.Empty<(Type, object)>(); protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) { var dependencyContainer = new DependencyContainer(base.CreateChildDependencies(parent)); - foreach (var type in Types) - { - object value = Values.FirstOrDefault(v => type.IsInstanceOfType(v)) ?? - Children.FirstOrDefault(d => type.IsInstanceOfType(d)) ?? - throw new InvalidOperationException($"The type {type} is specified in this {nameof(DependencyProvidingContainer)}, but no corresponding value is provided."); - + foreach (var (type, value) in CachedDependencies) dependencyContainer.CacheAs(type, value); - } return dependencyContainer; } From 13cb658d29e4372b5f986f1caad46adc2d28be91 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 19 Jul 2021 18:15:09 +0900 Subject: [PATCH 077/106] Mark identifiers as verbatim strings --- osu.Game/Overlays/Profile/Sections/AboutSection.cs | 2 +- osu.Game/Overlays/Profile/Sections/BeatmapsSection.cs | 2 +- osu.Game/Overlays/Profile/Sections/HistoricalSection.cs | 2 +- osu.Game/Overlays/Profile/Sections/KudosuSection.cs | 2 +- osu.Game/Overlays/Profile/Sections/MedalsSection.cs | 2 +- osu.Game/Overlays/Profile/Sections/RanksSection.cs | 2 +- osu.Game/Overlays/Profile/Sections/RecentSection.cs | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game/Overlays/Profile/Sections/AboutSection.cs b/osu.Game/Overlays/Profile/Sections/AboutSection.cs index c224d2b1be..d0d9362fd2 100644 --- a/osu.Game/Overlays/Profile/Sections/AboutSection.cs +++ b/osu.Game/Overlays/Profile/Sections/AboutSection.cs @@ -10,6 +10,6 @@ namespace osu.Game.Overlays.Profile.Sections { public override LocalisableString Title => UsersStrings.ShowExtraMeTitle; - public override string Identifier => "me"; + public override string Identifier => @"me"; } } diff --git a/osu.Game/Overlays/Profile/Sections/BeatmapsSection.cs b/osu.Game/Overlays/Profile/Sections/BeatmapsSection.cs index b8fbe73c0c..843ab531be 100644 --- a/osu.Game/Overlays/Profile/Sections/BeatmapsSection.cs +++ b/osu.Game/Overlays/Profile/Sections/BeatmapsSection.cs @@ -12,7 +12,7 @@ namespace osu.Game.Overlays.Profile.Sections { public override LocalisableString Title => UsersStrings.ShowExtraBeatmapsTitle; - public override string Identifier => "beatmaps"; + public override string Identifier => @"beatmaps"; public BeatmapsSection() { diff --git a/osu.Game/Overlays/Profile/Sections/HistoricalSection.cs b/osu.Game/Overlays/Profile/Sections/HistoricalSection.cs index cba25c0a8b..203844b6b5 100644 --- a/osu.Game/Overlays/Profile/Sections/HistoricalSection.cs +++ b/osu.Game/Overlays/Profile/Sections/HistoricalSection.cs @@ -14,7 +14,7 @@ namespace osu.Game.Overlays.Profile.Sections { public override LocalisableString Title => UsersStrings.ShowExtraHistoricalTitle; - public override string Identifier => "historical"; + public override string Identifier => @"historical"; public HistoricalSection() { diff --git a/osu.Game/Overlays/Profile/Sections/KudosuSection.cs b/osu.Game/Overlays/Profile/Sections/KudosuSection.cs index ffa7ef4eaf..5b749c78a8 100644 --- a/osu.Game/Overlays/Profile/Sections/KudosuSection.cs +++ b/osu.Game/Overlays/Profile/Sections/KudosuSection.cs @@ -12,7 +12,7 @@ namespace osu.Game.Overlays.Profile.Sections { public override LocalisableString Title => UsersStrings.ShowExtraKudosuTitle; - public override string Identifier => "kudosu"; + public override string Identifier => @"kudosu"; public KudosuSection() { diff --git a/osu.Game/Overlays/Profile/Sections/MedalsSection.cs b/osu.Game/Overlays/Profile/Sections/MedalsSection.cs index 3512333e27..cacdd44b61 100644 --- a/osu.Game/Overlays/Profile/Sections/MedalsSection.cs +++ b/osu.Game/Overlays/Profile/Sections/MedalsSection.cs @@ -10,6 +10,6 @@ namespace osu.Game.Overlays.Profile.Sections { public override LocalisableString Title => UsersStrings.ShowExtraMedalsTitle; - public override string Identifier => "medals"; + public override string Identifier => @"medals"; } } diff --git a/osu.Game/Overlays/Profile/Sections/RanksSection.cs b/osu.Game/Overlays/Profile/Sections/RanksSection.cs index 5e0648dbb1..00a68d5bf9 100644 --- a/osu.Game/Overlays/Profile/Sections/RanksSection.cs +++ b/osu.Game/Overlays/Profile/Sections/RanksSection.cs @@ -12,7 +12,7 @@ namespace osu.Game.Overlays.Profile.Sections { public override LocalisableString Title => UsersStrings.ShowExtraTopRanksTitle; - public override string Identifier => "top_ranks"; + public override string Identifier => @"top_ranks"; public RanksSection() { diff --git a/osu.Game/Overlays/Profile/Sections/RecentSection.cs b/osu.Game/Overlays/Profile/Sections/RecentSection.cs index 7a6536c3af..33d435aa1b 100644 --- a/osu.Game/Overlays/Profile/Sections/RecentSection.cs +++ b/osu.Game/Overlays/Profile/Sections/RecentSection.cs @@ -11,7 +11,7 @@ namespace osu.Game.Overlays.Profile.Sections { public override LocalisableString Title => UsersStrings.ShowExtraRecentActivityTitle; - public override string Identifier => "recent_activity"; + public override string Identifier => @"recent_activity"; public RecentSection() { From 443058f87994c2db0d8cbe031fd943c75f7d67e0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 19 Jul 2021 18:41:29 +0900 Subject: [PATCH 078/106] Move playfield constant to top of file and make `internal` --- osu.Game.Rulesets.Catch/Objects/JuiceStreamPath.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStreamPath.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStreamPath.cs index ac11bd9918..f1cdb39e91 100644 --- a/osu.Game.Rulesets.Catch/Objects/JuiceStreamPath.cs +++ b/osu.Game.Rulesets.Catch/Objects/JuiceStreamPath.cs @@ -28,6 +28,12 @@ namespace osu.Game.Rulesets.Catch.Objects /// public class JuiceStreamPath { + /// + /// The height of legacy osu!standard playfield. + /// The sliders converted by are vertically contained in this height. + /// + internal const float OSU_PLAYFIELD_HEIGHT = 384; + /// /// The list of vertices of the path, which is represented as a polyline connecting the vertices. /// @@ -211,12 +217,6 @@ namespace osu.Game.Rulesets.Catch.Objects invalidate(); } - /// - /// The height of legacy osu!standard playfield. - /// The sliders converted by are vertically contained in this height. - /// - public const float OSU_PLAYFIELD_HEIGHT = 384; - /// /// Convert the path of this to a and write the result to . /// The resulting slider is "folded" to make it vertically contained in the playfield `(0..)` assuming the slider start position is . From f16b4957aae84870d90c7e964e972779d01e702e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 19 Jul 2021 19:18:04 +0900 Subject: [PATCH 079/106] Move clone to earlier in the process --- osu.Game/Screens/Play/Player.cs | 8 +++++--- osu.Game/Screens/Play/SoloPlayer.cs | 6 ++---- osu.Game/Screens/Play/SubmittingPlayer.cs | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 51f1dbd121..2328390d76 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -694,9 +694,11 @@ namespace osu.Game.Screens.Play /// The final score. private async Task prepareScoreForResults() { + var scoreCopy = Score.DeepClone(); + try { - await PrepareScoreForResultsAsync(Score).ConfigureAwait(false); + await PrepareScoreForResultsAsync(scoreCopy).ConfigureAwait(false); } catch (Exception ex) { @@ -705,14 +707,14 @@ namespace osu.Game.Screens.Play try { - await ImportScore(Score).ConfigureAwait(false); + await ImportScore(scoreCopy).ConfigureAwait(false); } catch (Exception ex) { Logger.Error(ex, @"Score import failed!"); } - return Score.ScoreInfo; + return scoreCopy.ScoreInfo; } /// diff --git a/osu.Game/Screens/Play/SoloPlayer.cs b/osu.Game/Screens/Play/SoloPlayer.cs index 7d15fcf49f..d90e8e0168 100644 --- a/osu.Game/Screens/Play/SoloPlayer.cs +++ b/osu.Game/Screens/Play/SoloPlayer.cs @@ -38,15 +38,13 @@ namespace osu.Game.Screens.Play protected override APIRequest CreateSubmissionRequest(Score score, long token) { - var scoreCopy = score.DeepClone(); - - var beatmap = scoreCopy.ScoreInfo.Beatmap; + var beatmap = score.ScoreInfo.Beatmap; Debug.Assert(beatmap.OnlineBeatmapID != null); int beatmapId = beatmap.OnlineBeatmapID.Value; - return new SubmitSoloScoreRequest(beatmapId, token, scoreCopy.ScoreInfo); + return new SubmitSoloScoreRequest(beatmapId, token, score.ScoreInfo); } } } diff --git a/osu.Game/Screens/Play/SubmittingPlayer.cs b/osu.Game/Screens/Play/SubmittingPlayer.cs index 76e9f28dae..65622a4702 100644 --- a/osu.Game/Screens/Play/SubmittingPlayer.cs +++ b/osu.Game/Screens/Play/SubmittingPlayer.cs @@ -116,7 +116,7 @@ namespace osu.Game.Screens.Play { var exiting = base.OnExiting(next); - submitScore(Score); + submitScore(Score.DeepClone()); return exiting; } From b3f60c82535fe292cd172b291449cb0d6a61a878 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 19 Jul 2021 19:28:35 +0900 Subject: [PATCH 080/106] Fix date being updated on replays unexpectedly --- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 1 - osu.Game/Screens/Play/Player.cs | 8 +------- osu.Game/Screens/Play/SubmittingPlayer.cs | 2 ++ 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index eeb881cd39..6a2601170c 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -338,7 +338,6 @@ namespace osu.Game.Rulesets.Scoring score.MaxCombo = HighestCombo.Value; score.Accuracy = Accuracy.Value; score.Rank = Rank.Value; - score.Date = DateTimeOffset.Now; foreach (var result in Enum.GetValues(typeof(HitResult)).OfType().Where(r => r.IsScorable())) score.Statistics[result] = GetStatistic(result); diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 2328390d76..dbe8d530b9 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -1019,13 +1019,7 @@ namespace osu.Game.Screens.Play /// /// The to prepare. /// A task that prepares the provided score. On completion, the score is assumed to be ready for display. - protected virtual Task PrepareScoreForResultsAsync(Score score) - { - // perform one final population to ensure everything is up-to-date. - ScoreProcessor.PopulateScore(score.ScoreInfo); - - return Task.CompletedTask; - } + protected virtual Task PrepareScoreForResultsAsync(Score score) => Task.CompletedTask; /// /// Creates the for a . diff --git a/osu.Game/Screens/Play/SubmittingPlayer.cs b/osu.Game/Screens/Play/SubmittingPlayer.cs index 65622a4702..5faa384d03 100644 --- a/osu.Game/Screens/Play/SubmittingPlayer.cs +++ b/osu.Game/Screens/Play/SubmittingPlayer.cs @@ -109,6 +109,8 @@ namespace osu.Game.Screens.Play { await base.PrepareScoreForResultsAsync(score).ConfigureAwait(false); + score.ScoreInfo.Date = DateTimeOffset.Now; + await submitScore(score).ConfigureAwait(false); } From 063f14da98b0491a874b9014c230f5e96e830b07 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 19 Jul 2021 19:55:08 +0900 Subject: [PATCH 081/106] Update test room manager to not return passwords --- osu.Game/Online/Rooms/JoinRoomRequest.cs | 12 ++--- .../Multiplayer/TestMultiplayerRoomManager.cs | 47 ++++++++++++------- 2 files changed, 37 insertions(+), 22 deletions(-) diff --git a/osu.Game/Online/Rooms/JoinRoomRequest.cs b/osu.Game/Online/Rooms/JoinRoomRequest.cs index 53bd2a6106..0111882d9a 100644 --- a/osu.Game/Online/Rooms/JoinRoomRequest.cs +++ b/osu.Game/Online/Rooms/JoinRoomRequest.cs @@ -9,23 +9,23 @@ namespace osu.Game.Online.Rooms { public class JoinRoomRequest : APIRequest { - private readonly Room room; - private readonly string password; + public readonly Room Room; + public readonly string Password; public JoinRoomRequest(Room room, string password) { - this.room = room; - this.password = password; + Room = room; + Password = password; } protected override WebRequest CreateWebRequest() { var req = base.CreateWebRequest(); req.Method = HttpMethod.Put; - req.AddParameter("password", password); + req.AddParameter("password", Password); return req; } - protected override string Target => $"rooms/{room.RoomID.Value}/users/{User.Id}"; + protected override string Target => $"rooms/{Room.RoomID.Value}/users/{User.Id}"; } } diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs index 5d66cdba02..77238f47fb 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs @@ -45,21 +45,33 @@ namespace osu.Game.Tests.Visual.Multiplayer switch (req) { case CreateRoomRequest createRoomRequest: - var createdRoom = new APICreatedRoom(); + var apiRoom = new Room(); - createdRoom.CopyFrom(createRoomRequest.Room); - createdRoom.RoomID.Value ??= currentRoomId++; + apiRoom.CopyFrom(createRoomRequest.Room); + apiRoom.RoomID.Value ??= currentRoomId++; + for (int i = 0; i < apiRoom.Playlist.Count; i++) + apiRoom.Playlist[i].ID = currentPlaylistItemId++; - for (int i = 0; i < createdRoom.Playlist.Count; i++) - createdRoom.Playlist[i].ID = currentPlaylistItemId++; + var responseRoom = new APICreatedRoom(); + responseRoom.CopyFrom(createResponseRoom(apiRoom, false)); - Rooms.Add(createdRoom); - createRoomRequest.TriggerSuccess(createdRoom); + Rooms.Add(apiRoom); + createRoomRequest.TriggerSuccess(responseRoom); return true; case JoinRoomRequest joinRoomRequest: + { + var room = Rooms.Single(r => r.RoomID.Value == joinRoomRequest.Room.RoomID.Value); + + if (joinRoomRequest.Password != room.Password.Value) + { + joinRoomRequest.TriggerFailure(new InvalidOperationException("Invalid password.")); + return true; + } + joinRoomRequest.TriggerSuccess(); return true; + } case PartRoomRequest partRoomRequest: partRoomRequest.TriggerSuccess(); @@ -69,20 +81,13 @@ namespace osu.Game.Tests.Visual.Multiplayer var roomsWithoutParticipants = new List(); foreach (var r in Rooms) - { - var newRoom = new Room(); - - newRoom.CopyFrom(r); - newRoom.RecentParticipants.Clear(); - - roomsWithoutParticipants.Add(newRoom); - } + roomsWithoutParticipants.Add(createResponseRoom(r, false)); getRoomsRequest.TriggerSuccess(roomsWithoutParticipants); return true; case GetRoomRequest getRoomRequest: - getRoomRequest.TriggerSuccess(Rooms.Single(r => r.RoomID.Value == getRoomRequest.RoomId)); + getRoomRequest.TriggerSuccess(createResponseRoom(Rooms.Single(r => r.RoomID.Value == getRoomRequest.RoomId), true)); return true; case GetBeatmapSetRequest getBeatmapSetRequest: @@ -118,6 +123,16 @@ namespace osu.Game.Tests.Visual.Multiplayer }; } + private Room createResponseRoom(Room room, bool withParticipants) + { + var responseRoom = new Room(); + responseRoom.CopyFrom(room); + responseRoom.Password.Value = null; + if (!withParticipants) + responseRoom.RecentParticipants.Clear(); + return responseRoom; + } + public new void ClearRooms() => base.ClearRooms(); public new void Schedule(Action action) => base.Schedule(action); From 8c0daa89a0ec0d027c0ba20d6d131a94073d5259 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 19 Jul 2021 20:01:44 +0900 Subject: [PATCH 082/106] Make test multiplayer client validate password --- osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index adc632a2b1..82cb68b452 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -119,6 +119,9 @@ namespace osu.Game.Tests.Visual.Multiplayer { var apiRoom = roomManager.Rooms.Single(r => r.RoomID.Value == roomId); + if (password != apiRoom.Password.Value) + throw new InvalidOperationException("Invalid password."); + var localUser = new MultiplayerRoomUser(api.LocalUser.Value.Id) { User = api.LocalUser.Value From 2515785f933682593721279519855552507085a2 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 19 Jul 2021 20:02:14 +0900 Subject: [PATCH 083/106] Use room password to fill settings textbox --- osu.Game/Online/Rooms/Room.cs | 2 +- .../Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs | 1 + osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs | 3 +++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/Rooms/Room.cs b/osu.Game/Online/Rooms/Room.cs index f79a410bd9..48e9f94dd5 100644 --- a/osu.Game/Online/Rooms/Room.cs +++ b/osu.Game/Online/Rooms/Room.cs @@ -85,6 +85,7 @@ namespace osu.Game.Online.Rooms #region Properties only used for room creation request + [Cached(Name = nameof(Password))] [JsonProperty("password")] public readonly Bindable Password = new Bindable(); @@ -159,7 +160,6 @@ namespace osu.Game.Online.Rooms ChannelId.Value = other.ChannelId.Value; Status.Value = other.Status.Value; Availability.Value = other.Availability.Value; - Password.Value = other.Password.Value; HasPassword.Value = other.HasPassword.Value; Type.Value = other.Type.Value; MaxParticipants.Value = other.MaxParticipants.Value; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs index fceb124e0a..338d2c9e84 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs @@ -271,6 +271,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match Type.BindValueChanged(type => TypePicker.Current.Value = type.NewValue, true); MaxParticipants.BindValueChanged(count => MaxParticipantsField.Text = count.NewValue?.ToString(), true); RoomID.BindValueChanged(roomId => initialBeatmapControl.Alpha = roomId.NewValue == null ? 1 : 0, true); + Password.BindValueChanged(password => PasswordTextBox.Text = password.NewValue ?? string.Empty, true); operationInProgress.BindTo(ongoingOperationTracker.InProgress); operationInProgress.BindValueChanged(v => diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs index eb0b23f13f..0b28bc1a7e 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs @@ -56,6 +56,9 @@ namespace osu.Game.Screens.OnlinePlay [Resolved(typeof(Room))] protected Bindable Availability { get; private set; } + [Resolved(typeof(Room), nameof(Room.Password))] + public Bindable Password { get; private set; } + [Resolved(typeof(Room))] protected Bindable Duration { get; private set; } From 26d0eea4854d836e7af67617b7c2688f4d3d7b2a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 19 Jul 2021 20:03:00 +0900 Subject: [PATCH 084/106] Set HasPassword correctly in the response room --- osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs index 77238f47fb..c94c0b8683 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs @@ -127,6 +127,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { var responseRoom = new Room(); responseRoom.CopyFrom(room); + responseRoom.HasPassword.Value = !string.IsNullOrEmpty(responseRoom.Password.Value); responseRoom.Password.Value = null; if (!withParticipants) responseRoom.RecentParticipants.Clear(); From a5a0f12e199f4fc2b7fe171f0879b4974e8744f3 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 19 Jul 2021 20:07:56 +0900 Subject: [PATCH 085/106] Also copy password in test room manager --- .../Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs index c94c0b8683..59679f3d66 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs @@ -49,6 +49,11 @@ namespace osu.Game.Tests.Visual.Multiplayer apiRoom.CopyFrom(createRoomRequest.Room); apiRoom.RoomID.Value ??= currentRoomId++; + + // Passwords are explicitly not copied between rooms. + apiRoom.HasPassword.Value = !string.IsNullOrEmpty(createRoomRequest.Room.Password.Value); + apiRoom.Password.Value = createRoomRequest.Room.Password.Value; + for (int i = 0; i < apiRoom.Playlist.Count; i++) apiRoom.Playlist[i].ID = currentPlaylistItemId++; @@ -127,7 +132,6 @@ namespace osu.Game.Tests.Visual.Multiplayer { var responseRoom = new Room(); responseRoom.CopyFrom(room); - responseRoom.HasPassword.Value = !string.IsNullOrEmpty(responseRoom.Password.Value); responseRoom.Password.Value = null; if (!withParticipants) responseRoom.RecentParticipants.Clear(); From 1b9d297911e071b569e64f31428a377384c41353 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 19 Jul 2021 20:08:29 +0900 Subject: [PATCH 086/106] Add test --- .../Multiplayer/TestSceneMultiplayer.cs | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index 7673efb78f..f45ecca424 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -85,6 +85,26 @@ namespace osu.Game.Tests.Visual.Multiplayer // used to test the flow of multiplayer from visual tests. } + [Test] + public void TestCreateRoomWithPassword() + { + createRoom(() => new Room + { + Name = { Value = "Test Room" }, + Password = { Value = "password" }, + Playlist = + { + new PlaylistItem + { + Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo }, + Ruleset = { Value = new OsuRuleset().RulesetInfo }, + } + } + }); + + AddAssert("room has password", () => client.APIRoom?.Password.Value == "password"); + } + [Test] public void TestUserSetToIdleWhenBeatmapDeleted() { From d6aa15e5d7c92df054e29a12b6ff2d00068bec8b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 19 Jul 2021 20:19:23 +0900 Subject: [PATCH 087/106] Remove local APIRoom from test multiplayer client --- .../Tests/Visual/Multiplayer/TestMultiplayerClient.cs | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 82cb68b452..1528ed0bc8 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -28,7 +28,7 @@ namespace osu.Game.Tests.Visual.Multiplayer public override IBindable IsConnected => isConnected; private readonly Bindable isConnected = new Bindable(true); - public Room? APIRoom { get; private set; } + public new Room? APIRoom => base.APIRoom; public Action? RoomSetupAction; @@ -138,6 +138,7 @@ namespace osu.Game.Tests.Visual.Multiplayer RequiredMods = apiRoom.Playlist.Last().RequiredMods.Select(m => new APIMod(m)).ToArray(), AllowedMods = apiRoom.Playlist.Last().AllowedMods.Select(m => new APIMod(m)).ToArray(), PlaylistItemId = apiRoom.Playlist.Last().ID, + // ReSharper disable once ConstantNullCoalescingCondition Incorrect inspection due to lack of nullable in Room.cs. Password = password ?? string.Empty, }, Users = { localUser }, @@ -147,16 +148,10 @@ namespace osu.Game.Tests.Visual.Multiplayer RoomSetupAction?.Invoke(room); RoomSetupAction = null; - APIRoom = apiRoom; - return Task.FromResult(room); } - protected override Task LeaveRoomInternal() - { - APIRoom = null; - return Task.CompletedTask; - } + protected override Task LeaveRoomInternal() => Task.CompletedTask; public override Task TransferHost(int userId) => ((IMultiplayerClient)this).HostChanged(userId); From 2eec524f2744e68705f9ede888ca57796b2bae4d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 19 Jul 2021 20:20:08 +0900 Subject: [PATCH 088/106] Fix password not copied from multiplayer client --- .../Multiplayer/TestSceneMultiplayer.cs | 21 ++++++++++++++ .../Online/Multiplayer/MultiplayerClient.cs | 29 ++++++++++--------- 2 files changed, 36 insertions(+), 14 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index f45ecca424..4ea628cb55 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -105,6 +105,27 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("room has password", () => client.APIRoom?.Password.Value == "password"); } + [Test] + public void TestLocalPasswordUpdatedWhenMultiplayerSettingsChange() + { + createRoom(() => new Room + { + Name = { Value = "Test Room" }, + Password = { Value = "password" }, + Playlist = + { + new PlaylistItem + { + Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo }, + Ruleset = { Value = new OsuRuleset().RulesetInfo }, + } + } + }); + + AddStep("change password", () => client.ChangeSettings(password: "password2")); + AddUntilStep("local password changed", () => client.APIRoom?.Password.Value == "password2"); + } + [Test] public void TestUserSetToIdleWhenBeatmapDeleted() { diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 42d436ef11..9972d7e88d 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -92,7 +92,7 @@ namespace osu.Game.Online.Multiplayer [Resolved] private UserLookupCache userLookupCache { get; set; } = null!; - private Room? apiRoom; + protected Room? APIRoom { get; private set; } [BackgroundDependencyLoader] private void load() @@ -139,7 +139,7 @@ namespace osu.Game.Online.Multiplayer await scheduleAsync(() => { Room = joinedRoom; - apiRoom = room; + APIRoom = room; foreach (var user in joinedRoom.Users) updateUserPlayingState(user.UserID, user.State); }, cancellationSource.Token).ConfigureAwait(false); @@ -168,7 +168,7 @@ namespace osu.Game.Online.Multiplayer // For example, if a room was left and the user immediately pressed the "create room" button, then the user could be taken into the lobby if the value of Room is not reset in time. var scheduledReset = scheduleAsync(() => { - apiRoom = null; + APIRoom = null; Room = null; CurrentMatchPlayingUserIds.Clear(); @@ -305,22 +305,22 @@ namespace osu.Game.Online.Multiplayer if (Room == null) return; - Debug.Assert(apiRoom != null); + Debug.Assert(APIRoom != null); Room.State = state; switch (state) { case MultiplayerRoomState.Open: - apiRoom.Status.Value = new RoomStatusOpen(); + APIRoom.Status.Value = new RoomStatusOpen(); break; case MultiplayerRoomState.Playing: - apiRoom.Status.Value = new RoomStatusPlaying(); + APIRoom.Status.Value = new RoomStatusPlaying(); break; case MultiplayerRoomState.Closed: - apiRoom.Status.Value = new RoomStatusEnded(); + APIRoom.Status.Value = new RoomStatusEnded(); break; } @@ -381,12 +381,12 @@ namespace osu.Game.Online.Multiplayer if (Room == null) return; - Debug.Assert(apiRoom != null); + Debug.Assert(APIRoom != null); var user = Room.Users.FirstOrDefault(u => u.UserID == userId); Room.Host = user; - apiRoom.Host.Value = user?.User; + APIRoom.Host.Value = user?.User; RoomUpdated?.Invoke(); }, false); @@ -529,11 +529,12 @@ namespace osu.Game.Online.Multiplayer if (Room == null) return; - Debug.Assert(apiRoom != null); + Debug.Assert(APIRoom != null); // Update a few properties of the room instantaneously. Room.Settings = settings; - apiRoom.Name.Value = Room.Settings.Name; + APIRoom.Name.Value = Room.Settings.Name; + APIRoom.Password.Value = Room.Settings.Password; // The current item update is delayed until an online beatmap lookup (below) succeeds. // In-order for the client to not display an outdated beatmap, the current item is forcefully cleared here. @@ -555,7 +556,7 @@ namespace osu.Game.Online.Multiplayer if (Room == null || !Room.Settings.Equals(settings)) return; - Debug.Assert(apiRoom != null); + Debug.Assert(APIRoom != null); var beatmap = beatmapSet.Beatmaps.Single(b => b.OnlineBeatmapID == settings.BeatmapID); beatmap.MD5Hash = settings.BeatmapChecksum; @@ -565,7 +566,7 @@ namespace osu.Game.Online.Multiplayer var allowedMods = settings.AllowedMods.Select(m => m.ToMod(ruleset)); // Try to retrieve the existing playlist item from the API room. - var playlistItem = apiRoom.Playlist.FirstOrDefault(i => i.ID == settings.PlaylistItemId); + var playlistItem = APIRoom.Playlist.FirstOrDefault(i => i.ID == settings.PlaylistItemId); if (playlistItem != null) updateItem(playlistItem); @@ -573,7 +574,7 @@ namespace osu.Game.Online.Multiplayer { // An existing playlist item does not exist, so append a new one. updateItem(playlistItem = new PlaylistItem()); - apiRoom.Playlist.Add(playlistItem); + APIRoom.Playlist.Add(playlistItem); } CurrentMatchPlayingItem.Value = playlistItem; From 80c2b1449bb2ae088104a37712b17829a32893d4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 19 Jul 2021 20:27:00 +0900 Subject: [PATCH 089/106] Fix API request potentially firing failed events after completion Specifically, `Cancel()` calls were not thread safe. Due to a series of events, `ListPollingComponent` could call `Cancel` from a non-update thread, leading to a race condition where both a `Success` and `Fail` event can be fired. This is intended to be the simplest fix possible, locking and guarding specifically on the callbacks. Further work could be done in the future to improve the flow surrounding `pendingFailure`, potentially reducing redundant work and cleaning up the code, but that's not happening here. Closes https://github.com/ppy/osu/issues/13632. --- osu.Game/Online/API/APIException.cs | 15 +++++ osu.Game/Online/API/APIRequest.cs | 58 ++++++++++++------- .../Online/API/APIRequestCompletionState.cs | 23 ++++++++ osu.Game/Online/PollingComponent.cs | 2 +- 4 files changed, 75 insertions(+), 23 deletions(-) create mode 100644 osu.Game/Online/API/APIException.cs create mode 100644 osu.Game/Online/API/APIRequestCompletionState.cs diff --git a/osu.Game/Online/API/APIException.cs b/osu.Game/Online/API/APIException.cs new file mode 100644 index 0000000000..97786bced9 --- /dev/null +++ b/osu.Game/Online/API/APIException.cs @@ -0,0 +1,15 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; + +namespace osu.Game.Online.API +{ + public class APIException : InvalidOperationException + { + public APIException(string messsage, Exception innerException) + : base(messsage, innerException) + { + } + } +} diff --git a/osu.Game/Online/API/APIRequest.cs b/osu.Game/Online/API/APIRequest.cs index 1a6868cfa4..8d816d3975 100644 --- a/osu.Game/Online/API/APIRequest.cs +++ b/osu.Game/Online/API/APIRequest.cs @@ -79,7 +79,13 @@ namespace osu.Game.Online.API /// public event APIFailureHandler Failure; - private bool cancelled; + private readonly object completionStateLock = new object(); + + /// + /// The state of this request, from an outside perspective. + /// This is used to ensure correct notification events are fired. + /// + private APIRequestCompletionState completionState; private Action pendingFailure; @@ -116,12 +122,7 @@ namespace osu.Game.Online.API PostProcess(); - API.Schedule(delegate - { - if (cancelled) return; - - TriggerSuccess(); - }); + API.Schedule(TriggerSuccess); } /// @@ -131,16 +132,29 @@ namespace osu.Game.Online.API { } - private bool succeeded; - internal virtual void TriggerSuccess() { - succeeded = true; + lock (completionStateLock) + { + if (completionState != APIRequestCompletionState.Waiting) + return; + + completionState = APIRequestCompletionState.Completed; + } + Success?.Invoke(); } internal void TriggerFailure(Exception e) { + lock (completionStateLock) + { + if (completionState != APIRequestCompletionState.Waiting) + return; + + completionState = APIRequestCompletionState.Failed; + } + Failure?.Invoke(e); } @@ -148,10 +162,14 @@ namespace osu.Game.Online.API public void Fail(Exception e) { - if (succeeded || cancelled) - return; + lock (completionStateLock) + { + // while it doesn't matter if code following this check is run more than once, + // this avoids unnecessarily performing work where we are already sure the user has been informed. + if (completionState != APIRequestCompletionState.Waiting) + return; + } - cancelled = true; WebRequest?.Abort(); string responseString = WebRequest?.GetResponseString(); @@ -181,7 +199,11 @@ namespace osu.Game.Online.API /// Whether we are in a failed or cancelled state. private bool checkAndScheduleFailure() { - if (pendingFailure == null) return cancelled; + lock (completionStateLock) + { + if (pendingFailure == null) + return completionState == APIRequestCompletionState.Failed; + } if (API == null) pendingFailure(); @@ -199,14 +221,6 @@ namespace osu.Game.Online.API } } - public class APIException : InvalidOperationException - { - public APIException(string messsage, Exception innerException) - : base(messsage, innerException) - { - } - } - public delegate void APIFailureHandler(Exception e); public delegate void APISuccessHandler(); diff --git a/osu.Game/Online/API/APIRequestCompletionState.cs b/osu.Game/Online/API/APIRequestCompletionState.cs new file mode 100644 index 0000000000..84c9974dd8 --- /dev/null +++ b/osu.Game/Online/API/APIRequestCompletionState.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. + +namespace osu.Game.Online.API +{ + public enum APIRequestCompletionState + { + /// + /// Not yet run or currently waiting on response. + /// + Waiting, + + /// + /// Ran to completion. + /// + Completed, + + /// + /// Cancelled or failed due to error. + /// + Failed + } +} diff --git a/osu.Game/Online/PollingComponent.cs b/osu.Game/Online/PollingComponent.cs index 3d19f2ab09..806c0047e7 100644 --- a/osu.Game/Online/PollingComponent.cs +++ b/osu.Game/Online/PollingComponent.cs @@ -70,7 +70,7 @@ namespace osu.Game.Online return true; } - // not ennough time has passed since the last poll. we do want to schedule a poll to happen, though. + // not enough time has passed since the last poll. we do want to schedule a poll to happen, though. scheduleNextPoll(); return false; } From 3168a927dc691e6025355cc872fba67c577a3a2c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 19 Jul 2021 20:50:30 +0900 Subject: [PATCH 090/106] Fix possible exception --- .../Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 561fa220c8..f35671a761 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -326,7 +326,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { // the room may not be left immediately after a disconnection due to async flow, // so checking the IsConnected status is also required. - if (client.Room == null || !client.IsConnected.Value) + if (client?.Room == null || !client.IsConnected.Value) { // room has not been created yet; exit immediately. return base.OnExiting(next); From 05295241b878dc5ac51d4851765af77295fda6f8 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 19 Jul 2021 20:55:14 +0900 Subject: [PATCH 091/106] Add room joining tests --- .../Multiplayer/TestSceneMultiplayer.cs | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index 4ea628cb55..92f9c5733f 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -7,6 +7,7 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.UserInterface; using osu.Framework.Platform; using osu.Framework.Screens; using osu.Framework.Testing; @@ -22,6 +23,7 @@ using osu.Game.Rulesets.Osu.Mods; using osu.Game.Screens; using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Lounge; +using osu.Game.Screens.OnlinePlay.Lounge.Components; using osu.Game.Screens.OnlinePlay.Match.Components; using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Screens.OnlinePlay.Multiplayer.Match; @@ -85,6 +87,50 @@ namespace osu.Game.Tests.Visual.Multiplayer // used to test the flow of multiplayer from visual tests. } + [Test] + public void TestCreateRoomWithoutPassword() + { + createRoom(() => new Room + { + Name = { Value = "Test Room" }, + Playlist = + { + new PlaylistItem + { + Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo }, + Ruleset = { Value = new OsuRuleset().RulesetInfo }, + } + } + }); + } + + [Test] + public void TestJoinRoomWithoutPassword() + { + AddStep("create room", () => + { + API.Queue(new CreateRoomRequest(new Room + { + Name = { Value = "Test Room" }, + Playlist = + { + new PlaylistItem + { + Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo }, + Ruleset = { Value = new OsuRuleset().RulesetInfo }, + } + } + })); + }); + + AddStep("refresh rooms", () => multiplayerScreen.RoomManager.Filter.Value = new FilterCriteria()); + AddStep("select room", () => InputManager.Key(Key.Down)); + AddStep("join room", () => InputManager.Key(Key.Enter)); + + AddUntilStep("wait for room open", () => this.ChildrenOfType().FirstOrDefault()?.IsLoaded == true); + AddUntilStep("wait for join", () => client.Room != null); + } + [Test] public void TestCreateRoomWithPassword() { @@ -105,6 +151,39 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("room has password", () => client.APIRoom?.Password.Value == "password"); } + [Test] + public void TestJoinRoomWithPassword() + { + AddStep("create room", () => + { + API.Queue(new CreateRoomRequest(new Room + { + Name = { Value = "Test Room" }, + Password = { Value = "password" }, + Playlist = + { + new PlaylistItem + { + Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo }, + Ruleset = { Value = new OsuRuleset().RulesetInfo }, + } + } + })); + }); + + AddStep("refresh rooms", () => multiplayerScreen.RoomManager.Filter.Value = new FilterCriteria()); + AddStep("select room", () => InputManager.Key(Key.Down)); + AddStep("join room", () => InputManager.Key(Key.Enter)); + + DrawableRoom.PasswordEntryPopover passwordEntryPopover = null; + AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType().FirstOrDefault()) != null); + AddStep("enter password in text box", () => passwordEntryPopover.ChildrenOfType().First().Text = "password"); + AddStep("press join room button", () => passwordEntryPopover.ChildrenOfType().First().Click()); + + AddUntilStep("wait for room open", () => this.ChildrenOfType().FirstOrDefault()?.IsLoaded == true); + AddUntilStep("wait for join", () => client.Room != null); + } + [Test] public void TestLocalPasswordUpdatedWhenMultiplayerSettingsChange() { From a001e4aa166675a81c8beacc065284aebc158871 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 19 Jul 2021 20:55:25 +0900 Subject: [PATCH 092/106] Fix web request failing if password is null --- osu.Game/Online/Rooms/JoinRoomRequest.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/Rooms/JoinRoomRequest.cs b/osu.Game/Online/Rooms/JoinRoomRequest.cs index 0111882d9a..eab508ab7d 100644 --- a/osu.Game/Online/Rooms/JoinRoomRequest.cs +++ b/osu.Game/Online/Rooms/JoinRoomRequest.cs @@ -22,7 +22,10 @@ namespace osu.Game.Online.Rooms { var req = base.CreateWebRequest(); req.Method = HttpMethod.Put; - req.AddParameter("password", Password); + + if (!string.IsNullOrEmpty(Password)) + req.AddParameter("password", Password); + return req; } From 0a43e54dfc5d605d45f55cc1327cb95056fdc262 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 19 Jul 2021 21:24:22 +0900 Subject: [PATCH 093/106] Fix request failing due to parameters --- osu.Game/Online/Rooms/JoinRoomRequest.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/osu.Game/Online/Rooms/JoinRoomRequest.cs b/osu.Game/Online/Rooms/JoinRoomRequest.cs index eab508ab7d..b2d772cac7 100644 --- a/osu.Game/Online/Rooms/JoinRoomRequest.cs +++ b/osu.Game/Online/Rooms/JoinRoomRequest.cs @@ -22,13 +22,10 @@ namespace osu.Game.Online.Rooms { var req = base.CreateWebRequest(); req.Method = HttpMethod.Put; - - if (!string.IsNullOrEmpty(Password)) - req.AddParameter("password", Password); - return req; } - protected override string Target => $"rooms/{Room.RoomID.Value}/users/{User.Id}"; + // Todo: Password needs to be specified here rather than via AddParameter() because this is a PUT request. May be a framework bug. + protected override string Target => $"rooms/{Room.RoomID.Value}/users/{User.Id}?password={Password}"; } } From 892d858d5f4852a65cb8e2af022745a0b12a618d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 19 Jul 2021 22:23:31 +0900 Subject: [PATCH 094/106] Fix compile error --- osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs index 951b00376e..54c01e2c20 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs @@ -249,7 +249,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components { new OsuMenuItem("Create copy", MenuItemType.Standard, () => { - parentScreen?.OpenNewRoom(Room.CreateCopy()); + parentScreen?.OpenNewRoom(Room.DeepClone()); }) }; From 57a99886d5f3fd2da33b9ce8c620dde142338856 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 19 Jul 2021 22:31:01 +0900 Subject: [PATCH 095/106] Fix password icon not disappearing when no password --- .../Visual/Multiplayer/TestSceneRoomStatus.cs | 26 +++++++++++++++++++ .../Lounge/Components/DrawableRoom.cs | 19 +++++++------- 2 files changed, 36 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomStatus.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomStatus.cs index cec40635f3..6f1b34b078 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomStatus.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomStatus.cs @@ -2,8 +2,12 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Linq; +using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; +using osu.Framework.Utils; using osu.Game.Online.Rooms; using osu.Game.Online.Rooms.RoomStatuses; using osu.Game.Screens.OnlinePlay.Lounge.Components; @@ -47,5 +51,27 @@ namespace osu.Game.Tests.Visual.Multiplayer } }; } + + [Test] + public void TestEnableAndDisablePassword() + { + DrawableRoom drawableRoom = null; + Room room = null; + + AddStep("create room", () => Child = drawableRoom = new DrawableRoom(room = new Room + { + Name = { Value = "Room with password" }, + Status = { Value = new RoomStatusOpen() }, + Category = { Value = RoomCategory.Realtime }, + }) { MatchingFilter = true }); + + AddAssert("password icon hidden", () => Precision.AlmostEquals(0, drawableRoom.ChildrenOfType().Single().Alpha)); + + AddStep("set password", () => room.Password.Value = "password"); + AddAssert("password icon visible", () => Precision.AlmostEquals(1, drawableRoom.ChildrenOfType().Single().Alpha)); + + AddStep("unset password", () => room.Password.Value = string.Empty); + AddAssert("password icon hidden", () => Precision.AlmostEquals(0, drawableRoom.ChildrenOfType().Single().Alpha)); + } } } diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs index 54c01e2c20..236408851f 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs @@ -58,8 +58,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components [Resolved(canBeNull: true)] private LoungeSubScreen lounge { get; set; } - private Container content; - public readonly Room Room; private SelectionState state; @@ -105,6 +103,10 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components public bool FilteringActive { get; set; } + private PasswordProtectedIcon passwordIcon; + + private readonly Bindable hasPassword = new Bindable(); + public DrawableRoom(Room room) { Room = room; @@ -138,7 +140,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components { RelativeSizeAxes = Axes.Both, Padding = new MarginPadding(SELECTION_BORDER_WIDTH), - Child = content = new Container + Child = new Container { RelativeSizeAxes = Axes.Both, Masking = true, @@ -214,15 +216,11 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components }, }, }, + passwordIcon = new PasswordProtectedIcon { Alpha = 0 } }, }, }, }; - - if (Room.HasPassword.Value) - { - content.Add(new PasswordProtectedIcon()); - } } protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) @@ -241,6 +239,9 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components this.FadeInFromZero(transition_duration); else Alpha = 0; + + hasPassword.BindTo(Room.HasPassword); + hasPassword.BindValueChanged(v => passwordIcon.Alpha = v.NewValue ? 1 : 0, true); } public Popover GetPopover() => new PasswordEntryPopover(Room) { JoinRequested = lounge.Join }; @@ -313,7 +314,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components } } - private class PasswordProtectedIcon : CompositeDrawable + public class PasswordProtectedIcon : CompositeDrawable { [BackgroundDependencyLoader] private void load(OsuColour colours) From 7956f73f629641a178f5b0015cdc43de3d2412e1 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 19 Jul 2021 22:31:53 +0900 Subject: [PATCH 096/106] Move initial content into step --- .../Visual/Multiplayer/TestSceneRoomStatus.cs | 64 ++++++++++--------- 1 file changed, 34 insertions(+), 30 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomStatus.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomStatus.cs index 6f1b34b078..8c4133418c 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomStatus.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomStatus.cs @@ -16,40 +16,44 @@ namespace osu.Game.Tests.Visual.Multiplayer { public class TestSceneRoomStatus : OsuTestScene { - public TestSceneRoomStatus() + [Test] + public void TestMultipleStatuses() { - Child = new FillFlowContainer + AddStep("create rooms", () => { - RelativeSizeAxes = Axes.Both, - Width = 0.5f, - Children = new Drawable[] + Child = new FillFlowContainer { - new DrawableRoom(new Room + RelativeSizeAxes = Axes.Both, + Width = 0.5f, + Children = new Drawable[] { - Name = { Value = "Open - ending in 1 day" }, - Status = { Value = new RoomStatusOpen() }, - EndDate = { Value = DateTimeOffset.Now.AddDays(1) } - }) { MatchingFilter = true }, - new DrawableRoom(new Room - { - Name = { Value = "Playing - ending in 1 day" }, - Status = { Value = new RoomStatusPlaying() }, - EndDate = { Value = DateTimeOffset.Now.AddDays(1) } - }) { MatchingFilter = true }, - new DrawableRoom(new Room - { - Name = { Value = "Ended" }, - Status = { Value = new RoomStatusEnded() }, - EndDate = { Value = DateTimeOffset.Now } - }) { MatchingFilter = true }, - new DrawableRoom(new Room - { - Name = { Value = "Open" }, - Status = { Value = new RoomStatusOpen() }, - Category = { Value = RoomCategory.Realtime } - }) { MatchingFilter = true }, - } - }; + new DrawableRoom(new Room + { + Name = { Value = "Open - ending in 1 day" }, + Status = { Value = new RoomStatusOpen() }, + EndDate = { Value = DateTimeOffset.Now.AddDays(1) } + }) { MatchingFilter = true }, + new DrawableRoom(new Room + { + Name = { Value = "Playing - ending in 1 day" }, + Status = { Value = new RoomStatusPlaying() }, + EndDate = { Value = DateTimeOffset.Now.AddDays(1) } + }) { MatchingFilter = true }, + new DrawableRoom(new Room + { + Name = { Value = "Ended" }, + Status = { Value = new RoomStatusEnded() }, + EndDate = { Value = DateTimeOffset.Now } + }) { MatchingFilter = true }, + new DrawableRoom(new Room + { + Name = { Value = "Open" }, + Status = { Value = new RoomStatusOpen() }, + Category = { Value = RoomCategory.Realtime } + }) { MatchingFilter = true }, + } + }; + }); } [Test] From 6a55cb9df0e529a137c6fbd430bffb2676afbe40 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 19 Jul 2021 22:52:05 +0900 Subject: [PATCH 097/106] Revert unintended change It's a deeper issue with ScreenStack (see: https://github.com/ppy/osu-framework/issues/4619). --- .../Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index f35671a761..561fa220c8 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -326,7 +326,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { // the room may not be left immediately after a disconnection due to async flow, // so checking the IsConnected status is also required. - if (client?.Room == null || !client.IsConnected.Value) + if (client.Room == null || !client.IsConnected.Value) { // room has not been created yet; exit immediately. return base.OnExiting(next); From 9ea1f5900a78ca02bdb11efa37fd3a87b6f71889 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 20 Jul 2021 01:05:36 +0900 Subject: [PATCH 098/106] Don't consider version suffixes when checking for updates This is just to make life easier for me with deploys for now. --- osu.Game/Updater/SimpleUpdateManager.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/osu.Game/Updater/SimpleUpdateManager.cs b/osu.Game/Updater/SimpleUpdateManager.cs index 50572a7867..e0409e34df 100644 --- a/osu.Game/Updater/SimpleUpdateManager.cs +++ b/osu.Game/Updater/SimpleUpdateManager.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Newtonsoft.Json; using osu.Framework; @@ -41,11 +42,16 @@ namespace osu.Game.Updater var latest = releases.ResponseObject; - if (latest.TagName != version) + // avoid any discrepancies due to build suffixes for now. + // eventually we will want to support release streams and consider these. + version = version.Split('-').First(); + var latestTagName = latest.TagName.Split('-').First(); + + if (latestTagName != version) { Notifications.Post(new SimpleNotification { - Text = $"A newer release of osu! has been found ({version} → {latest.TagName}).\n\n" + Text = $"A newer release of osu! has been found ({version} → {latestTagName}).\n\n" + "Click here to download the new version, which can be installed over the top of your existing installation", Icon = FontAwesome.Solid.Upload, Activated = () => From a387d8df740d28acb4d5f285913537711e2db912 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Tue, 20 Jul 2021 10:30:40 +0800 Subject: [PATCH 099/106] Use `BeatSyncClock` --- osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs index 918b9b1c94..f682860498 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs @@ -374,8 +374,7 @@ namespace osu.Game.Rulesets.Osu.Mods int timeSignature = (int)timingPoint.TimeSignature; // play metronome from one measure before the first object. - // TODO: Use BeatSyncClock from https://github.com/ppy/osu/pull/13894. - if (Clock.CurrentTime < firstHitTime - timingPoint.BeatLength * timeSignature) + if (BeatSyncClock.CurrentTime < firstHitTime - timingPoint.BeatLength * timeSignature) return; sample.Frequency.Value = beatIndex % timeSignature == 0 ? 1 : 0.5f; From 1c6a13fca79aacd28a269effe010b4332a064264 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Tue, 20 Jul 2021 10:31:19 +0800 Subject: [PATCH 100/106] Disallow mistimed event firing --- osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs index f682860498..e21d1da009 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs @@ -353,6 +353,7 @@ namespace osu.Game.Rulesets.Osu.Mods public TargetBeatContainer(double firstHitTime) { this.firstHitTime = firstHitTime; + AllowMistimedEventFiring = false; Divisor = 1; } From fe414b942f69230c473398b8ee2e199c06449a84 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 20 Jul 2021 15:44:32 +0900 Subject: [PATCH 101/106] Ensure online play subscreen is loaded before forwarding `OnExiting` Closes https://github.com/ppy/osu-framework/issues/4619 (actually not a framework issue; the framework correctly guards against this scenario, see https://github.com/ppy/osu-framework/blob/4e29504384bb78bbccc0d492de58124f17270eb2/osu.Framework/Screens/ScreenStack.cs#L277). Added the assertions to be very explicit about the nested stack's state at this point. Both of those events can only be triggered if the stack has a loaded screen (as far as I can tell), making this check unnecessary in those cases. --- osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs index ceee002c6e..3aef05c26e 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Diagnostics; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; @@ -222,7 +223,9 @@ namespace osu.Game.Screens.OnlinePlay this.FadeIn(250); this.ScaleTo(1, 250, Easing.OutSine); - screenStack.CurrentScreen?.OnResuming(last); + Debug.Assert(screenStack.CurrentScreen != null); + screenStack.CurrentScreen.OnResuming(last); + base.OnResuming(last); UpdatePollingRate(isIdle.Value); @@ -233,14 +236,16 @@ namespace osu.Game.Screens.OnlinePlay this.ScaleTo(1.1f, 250, Easing.InSine); this.FadeOut(250); - screenStack.CurrentScreen?.OnSuspending(next); + Debug.Assert(screenStack.CurrentScreen != null); + screenStack.CurrentScreen.OnSuspending(next); UpdatePollingRate(isIdle.Value); } public override bool OnExiting(IScreen next) { - if (screenStack.CurrentScreen?.OnExiting(next) == true) + var subScreen = screenStack.CurrentScreen as Drawable; + if (subScreen?.IsLoaded == true && screenStack.CurrentScreen.OnExiting(next)) return true; RoomManager.PartRoom(); From 4113eae6ade3cc1d55819d0d19cde3fda4458877 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 20 Jul 2021 16:23:48 +0900 Subject: [PATCH 102/106] Add test coverage of fail scenario --- .../Multiplayer/TestSceneMultiplayer.cs | 33 +++++++++++++++++-- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index 92f9c5733f..f39455dc85 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -104,6 +104,36 @@ namespace osu.Game.Tests.Visual.Multiplayer }); } + [Test] + public void TestExitMidJoin() + { + Room room = null; + + AddStep("create room", () => + { + room = new Room + { + Name = { Value = "Test Room" }, + Playlist = + { + new PlaylistItem + { + Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo }, + Ruleset = { Value = new OsuRuleset().RulesetInfo }, + } + } + }; + }); + + AddStep("refresh rooms", () => multiplayerScreen.RoomManager.Filter.Value = new FilterCriteria()); + AddStep("select room", () => InputManager.Key(Key.Down)); + AddStep("join room and immediately exit", () => + { + multiplayerScreen.ChildrenOfType().Single().Open(room); + Schedule(() => Stack.CurrentScreen.Exit()); + }); + } + [Test] public void TestJoinRoomWithoutPassword() { @@ -179,9 +209,6 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType().FirstOrDefault()) != null); AddStep("enter password in text box", () => passwordEntryPopover.ChildrenOfType().First().Text = "password"); AddStep("press join room button", () => passwordEntryPopover.ChildrenOfType().First().Click()); - - AddUntilStep("wait for room open", () => this.ChildrenOfType().FirstOrDefault()?.IsLoaded == true); - AddUntilStep("wait for join", () => client.Room != null); } [Test] From d87ea741edb57bae885f714ec3e81158ce1e4999 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 20 Jul 2021 16:38:39 +0900 Subject: [PATCH 103/106] Fix `OnlinePlayScreen` sub-screens not loaded asynchronously --- .../Screens/OnlinePlay/OnlinePlayScreen.cs | 67 +++++++++++-------- 1 file changed, 40 insertions(+), 27 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs index ceee002c6e..a4bcad5776 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs @@ -30,17 +30,19 @@ namespace osu.Game.Screens.OnlinePlay [Cached] public abstract class OnlinePlayScreen : OsuScreen, IHasSubScreenStack { - public override bool CursorVisible => (screenStack.CurrentScreen as IOnlinePlaySubScreen)?.CursorVisible ?? true; + public override bool CursorVisible => (screenStack?.CurrentScreen as IOnlinePlaySubScreen)?.CursorVisible ?? true; // this is required due to PlayerLoader eventually being pushed to the main stack // while leases may be taken out by a subscreen. public override bool DisallowExternalBeatmapRulesetChanges => true; - private readonly MultiplayerWaveContainer waves; + private MultiplayerWaveContainer waves; - private readonly OsuButton createButton; - private readonly LoungeSubScreen loungeSubScreen; - private readonly ScreenStack screenStack; + private OsuButton createButton; + + private ScreenStack screenStack; + + private LoungeSubScreen loungeSubScreen; private readonly IBindable isIdle = new BindableBool(); @@ -54,7 +56,7 @@ namespace osu.Game.Screens.OnlinePlay private readonly Bindable currentFilter = new Bindable(new FilterCriteria()); [Cached] - private OngoingOperationTracker ongoingOperationTracker { get; set; } + private readonly OngoingOperationTracker ongoingOperationTracker = new OngoingOperationTracker(); [Resolved(CanBeNull = true)] private MusicController music { get; set; } @@ -65,11 +67,14 @@ namespace osu.Game.Screens.OnlinePlay [Resolved] protected IAPIProvider API { get; private set; } + [Resolved(CanBeNull = true)] + private IdleTracker idleTracker { get; set; } + [Resolved(CanBeNull = true)] private OsuLogo logo { get; set; } - private readonly Drawable header; - private readonly Drawable headerBackground; + private Drawable header; + private Drawable headerBackground; protected OnlinePlayScreen() { @@ -78,6 +83,14 @@ namespace osu.Game.Screens.OnlinePlay RelativeSizeAxes = Axes.Both; Padding = new MarginPadding { Horizontal = -HORIZONTAL_OVERFLOW_PADDING }; + RoomManager = CreateRoomManager(); + } + + private readonly IBindable apiState = new Bindable(); + + [BackgroundDependencyLoader] + private void load() + { var backgroundColour = Color4Extensions.FromHex(@"3e3a44"); InternalChild = waves = new MultiplayerWaveContainer @@ -144,27 +157,14 @@ namespace osu.Game.Screens.OnlinePlay }; button.Action = () => OpenNewRoom(); }), - RoomManager = CreateRoomManager(), - ongoingOperationTracker = new OngoingOperationTracker() + RoomManager, + ongoingOperationTracker, } }; - screenStack.ScreenPushed += screenPushed; - screenStack.ScreenExited += screenExited; - - screenStack.Push(loungeSubScreen = CreateLounge()); - } - - private readonly IBindable apiState = new Bindable(); - - [BackgroundDependencyLoader(true)] - private void load(IdleTracker idleTracker) - { - apiState.BindTo(API.State); - apiState.BindValueChanged(onlineStateChanged, true); - - if (idleTracker != null) - isIdle.BindTo(idleTracker.IsIdle); + // a lot of the functionality in this class depends on loungeSubScreen being in a ready to go state. + // as such, we intentionally load this inline so it is ready alongside this screen. + LoadComponent(loungeSubScreen = CreateLounge()); } private void onlineStateChanged(ValueChangedEvent state) => Schedule(() => @@ -179,7 +179,20 @@ namespace osu.Game.Screens.OnlinePlay protected override void LoadComplete() { base.LoadComplete(); - isIdle.BindValueChanged(idle => UpdatePollingRate(idle.NewValue), true); + + screenStack.ScreenPushed += screenPushed; + screenStack.ScreenExited += screenExited; + + screenStack.Push(loungeSubScreen); + + apiState.BindTo(API.State); + apiState.BindValueChanged(onlineStateChanged, true); + + if (idleTracker != null) + { + isIdle.BindTo(idleTracker.IsIdle); + isIdle.BindValueChanged(idle => UpdatePollingRate(idle.NewValue), true); + } } protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) From 26cc4af87c03fe513f4b5d31e27fb61db7245519 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 20 Jul 2021 16:44:51 +0900 Subject: [PATCH 104/106] Revert unintended changes --- osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index f39455dc85..36dd9c2de3 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -209,6 +209,9 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType().FirstOrDefault()) != null); AddStep("enter password in text box", () => passwordEntryPopover.ChildrenOfType().First().Text = "password"); AddStep("press join room button", () => passwordEntryPopover.ChildrenOfType().First().Click()); + + AddUntilStep("wait for room open", () => this.ChildrenOfType().FirstOrDefault()?.IsLoaded == true); + AddUntilStep("wait for join", () => client.Room != null); } [Test] From 1e634d9db091ea1ba71ddfa90a4ffd06c5e4aa71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 20 Jul 2021 21:48:38 +0200 Subject: [PATCH 105/106] Adjust user beatmap sections on profile overlay to match web --- osu.Game/Online/API/Requests/GetUserBeatmapsRequest.cs | 4 ++-- .../Sections/Beatmaps/PaginatedBeatmapContainer.cs | 8 ++++---- osu.Game/Overlays/Profile/Sections/BeatmapsSection.cs | 4 ++-- osu.Game/Users/User.cs | 8 ++++---- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/osu.Game/Online/API/Requests/GetUserBeatmapsRequest.cs b/osu.Game/Online/API/Requests/GetUserBeatmapsRequest.cs index fb1385793f..e2c0ed4301 100644 --- a/osu.Game/Online/API/Requests/GetUserBeatmapsRequest.cs +++ b/osu.Game/Online/API/Requests/GetUserBeatmapsRequest.cs @@ -26,9 +26,9 @@ namespace osu.Game.Online.API.Requests public enum BeatmapSetType { Favourite, - RankedAndApproved, + Ranked, Loved, - Unranked, + Pending, Graveyard } } diff --git a/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs b/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs index ec64371a5d..8657e356c9 100644 --- a/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs @@ -46,11 +46,11 @@ namespace osu.Game.Overlays.Profile.Sections.Beatmaps case BeatmapSetType.Loved: return user.LovedBeatmapsetCount; - case BeatmapSetType.RankedAndApproved: - return user.RankedAndApprovedBeatmapsetCount; + case BeatmapSetType.Ranked: + return user.RankedBeatmapsetCount; - case BeatmapSetType.Unranked: - return user.UnrankedBeatmapsetCount; + case BeatmapSetType.Pending: + return user.PendingBeatmapsetCount; default: return 0; diff --git a/osu.Game/Overlays/Profile/Sections/BeatmapsSection.cs b/osu.Game/Overlays/Profile/Sections/BeatmapsSection.cs index 843ab531be..af6ab4aad1 100644 --- a/osu.Game/Overlays/Profile/Sections/BeatmapsSection.cs +++ b/osu.Game/Overlays/Profile/Sections/BeatmapsSection.cs @@ -19,9 +19,9 @@ namespace osu.Game.Overlays.Profile.Sections Children = new[] { new PaginatedBeatmapContainer(BeatmapSetType.Favourite, User, UsersStrings.ShowExtraBeatmapsFavouriteTitle), - new PaginatedBeatmapContainer(BeatmapSetType.RankedAndApproved, User, UsersStrings.ShowExtraBeatmapsRankedTitle), + new PaginatedBeatmapContainer(BeatmapSetType.Ranked, User, UsersStrings.ShowExtraBeatmapsRankedTitle), new PaginatedBeatmapContainer(BeatmapSetType.Loved, User, UsersStrings.ShowExtraBeatmapsLovedTitle), - new PaginatedBeatmapContainer(BeatmapSetType.Unranked, User, UsersStrings.ShowExtraBeatmapsPendingTitle), + new PaginatedBeatmapContainer(BeatmapSetType.Pending, User, UsersStrings.ShowExtraBeatmapsPendingTitle), new PaginatedBeatmapContainer(BeatmapSetType.Graveyard, User, UsersStrings.ShowExtraBeatmapsGraveyardTitle) }; } diff --git a/osu.Game/Users/User.cs b/osu.Game/Users/User.cs index 2e04693e82..20c23153f0 100644 --- a/osu.Game/Users/User.cs +++ b/osu.Game/Users/User.cs @@ -138,11 +138,11 @@ namespace osu.Game.Users [JsonProperty(@"loved_beatmapset_count")] public int LovedBeatmapsetCount; - [JsonProperty(@"ranked_and_approved_beatmapset_count")] - public int RankedAndApprovedBeatmapsetCount; + [JsonProperty(@"ranked_beatmapset_count")] + public int RankedBeatmapsetCount; - [JsonProperty(@"unranked_beatmapset_count")] - public int UnrankedBeatmapsetCount; + [JsonProperty(@"pending_beatmapset_count")] + public int PendingBeatmapsetCount; [JsonProperty(@"scores_best_count")] public int ScoresBestCount; From db9cf443c7b5c2d76b06fe40f4993ad2bccc20a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 21 Jul 2021 00:04:51 +0200 Subject: [PATCH 106/106] Allow confirming room password by pressing Enter --- .../TestSceneMultiplayerLoungeSubScreen.cs | 16 ++++++++++++++++ .../OnlinePlay/Lounge/Components/DrawableRoom.cs | 1 + 2 files changed, 17 insertions(+) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs index de46d9e25a..4ea635fd3e 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs @@ -80,6 +80,22 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("room join password correct", () => lastJoinedPassword == "password"); } + [Test] + public void TestJoinRoomWithPasswordViaKeyboardOnly() + { + DrawableRoom.PasswordEntryPopover passwordEntryPopover = null; + + AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true)); + AddStep("select room", () => InputManager.Key(Key.Down)); + AddStep("attempt join room", () => InputManager.Key(Key.Enter)); + AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType().FirstOrDefault()) != null); + AddStep("enter password in text box", () => passwordEntryPopover.ChildrenOfType().First().Text = "password"); + AddStep("press enter", () => InputManager.Key(Key.Enter)); + + AddAssert("room join requested", () => lastJoinedRoom == RoomManager.Rooms.First()); + AddAssert("room join password correct", () => lastJoinedPassword == "password"); + } + private void onRoomJoined(Room room, string password) { lastJoinedRoom = room; diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs index 236408851f..940ae873ec 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs @@ -390,6 +390,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components base.LoadComplete(); Schedule(() => GetContainingInputManager().ChangeFocus(passwordTextbox)); + passwordTextbox.OnCommit += (_, __) => JoinRequested?.Invoke(room, passwordTextbox.Text); } } }