From 9dda7da489918120d251c6c266272f41a2fa8671 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 16 Apr 2020 14:11:38 +0900 Subject: [PATCH 01/35] Fix spinners being considered the "first object" for increased visibility in hidden --- .../Mods/TestSceneOsuModHidden.cs | 106 ++++++++++++++++++ osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs | 2 + osu.Game/Rulesets/Mods/ModHidden.cs | 14 ++- 3 files changed, 120 insertions(+), 2 deletions(-) create mode 100644 osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs new file mode 100644 index 0000000000..8bd3d3c7cc --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs @@ -0,0 +1,106 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Tests.Visual; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Tests.Mods +{ + public class TestSceneOsuModHidden : ModTestScene + { + public TestSceneOsuModHidden() + : base(new OsuRuleset()) + { + } + + [Test] + public void TestDefaultBeatmapTest() => CreateModTest(new ModTestData + { + Mod = new OsuModHidden(), + Autoplay = true, + PassCondition = checkSomeHit + }); + + [Test] + public void FirstCircleAfterTwoSpinners() => CreateModTest(new ModTestData + { + Mod = new OsuModHidden(), + Autoplay = true, + Beatmap = new Beatmap + { + HitObjects = new List + { + new Spinner + { + Position = new Vector2(256, 192), + EndTime = 1000, + }, + new Spinner + { + Position = new Vector2(256, 192), + StartTime = 1200, + EndTime = 2200, + }, + new HitCircle + { + Position = new Vector2(300, 192), + StartTime = 3200, + }, + new HitCircle + { + Position = new Vector2(384, 192), + StartTime = 4200, + } + } + }, + PassCondition = checkSomeHit + }); + + [Test] + public void FirstSliderAfterTwoSpinners() => CreateModTest(new ModTestData + { + Mod = new OsuModHidden(), + Autoplay = true, + Beatmap = new Beatmap + { + HitObjects = new List + { + new Spinner + { + Position = new Vector2(256, 192), + EndTime = 1000, + }, + new Spinner + { + Position = new Vector2(256, 192), + StartTime = 1200, + EndTime = 2200, + }, + new Slider + { + StartTime = 3200, + Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(100, 0), }) + }, + new Slider + { + StartTime = 5200, + Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(100, 0), }) + } + } + }, + PassCondition = checkSomeHit + }); + + private bool checkSomeHit() + { + return Player.ScoreProcessor.JudgedHits >= 4; + } + } +} diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs index 91a4e049e3..fdba03f260 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs @@ -23,6 +23,8 @@ namespace osu.Game.Rulesets.Osu.Mods private const double fade_in_duration_multiplier = 0.4; private const double fade_out_duration_multiplier = 0.3; + protected override bool IsFirstHideableObject(DrawableHitObject hitObject) => !(hitObject is DrawableSpinner); + public override void ApplyToDrawableHitObjects(IEnumerable drawables) { static void adjustFadeIn(OsuHitObject h) => h.TimeFadeIn = h.TimePreempt * fade_in_duration_multiplier; diff --git a/osu.Game/Rulesets/Mods/ModHidden.cs b/osu.Game/Rulesets/Mods/ModHidden.cs index 4e4a75db82..a1915b974c 100644 --- a/osu.Game/Rulesets/Mods/ModHidden.cs +++ b/osu.Game/Rulesets/Mods/ModHidden.cs @@ -23,6 +23,13 @@ namespace osu.Game.Rulesets.Mods protected Bindable IncreaseFirstObjectVisibility = new Bindable(); + /// + /// Check whether the provided hitobject should be considered the "first" hideable object. + /// Can be used to skip spinners, for instance. + /// + /// The hitobject to check. + protected virtual bool IsFirstHideableObject(DrawableHitObject hitObject) => true; + public void ReadFromConfig(OsuConfigManager config) { IncreaseFirstObjectVisibility = config.GetBindable(OsuSetting.IncreaseFirstObjectVisibility); @@ -30,8 +37,11 @@ namespace osu.Game.Rulesets.Mods public virtual void ApplyToDrawableHitObjects(IEnumerable drawables) { - foreach (var d in drawables.Skip(IncreaseFirstObjectVisibility.Value ? 1 : 0)) - d.ApplyCustomUpdateState += ApplyHiddenState; + if (IncreaseFirstObjectVisibility.Value) + drawables = drawables.SkipWhile(h => !IsFirstHideableObject(h)).Skip(1); + + foreach (var dho in drawables) + dho.ApplyCustomUpdateState += ApplyHiddenState; } public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor) From ef0da9e3e831096674d37ba799246de1d569a786 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Thu, 16 Apr 2020 11:01:36 +0300 Subject: [PATCH 02/35] Basic overlay layout implementation --- .../Online/TestSceneDashboardOverlay.cs | 43 +++++++++++++ .../Dashboard/DashboardOverlayHeader.cs | 24 +++++++ osu.Game/Overlays/DashboardOverlay.cs | 62 +++++++++++++++++++ 3 files changed, 129 insertions(+) create mode 100644 osu.Game.Tests/Visual/Online/TestSceneDashboardOverlay.cs create mode 100644 osu.Game/Overlays/Dashboard/DashboardOverlayHeader.cs create mode 100644 osu.Game/Overlays/DashboardOverlay.cs diff --git a/osu.Game.Tests/Visual/Online/TestSceneDashboardOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneDashboardOverlay.cs new file mode 100644 index 0000000000..df95f24686 --- /dev/null +++ b/osu.Game.Tests/Visual/Online/TestSceneDashboardOverlay.cs @@ -0,0 +1,43 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using NUnit.Framework; +using osu.Game.Overlays; +using osu.Game.Overlays.Dashboard; +using osu.Game.Overlays.Dashboard.Friends; + +namespace osu.Game.Tests.Visual.Online +{ + public class TestSceneDashboardOverlay : OsuTestScene + { + public override IReadOnlyList RequiredTypes => new[] + { + typeof(DashboardOverlay), + typeof(DashboardOverlayHeader), + typeof(FriendDisplay) + }; + + protected override bool UseOnlineAPI => true; + + private readonly DashboardOverlay overlay; + + public TestSceneDashboardOverlay() + { + Add(overlay = new DashboardOverlay()); + } + + [Test] + public void TestShow() + { + AddStep("Show", overlay.Show); + } + + [Test] + public void TestHide() + { + AddStep("Hide", overlay.Hide); + } + } +} diff --git a/osu.Game/Overlays/Dashboard/DashboardOverlayHeader.cs b/osu.Game/Overlays/Dashboard/DashboardOverlayHeader.cs new file mode 100644 index 0000000000..1c52b033a5 --- /dev/null +++ b/osu.Game/Overlays/Dashboard/DashboardOverlayHeader.cs @@ -0,0 +1,24 @@ +// 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.Overlays.Dashboard +{ + public class DashboardOverlayHeader : TabControlOverlayHeader + { + protected override OverlayTitle CreateTitle() => new DashboardTitle(); + + private class DashboardTitle : OverlayTitle + { + public DashboardTitle() + { + Title = "dashboard"; + IconTexture = "Icons/changelog"; + } + } + } + + public enum HomeOverlayTabs + { + Friends + } +} diff --git a/osu.Game/Overlays/DashboardOverlay.cs b/osu.Game/Overlays/DashboardOverlay.cs new file mode 100644 index 0000000000..a1a7c9889a --- /dev/null +++ b/osu.Game/Overlays/DashboardOverlay.cs @@ -0,0 +1,62 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays.Dashboard; + +namespace osu.Game.Overlays +{ + public class DashboardOverlay : FullscreenOverlay + { + private readonly Box background; + private readonly Container content; + + public DashboardOverlay() + : base(OverlayColourScheme.Purple) + { + Children = new Drawable[] + { + background = new Box + { + RelativeSizeAxes = Axes.Both + }, + new OverlayScrollContainer + { + RelativeSizeAxes = Axes.Both, + ScrollbarVisible = false, + Child = new FillFlowContainer + { + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new DashboardOverlayHeader + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Depth = -float.MaxValue + }, + content = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y + } + } + } + }, + new LoadingLayer(content), + }; + } + + [BackgroundDependencyLoader] + private void load() + { + background.Colour = ColourProvider.Background5; + } + } +} From 29bea4e11c03292545a9937a149f28c3686c14c4 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Thu, 16 Apr 2020 11:42:21 +0300 Subject: [PATCH 03/35] Implement OverlayView component --- .../Visual/Online/TestSceneFriendDisplay.cs | 17 ++- .../Dashboard/Friends/FriendDisplay.cs | 143 ++++++++---------- osu.Game/Overlays/OverlayView.cs | 71 +++++++++ 3 files changed, 149 insertions(+), 82 deletions(-) create mode 100644 osu.Game/Overlays/OverlayView.cs diff --git a/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs index cf365a7614..0b5ff1c960 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs @@ -10,6 +10,7 @@ using osu.Game.Users; using osu.Game.Overlays; using osu.Framework.Allocation; using NUnit.Framework; +using osu.Game.Online.API; namespace osu.Game.Tests.Visual.Online { @@ -27,7 +28,7 @@ namespace osu.Game.Tests.Visual.Online [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); - private FriendDisplay display; + private TestFriendDisplay display; [SetUp] public void Setup() => Schedule(() => @@ -35,7 +36,7 @@ namespace osu.Game.Tests.Visual.Online Child = new BasicScrollContainer { RelativeSizeAxes = Axes.Both, - Child = display = new FriendDisplay() + Child = display = new TestFriendDisplay() }; }); @@ -83,5 +84,17 @@ namespace osu.Game.Tests.Visual.Online LastVisit = DateTimeOffset.Now } }; + + private class TestFriendDisplay : FriendDisplay + { + public void Fetch() + { + base.APIStateChanged(API, APIState.Online); + } + + public override void APIStateChanged(IAPIProvider api, APIState state) + { + } + } } } diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs b/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs index 3c9b31daae..9764f82199 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs @@ -16,7 +16,7 @@ using osuTK; namespace osu.Game.Overlays.Dashboard.Friends { - public class FriendDisplay : CompositeDrawable + public class FriendDisplay : OverlayView> { private List users = new List(); @@ -26,15 +26,10 @@ namespace osu.Game.Overlays.Dashboard.Friends set { users = value; - onlineStreamControl.Populate(value); } } - [Resolved] - private IAPIProvider api { get; set; } - - private GetFriendsRequest request; private CancellationTokenSource cancellationToken; private Drawable currentContent; @@ -48,92 +43,85 @@ namespace osu.Game.Overlays.Dashboard.Friends public FriendDisplay() { - RelativeSizeAxes = Axes.X; - AutoSizeAxes = Axes.Y; - InternalChild = new FillFlowContainer + AddRange(new Drawable[] { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Children = new Drawable[] + new Container { - new Container + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Children = new Drawable[] + controlBackground = new Box { - controlBackground = new Box + RelativeSizeAxes = Axes.Both + }, + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { - RelativeSizeAxes = Axes.Both + Top = 20, + Horizontal = 45 }, - new Container - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Padding = new MarginPadding - { - Top = 20, - Horizontal = 45 - }, - Child = onlineStreamControl = new FriendOnlineStreamControl(), - } + Child = onlineStreamControl = new FriendOnlineStreamControl(), } - }, - new Container + } + }, + new Container + { + Name = "User List", + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] { - Name = "User List", - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Children = new Drawable[] + background = new Box { - background = new Box + RelativeSizeAxes = Axes.Both + }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Margin = new MarginPadding { Bottom = 20 }, + Children = new Drawable[] { - RelativeSizeAxes = Axes.Both - }, - new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Margin = new MarginPadding { Bottom = 20 }, - Children = new Drawable[] + new Container { - new Container + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Padding = new MarginPadding - { - Horizontal = 40, - Vertical = 20 - }, - Child = userListToolbar = new UserListToolbar - { - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - } + Horizontal = 40, + Vertical = 20 }, - new Container + Child = userListToolbar = new UserListToolbar { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Children = new Drawable[] + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + } + }, + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + itemsPlaceholder = new Container { - itemsPlaceholder = new Container - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Horizontal = 50 } - }, - loading = new LoadingLayer(itemsPlaceholder) - } + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Horizontal = 50 } + }, + loading = new LoadingLayer(itemsPlaceholder) } } } } } } - }; + }); } [BackgroundDependencyLoader] @@ -152,14 +140,11 @@ namespace osu.Game.Overlays.Dashboard.Friends userListToolbar.SortCriteria.BindValueChanged(_ => recreatePanels()); } - public void Fetch() - { - if (!api.IsLoggedIn) - return; + protected override APIRequest> CreateRequest() => new GetFriendsRequest(); - request = new GetFriendsRequest(); - request.Success += response => Schedule(() => Users = response); - api.Queue(request); + protected override void OnSuccess(List response) + { + Users = response; } private void recreatePanels() @@ -258,9 +243,7 @@ namespace osu.Game.Overlays.Dashboard.Friends protected override void Dispose(bool isDisposing) { - request?.Cancel(); cancellationToken?.Cancel(); - base.Dispose(isDisposing); } } diff --git a/osu.Game/Overlays/OverlayView.cs b/osu.Game/Overlays/OverlayView.cs new file mode 100644 index 0000000000..f39c6bd1b9 --- /dev/null +++ b/osu.Game/Overlays/OverlayView.cs @@ -0,0 +1,71 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Online.API; + +namespace osu.Game.Overlays +{ + /// + /// Drawable which used to represent online content in . + /// + /// Response type + public abstract class OverlayView : Container, IOnlineComponent + where T : class + { + [Resolved] + protected IAPIProvider API { get; private set; } + + protected override Container Content => content; + + private readonly FillFlowContainer content; + + protected OverlayView() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + AddInternal(content = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + }); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + API.Register(this); + } + + private APIRequest request; + + protected abstract APIRequest CreateRequest(); + + protected abstract void OnSuccess(T response); + + public virtual void APIStateChanged(IAPIProvider api, APIState state) + { + switch (state) + { + case APIState.Online: + request = CreateRequest(); + request.Success += response => Schedule(() => OnSuccess(response)); + api.Queue(request); + break; + + default: + break; + } + } + + protected override void Dispose(bool isDisposing) + { + request?.Cancel(); + API?.Unregister(this); + base.Dispose(isDisposing); + } + } +} From 894598eb220e7cc05f3fab5df81a786973f804d5 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Thu, 16 Apr 2020 12:05:51 +0300 Subject: [PATCH 04/35] Replace SocialOverlay with DashboardOverlay --- osu.Game.Tests/Visual/TestSceneOsuGame.cs | 2 +- osu.Game/OsuGame.cs | 8 +- .../Dashboard/DashboardOverlayHeader.cs | 4 +- osu.Game/Overlays/DashboardOverlay.cs | 94 ++++++++++++++++++- .../Overlays/Toolbar/ToolbarSocialButton.cs | 4 +- 5 files changed, 100 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tests/Visual/TestSceneOsuGame.cs b/osu.Game.Tests/Visual/TestSceneOsuGame.cs index 492494ada3..8793d880e3 100644 --- a/osu.Game.Tests/Visual/TestSceneOsuGame.cs +++ b/osu.Game.Tests/Visual/TestSceneOsuGame.cs @@ -48,7 +48,7 @@ namespace osu.Game.Tests.Visual typeof(OnScreenDisplay), typeof(NotificationOverlay), typeof(DirectOverlay), - typeof(SocialOverlay), + typeof(DashboardOverlay), typeof(ChannelManager), typeof(ChatOverlay), typeof(SettingsOverlay), diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 5e93d760e3..c861b84835 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -67,7 +67,7 @@ namespace osu.Game private DirectOverlay direct; - private SocialOverlay social; + private DashboardOverlay dashboard; private UserProfileOverlay userProfile; @@ -611,7 +611,7 @@ namespace osu.Game //overlay elements loadComponentSingleFile(direct = new DirectOverlay(), overlayContent.Add, true); - loadComponentSingleFile(social = new SocialOverlay(), overlayContent.Add, true); + loadComponentSingleFile(dashboard = new DashboardOverlay(), overlayContent.Add, true); var rankingsOverlay = loadComponentSingleFile(new RankingsOverlay(), overlayContent.Add, true); loadComponentSingleFile(channelManager = new ChannelManager(), AddInternal, true); loadComponentSingleFile(chatOverlay = new ChatOverlay(), overlayContent.Add, true); @@ -670,7 +670,7 @@ namespace osu.Game } // ensure only one of these overlays are open at once. - var singleDisplayOverlays = new OverlayContainer[] { chatOverlay, social, direct, changelogOverlay, rankingsOverlay }; + var singleDisplayOverlays = new OverlayContainer[] { chatOverlay, dashboard, direct, changelogOverlay, rankingsOverlay }; foreach (var overlay in singleDisplayOverlays) { @@ -842,7 +842,7 @@ namespace osu.Game return true; case GlobalAction.ToggleSocial: - social.ToggleVisibility(); + dashboard.ToggleVisibility(); return true; case GlobalAction.ResetInputSettings: diff --git a/osu.Game/Overlays/Dashboard/DashboardOverlayHeader.cs b/osu.Game/Overlays/Dashboard/DashboardOverlayHeader.cs index 1c52b033a5..9ee679a866 100644 --- a/osu.Game/Overlays/Dashboard/DashboardOverlayHeader.cs +++ b/osu.Game/Overlays/Dashboard/DashboardOverlayHeader.cs @@ -3,7 +3,7 @@ namespace osu.Game.Overlays.Dashboard { - public class DashboardOverlayHeader : TabControlOverlayHeader + public class DashboardOverlayHeader : TabControlOverlayHeader { protected override OverlayTitle CreateTitle() => new DashboardTitle(); @@ -17,7 +17,7 @@ namespace osu.Game.Overlays.Dashboard } } - public enum HomeOverlayTabs + public enum DashboardOverlayTabs { Friends } diff --git a/osu.Game/Overlays/DashboardOverlay.cs b/osu.Game/Overlays/DashboardOverlay.cs index a1a7c9889a..1e0fbc90b4 100644 --- a/osu.Game/Overlays/DashboardOverlay.cs +++ b/osu.Game/Overlays/DashboardOverlay.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; +using System.Threading; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API; using osu.Game.Overlays.Dashboard; +using osu.Game.Overlays.Dashboard.Friends; namespace osu.Game.Overlays { public class DashboardOverlay : FullscreenOverlay { + private CancellationTokenSource cancellationToken; + private readonly Box background; private readonly Container content; + private readonly DashboardOverlayHeader header; + private readonly LoadingLayer loading; + private readonly OverlayScrollContainer scrollFlow; public DashboardOverlay() : base(OverlayColourScheme.Purple) @@ -24,7 +34,7 @@ namespace osu.Game.Overlays { RelativeSizeAxes = Axes.Both }, - new OverlayScrollContainer + scrollFlow = new OverlayScrollContainer { RelativeSizeAxes = Axes.Both, ScrollbarVisible = false, @@ -35,7 +45,7 @@ namespace osu.Game.Overlays Direction = FillDirection.Vertical, Children = new Drawable[] { - new DashboardOverlayHeader + header = new DashboardOverlayHeader { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, @@ -49,7 +59,7 @@ namespace osu.Game.Overlays } } }, - new LoadingLayer(content), + loading = new LoadingLayer(content), }; } @@ -58,5 +68,83 @@ namespace osu.Game.Overlays { background.Colour = ColourProvider.Background5; } + + protected override void LoadComplete() + { + base.LoadComplete(); + header.Current.BindValueChanged(onTabChanged); + } + + private bool displayUpdateRequired = true; + + protected override void PopIn() + { + base.PopIn(); + + // We don't want to create new display on every call, only when exiting from fully closed state. + if (displayUpdateRequired) + { + header.Current.TriggerChange(); + displayUpdateRequired = false; + } + } + + protected override void PopOutComplete() + { + base.PopOutComplete(); + loadDisplay(Empty()); + displayUpdateRequired = true; + } + + private void loadDisplay(Drawable display) + { + scrollFlow.ScrollToStart(); + + LoadComponentAsync(display, loaded => + { + loading.Hide(); + content.Child = loaded; + }, (cancellationToken = new CancellationTokenSource()).Token); + } + + private void onTabChanged(ValueChangedEvent tab) + { + cancellationToken?.Cancel(); + + loading.Show(); + + switch (tab.NewValue) + { + case DashboardOverlayTabs.Friends: + loadDisplay(new FriendDisplay()); + break; + + default: + throw new NotImplementedException($"Display for {tab.NewValue} tab is not implemented"); + } + } + + public override void APIStateChanged(IAPIProvider api, APIState state) + { + switch (state) + { + case APIState.Online: + // Will force to create a display based on visibility state + displayUpdateRequired = true; + State.TriggerChange(); + return; + + default: + content.Clear(); + loading.Show(); + return; + } + } + + protected override void Dispose(bool isDisposing) + { + cancellationToken?.Cancel(); + base.Dispose(isDisposing); + } } } diff --git a/osu.Game/Overlays/Toolbar/ToolbarSocialButton.cs b/osu.Game/Overlays/Toolbar/ToolbarSocialButton.cs index 5e353d3319..f6646eb81d 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarSocialButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarSocialButton.cs @@ -14,9 +14,9 @@ namespace osu.Game.Overlays.Toolbar } [BackgroundDependencyLoader(true)] - private void load(SocialOverlay chat) + private void load(DashboardOverlay dashboard) { - StateContainer = chat; + StateContainer = dashboard; } } } From eb86be0a6da6d76dfef8526eff26ddb584d8bd7b Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Thu, 16 Apr 2020 12:07:38 +0300 Subject: [PATCH 05/35] Adjust header content margin --- osu.Game/Overlays/OverlayHeader.cs | 4 +++- osu.Game/Overlays/TabControlOverlayHeader.cs | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/OverlayHeader.cs b/osu.Game/Overlays/OverlayHeader.cs index 4ac0f697c3..dbc934bde9 100644 --- a/osu.Game/Overlays/OverlayHeader.cs +++ b/osu.Game/Overlays/OverlayHeader.cs @@ -12,6 +12,8 @@ namespace osu.Game.Overlays { public abstract class OverlayHeader : Container { + public const int CONTENT_X_MARGIN = 50; + private readonly Box titleBackground; protected readonly FillFlowContainer HeaderInfo; @@ -54,7 +56,7 @@ namespace osu.Game.Overlays AutoSizeAxes = Axes.Y, Padding = new MarginPadding { - Horizontal = UserProfileOverlay.CONTENT_X_MARGIN, + Horizontal = CONTENT_X_MARGIN, }, Children = new[] { diff --git a/osu.Game/Overlays/TabControlOverlayHeader.cs b/osu.Game/Overlays/TabControlOverlayHeader.cs index ab1a6aff78..e8e000f441 100644 --- a/osu.Game/Overlays/TabControlOverlayHeader.cs +++ b/osu.Game/Overlays/TabControlOverlayHeader.cs @@ -44,7 +44,7 @@ namespace osu.Game.Overlays }, TabControl = CreateTabControl().With(control => { - control.Margin = new MarginPadding { Left = UserProfileOverlay.CONTENT_X_MARGIN }; + control.Margin = new MarginPadding { Left = CONTENT_X_MARGIN }; control.Current = Current; }) } From 87f52b82331dc1f6ba4b198d96cb8b768a152c19 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Thu, 16 Apr 2020 12:09:44 +0300 Subject: [PATCH 06/35] Remove redundant switch section --- osu.Game/Overlays/OverlayView.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game/Overlays/OverlayView.cs b/osu.Game/Overlays/OverlayView.cs index f39c6bd1b9..e3a07fc2de 100644 --- a/osu.Game/Overlays/OverlayView.cs +++ b/osu.Game/Overlays/OverlayView.cs @@ -55,9 +55,6 @@ namespace osu.Game.Overlays request.Success += response => Schedule(() => OnSuccess(response)); api.Queue(request); break; - - default: - break; } } From c5a343d3a07daf31ad95a036850a05e7007f2a41 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Thu, 16 Apr 2020 14:10:39 +0300 Subject: [PATCH 07/35] Fix overlay accepting state changes while hidden --- osu.Game/Overlays/DashboardOverlay.cs | 28 +++++++++++++-------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/osu.Game/Overlays/DashboardOverlay.cs b/osu.Game/Overlays/DashboardOverlay.cs index 1e0fbc90b4..86c0f3bd83 100644 --- a/osu.Game/Overlays/DashboardOverlay.cs +++ b/osu.Game/Overlays/DashboardOverlay.cs @@ -81,7 +81,7 @@ namespace osu.Game.Overlays { base.PopIn(); - // We don't want to create new display on every call, only when exiting from fully closed state. + // We don't want to create a new display on every call, only when exiting from fully closed state. if (displayUpdateRequired) { header.Current.TriggerChange(); @@ -102,7 +102,9 @@ namespace osu.Game.Overlays LoadComponentAsync(display, loaded => { - loading.Hide(); + if (API.IsLoggedIn) + loading.Hide(); + content.Child = loaded; }, (cancellationToken = new CancellationTokenSource()).Token); } @@ -110,9 +112,14 @@ namespace osu.Game.Overlays private void onTabChanged(ValueChangedEvent tab) { cancellationToken?.Cancel(); - loading.Show(); + if (!API.IsLoggedIn) + { + loadDisplay(Empty()); + return; + } + switch (tab.NewValue) { case DashboardOverlayTabs.Friends: @@ -126,19 +133,10 @@ namespace osu.Game.Overlays public override void APIStateChanged(IAPIProvider api, APIState state) { - switch (state) - { - case APIState.Online: - // Will force to create a display based on visibility state - displayUpdateRequired = true; - State.TriggerChange(); - return; + if (State.Value == Visibility.Hidden) + return; - default: - content.Clear(); - loading.Show(); - return; - } + header.Current.TriggerChange(); } protected override void Dispose(bool isDisposing) From 3daacbc2d202b3d42ac84e7242e571a045d8fa09 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 17 Apr 2020 13:34:20 +0900 Subject: [PATCH 08/35] Initial inefficient refactor of hitobject enumeration --- osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs | 76 ++++++++------------ osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 2 +- 2 files changed, 29 insertions(+), 49 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs b/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs index dfca2aff7b..171ce6fe61 100644 --- a/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.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.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; -using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.UI; @@ -37,12 +37,9 @@ namespace osu.Game.Rulesets.Osu.UI DrawableHitObject blockingObject = null; // Find the last hitobject which blocks future hits. - foreach (var obj in hitObjectContainer.AliveObjects) + foreach (var obj in enumerateHitObjectsUpTo(hitObject)) { - if (obj == hitObject) - break; - - if (drawableCanBlockFutureHits(obj)) + if (hitObjectCanBlockFutureHits(obj)) blockingObject = obj; } @@ -64,64 +61,47 @@ namespace osu.Game.Rulesets.Osu.UI /// Handles a being hit to potentially miss all earlier s. /// /// The that was hit. - public void HandleHit(HitObject hitObject) + public void HandleHit(DrawableHitObject hitObject) { // Hitobjects which themselves don't block future hitobjects don't cause misses (e.g. slider ticks, spinners). if (!hitObjectCanBlockFutureHits(hitObject)) return; - double maximumTime = hitObject.StartTime; - - // Iterate through and apply miss results to all top-level and nested hitobjects which block future hits. - foreach (var obj in hitObjectContainer.AliveObjects) + foreach (var obj in enumerateHitObjectsUpTo(hitObject)) { - if (obj.Judged || obj.HitObject.StartTime >= maximumTime) + if (obj.Judged) continue; - if (hitObjectCanBlockFutureHits(obj.HitObject)) - applyMiss(obj); - - foreach (var nested in obj.NestedHitObjects) - { - if (nested.Judged || nested.HitObject.StartTime >= maximumTime) - continue; - - if (hitObjectCanBlockFutureHits(nested.HitObject)) - applyMiss(nested); - } + if (hitObjectCanBlockFutureHits(obj)) + ((DrawableOsuHitObject)obj).MissForcefully(); } - - static void applyMiss(DrawableHitObject obj) => ((DrawableOsuHitObject)obj).MissForcefully(); - } - - /// - /// Whether a blocks hits on future s until its start time is reached. - /// - /// - /// This will ONLY match on top-most s. - /// - /// The to test. - private static bool drawableCanBlockFutureHits(DrawableHitObject hitObject) - { - // Special considerations for slider tails aren't required since only top-most drawable hitobjects are being iterated over. - return hitObject is DrawableHitCircle || hitObject is DrawableSlider; } /// /// Whether a blocks hits on future s until its start time is reached. /// - /// - /// This is more rigorous and may not match on top-most s as does. - /// /// The to test. - private static bool hitObjectCanBlockFutureHits(HitObject hitObject) - { - // Unlike the above we will receive slider tails, but they do not block future hits. - if (hitObject is SliderTailCircle) - return false; + private static bool hitObjectCanBlockFutureHits(DrawableHitObject hitObject) + => hitObject is DrawableHitCircle; - // All other hitcircles continue to block future hits. - return hitObject is HitCircle; + // Todo: Inefficient + private IEnumerable enumerateHitObjectsUpTo(DrawableHitObject hitObject) + { + return enumerate(hitObjectContainer.AliveObjects); + + IEnumerable enumerate(IEnumerable list) + { + foreach (var obj in list) + { + if (obj.HitObject.StartTime >= hitObject.HitObject.StartTime) + yield break; + + yield return obj; + + foreach (var nested in enumerate(obj.NestedHitObjects)) + yield return nested; + } + } } } } diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index 2f222f59b4..4b1a2ce43c 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -86,7 +86,7 @@ namespace osu.Game.Rulesets.Osu.UI private void onNewResult(DrawableHitObject judgedObject, JudgementResult result) { // Hitobjects that block future hits should miss previous hitobjects if they're hit out-of-order. - hitPolicy.HandleHit(result.HitObject); + hitPolicy.HandleHit(judgedObject); if (!judgedObject.DisplayResult || !DisplayJudgements.Value) return; From 62f77a05befb156ac6cda6411f2dda85ffcc8b44 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 17 Apr 2020 14:00:00 +0900 Subject: [PATCH 09/35] Optimise by removing state machine --- osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs | 115 +++++++++++++++---- 1 file changed, 95 insertions(+), 20 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs b/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs index 171ce6fe61..b55e04ec4c 100644 --- a/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs @@ -1,7 +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; using System.Collections.Generic; +using System.Diagnostics; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables; @@ -36,11 +38,14 @@ namespace osu.Game.Rulesets.Osu.UI { DrawableHitObject blockingObject = null; - // Find the last hitobject which blocks future hits. - foreach (var obj in enumerateHitObjectsUpTo(hitObject)) + var enumerator = new HitObjectEnumerator(hitObjectContainer, hitObject.HitObject.StartTime); + + while (enumerator.MoveNext()) { - if (hitObjectCanBlockFutureHits(obj)) - blockingObject = obj; + Debug.Assert(enumerator.Current != null); + + if (hitObjectCanBlockFutureHits(enumerator.Current)) + blockingObject = enumerator.Current; } // If there is no previous hitobject, allow the hit. @@ -67,13 +72,17 @@ namespace osu.Game.Rulesets.Osu.UI if (!hitObjectCanBlockFutureHits(hitObject)) return; - foreach (var obj in enumerateHitObjectsUpTo(hitObject)) + var enumerator = new HitObjectEnumerator(hitObjectContainer, hitObject.HitObject.StartTime); + + while (enumerator.MoveNext()) { - if (obj.Judged) + Debug.Assert(enumerator.Current != null); + + if (enumerator.Current.Judged) continue; - if (hitObjectCanBlockFutureHits(obj)) - ((DrawableOsuHitObject)obj).MissForcefully(); + if (hitObjectCanBlockFutureHits(enumerator.Current)) + ((DrawableOsuHitObject)enumerator.Current).MissForcefully(); } } @@ -84,23 +93,89 @@ namespace osu.Game.Rulesets.Osu.UI private static bool hitObjectCanBlockFutureHits(DrawableHitObject hitObject) => hitObject is DrawableHitCircle; - // Todo: Inefficient - private IEnumerable enumerateHitObjectsUpTo(DrawableHitObject hitObject) + private struct HitObjectEnumerator : IEnumerator { - return enumerate(hitObjectContainer.AliveObjects); + private readonly IEnumerator hitObjectEnumerator; + private readonly double targetTime; - IEnumerable enumerate(IEnumerable list) + private DrawableHitObject currentTopLevel; + private int currentNestedIndex; + + public HitObjectEnumerator(HitObjectContainer hitObjectContainer, double targetTime) { - foreach (var obj in list) - { - if (obj.HitObject.StartTime >= hitObject.HitObject.StartTime) - yield break; + hitObjectEnumerator = hitObjectContainer.AliveObjects.GetEnumerator(); + this.targetTime = targetTime; - yield return obj; + currentTopLevel = null; + currentNestedIndex = -1; + Current = null; + } - foreach (var nested in enumerate(obj.NestedHitObjects)) - yield return nested; - } + /// + /// Attempts to move to the next top-level or nested hitobject. + /// Stops when no such hitobject is found or until the hitobject start time reaches . + /// + /// Whether a new hitobject was moved to. + public bool MoveNext() + { + // If we don't already have a top-level hitobject, try to get one. + if (currentTopLevel == null) + return moveNextTopLevel(); + + // If we have a top-level hitobject, try to move to the next nested hitobject or otherwise move to the next top-level hitobject. + if (!moveNextNested()) + return moveNextTopLevel(); + + // Guaranteed by moveNextNested() to have a hitobject. + return true; + } + + /// + /// Attempts to move to the next top-level hitobject. + /// + /// Whether a new top-level hitobject was found. + private bool moveNextTopLevel() + { + currentNestedIndex = -1; + + hitObjectEnumerator.MoveNext(); + currentTopLevel = hitObjectEnumerator.Current; + + Current = currentTopLevel; + + return Current?.HitObject.StartTime < targetTime; + } + + /// + /// Attempts to move to the next nested hitobject in the current top-level hitobject. + /// + /// Whether a new nested hitobject was moved to. + private bool moveNextNested() + { + currentNestedIndex++; + if (currentNestedIndex >= currentTopLevel.NestedHitObjects.Count) + return false; + + Current = currentTopLevel.NestedHitObjects[currentNestedIndex]; + Debug.Assert(Current != null); + + return Current?.HitObject.StartTime < targetTime; + } + + public void Reset() + { + hitObjectEnumerator.Reset(); + currentTopLevel = null; + currentNestedIndex = -1; + Current = null; + } + + public DrawableHitObject Current { get; set; } + + object IEnumerator.Current => Current; + + public void Dispose() + { } } } From ee5301b887a78a3bd0cabab17c306857133da794 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 17 Apr 2020 14:12:38 +0900 Subject: [PATCH 10/35] Fix head/tail circles not getting correct hit windows --- osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs index d6858f831e..3df51be600 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs @@ -371,6 +371,9 @@ namespace osu.Game.Rulesets.Osu.Tests { HeadCircle.HitWindows = new TestHitWindows(); TailCircle.HitWindows = new TestHitWindows(); + + HeadCircle.HitWindows.SetDifficulty(0); + TailCircle.HitWindows.SetDifficulty(0); }; } } From 08df9d49e52a968691b77a76fe360c3111eb3436 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 17 Apr 2020 14:12:43 +0900 Subject: [PATCH 11/35] Add failing test --- .../TestSceneOutOfOrderHits.cs | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs index 3df51be600..40ee53e8f2 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs @@ -296,6 +296,44 @@ namespace osu.Game.Rulesets.Osu.Tests addJudgementAssert(hitObjects[1], HitResult.Great); } + [Test] + public void TestHitSliderHeadBeforeHitCircle() + { + const double time_circle = 1000; + const double time_slider = 1200; + Vector2 positionCircle = Vector2.Zero; + Vector2 positionSlider = new Vector2(80); + + var hitObjects = new List + { + new TestHitCircle + { + StartTime = time_circle, + Position = positionCircle + }, + new TestSlider + { + StartTime = time_slider, + Position = positionSlider, + Path = new SliderPath(PathType.Linear, new[] + { + Vector2.Zero, + new Vector2(25, 0), + }) + } + }; + + performTest(hitObjects, new List + { + new OsuReplayFrame { Time = time_circle - 100, Position = positionSlider, Actions = { OsuAction.LeftButton } }, + new OsuReplayFrame { Time = time_circle, Position = positionCircle, Actions = { OsuAction.RightButton } }, + new OsuReplayFrame { Time = time_slider, Position = positionSlider, Actions = { OsuAction.LeftButton } }, + }); + + addJudgementAssert(hitObjects[0], HitResult.Great); + addJudgementAssert(hitObjects[1], HitResult.Great); + } + private void addJudgementAssert(OsuHitObject hitObject, HitResult result) { AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judgement is {result}", From a4a782381797f927bc80f108cbbf94f410faef99 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 17 Apr 2020 14:22:03 +0900 Subject: [PATCH 12/35] Add fail-safe to ensure hittability after a hit --- osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs b/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs index b55e04ec4c..31edefea83 100644 --- a/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.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.Collections; using System.Collections.Generic; using System.Diagnostics; @@ -72,6 +73,9 @@ namespace osu.Game.Rulesets.Osu.UI if (!hitObjectCanBlockFutureHits(hitObject)) return; + if (!IsHittable(hitObject, hitObject.HitObject.StartTime + hitObject.Result.TimeOffset)) + throw new InvalidOperationException($"A {hitObject} was hit before it become hittable!"); + var enumerator = new HitObjectEnumerator(hitObjectContainer, hitObject.HitObject.StartTime); while (enumerator.MoveNext()) From 4e4fe5cc904107ac647ffaa57d5ac17361ee073e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 17 Apr 2020 14:33:29 +0900 Subject: [PATCH 13/35] Fix slider heads not being blocked when hit out of order --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 522217a916..72502c02cd 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -125,7 +125,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables return new DrawableSliderTail(slider, tail); case SliderHeadCircle head: - return new DrawableSliderHead(slider, head) { OnShake = Shake }; + return new DrawableSliderHead(slider, head) + { + OnShake = Shake, + CheckHittable = (d, t) => CheckHittable?.Invoke(d, t) ?? true + }; case SliderTick tick: return new DrawableSliderTick(tick) { Position = tick.Position - slider.Position }; From 2dee5e03e30f158880a09aaa04ded47879d6f74d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 17 Apr 2020 14:40:29 +0900 Subject: [PATCH 14/35] Dispose enumerators for safety --- osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs | 31 +++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs b/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs index 31edefea83..4bc7da4794 100644 --- a/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs @@ -39,14 +39,15 @@ namespace osu.Game.Rulesets.Osu.UI { DrawableHitObject blockingObject = null; - var enumerator = new HitObjectEnumerator(hitObjectContainer, hitObject.HitObject.StartTime); - - while (enumerator.MoveNext()) + using (var enumerator = new HitObjectEnumerator(hitObjectContainer, hitObject.HitObject.StartTime)) { - Debug.Assert(enumerator.Current != null); + while (enumerator.MoveNext()) + { + Debug.Assert(enumerator.Current != null); - if (hitObjectCanBlockFutureHits(enumerator.Current)) - blockingObject = enumerator.Current; + if (hitObjectCanBlockFutureHits(enumerator.Current)) + blockingObject = enumerator.Current; + } } // If there is no previous hitobject, allow the hit. @@ -76,17 +77,18 @@ namespace osu.Game.Rulesets.Osu.UI if (!IsHittable(hitObject, hitObject.HitObject.StartTime + hitObject.Result.TimeOffset)) throw new InvalidOperationException($"A {hitObject} was hit before it become hittable!"); - var enumerator = new HitObjectEnumerator(hitObjectContainer, hitObject.HitObject.StartTime); - - while (enumerator.MoveNext()) + using (var enumerator = new HitObjectEnumerator(hitObjectContainer, hitObject.HitObject.StartTime)) { - Debug.Assert(enumerator.Current != null); + while (enumerator.MoveNext()) + { + Debug.Assert(enumerator.Current != null); - if (enumerator.Current.Judged) - continue; + if (enumerator.Current.Judged) + continue; - if (hitObjectCanBlockFutureHits(enumerator.Current)) - ((DrawableOsuHitObject)enumerator.Current).MissForcefully(); + if (hitObjectCanBlockFutureHits(enumerator.Current)) + ((DrawableOsuHitObject)enumerator.Current).MissForcefully(); + } } } @@ -180,6 +182,7 @@ namespace osu.Game.Rulesets.Osu.UI public void Dispose() { + hitObjectEnumerator?.Dispose(); } } } From bbcbd7e3fbc790e91415bc96ffcfb93c63bcffc6 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 17 Apr 2020 14:48:12 +0900 Subject: [PATCH 15/35] Simplify by removing custom enumerator --- osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs | 118 +++---------------- 1 file changed, 19 insertions(+), 99 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs b/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs index 4bc7da4794..cd9838e7bf 100644 --- a/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs @@ -2,9 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections; using System.Collections.Generic; -using System.Diagnostics; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables; @@ -39,15 +37,10 @@ namespace osu.Game.Rulesets.Osu.UI { DrawableHitObject blockingObject = null; - using (var enumerator = new HitObjectEnumerator(hitObjectContainer, hitObject.HitObject.StartTime)) + foreach (var obj in enumerateHitObjectsUpTo(hitObject.HitObject.StartTime)) { - while (enumerator.MoveNext()) - { - Debug.Assert(enumerator.Current != null); - - if (hitObjectCanBlockFutureHits(enumerator.Current)) - blockingObject = enumerator.Current; - } + if (hitObjectCanBlockFutureHits(obj)) + blockingObject = obj; } // If there is no previous hitobject, allow the hit. @@ -77,18 +70,13 @@ namespace osu.Game.Rulesets.Osu.UI if (!IsHittable(hitObject, hitObject.HitObject.StartTime + hitObject.Result.TimeOffset)) throw new InvalidOperationException($"A {hitObject} was hit before it become hittable!"); - using (var enumerator = new HitObjectEnumerator(hitObjectContainer, hitObject.HitObject.StartTime)) + foreach (var obj in enumerateHitObjectsUpTo(hitObject.HitObject.StartTime)) { - while (enumerator.MoveNext()) - { - Debug.Assert(enumerator.Current != null); + if (obj.Judged) + continue; - if (enumerator.Current.Judged) - continue; - - if (hitObjectCanBlockFutureHits(enumerator.Current)) - ((DrawableOsuHitObject)enumerator.Current).MissForcefully(); - } + if (hitObjectCanBlockFutureHits(obj)) + ((DrawableOsuHitObject)obj).MissForcefully(); } } @@ -99,90 +87,22 @@ namespace osu.Game.Rulesets.Osu.UI private static bool hitObjectCanBlockFutureHits(DrawableHitObject hitObject) => hitObject is DrawableHitCircle; - private struct HitObjectEnumerator : IEnumerator + private IEnumerable enumerateHitObjectsUpTo(double targetTime) { - private readonly IEnumerator hitObjectEnumerator; - private readonly double targetTime; - - private DrawableHitObject currentTopLevel; - private int currentNestedIndex; - - public HitObjectEnumerator(HitObjectContainer hitObjectContainer, double targetTime) + foreach (var obj in hitObjectContainer.AliveObjects) { - hitObjectEnumerator = hitObjectContainer.AliveObjects.GetEnumerator(); - this.targetTime = targetTime; + if (obj.HitObject.StartTime >= targetTime) + yield break; - currentTopLevel = null; - currentNestedIndex = -1; - Current = null; - } + yield return obj; - /// - /// Attempts to move to the next top-level or nested hitobject. - /// Stops when no such hitobject is found or until the hitobject start time reaches . - /// - /// Whether a new hitobject was moved to. - public bool MoveNext() - { - // If we don't already have a top-level hitobject, try to get one. - if (currentTopLevel == null) - return moveNextTopLevel(); + for (int i = 0; i < obj.NestedHitObjects.Count; i++) + { + if (obj.NestedHitObjects[i].HitObject.StartTime >= targetTime) + break; - // If we have a top-level hitobject, try to move to the next nested hitobject or otherwise move to the next top-level hitobject. - if (!moveNextNested()) - return moveNextTopLevel(); - - // Guaranteed by moveNextNested() to have a hitobject. - return true; - } - - /// - /// Attempts to move to the next top-level hitobject. - /// - /// Whether a new top-level hitobject was found. - private bool moveNextTopLevel() - { - currentNestedIndex = -1; - - hitObjectEnumerator.MoveNext(); - currentTopLevel = hitObjectEnumerator.Current; - - Current = currentTopLevel; - - return Current?.HitObject.StartTime < targetTime; - } - - /// - /// Attempts to move to the next nested hitobject in the current top-level hitobject. - /// - /// Whether a new nested hitobject was moved to. - private bool moveNextNested() - { - currentNestedIndex++; - if (currentNestedIndex >= currentTopLevel.NestedHitObjects.Count) - return false; - - Current = currentTopLevel.NestedHitObjects[currentNestedIndex]; - Debug.Assert(Current != null); - - return Current?.HitObject.StartTime < targetTime; - } - - public void Reset() - { - hitObjectEnumerator.Reset(); - currentTopLevel = null; - currentNestedIndex = -1; - Current = null; - } - - public DrawableHitObject Current { get; set; } - - object IEnumerator.Current => Current; - - public void Dispose() - { - hitObjectEnumerator?.Dispose(); + yield return obj.NestedHitObjects[i]; + } } } } From 5833a7ac913af77a8c8d801ffd5d117b9861930c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 17 Apr 2020 18:50:58 +0900 Subject: [PATCH 16/35] Fix presenting new ruleset and beatmap at once causing wedge display desync --- .../SongSelect/TestScenePlaySongSelect.cs | 62 +++++++++++++++++++ osu.Game/Screens/Select/SongSelect.cs | 6 +- 2 files changed, 66 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index 4405c75744..39e04ed39a 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -359,6 +359,68 @@ namespace osu.Game.Tests.Visual.SongSelect AddUntilStep("no selection", () => songSelect.Carousel.SelectedBeatmap == null); } + [Test] + public void TestPresentNewRulesetNewBeatmap() + { + createSongSelect(); + changeRuleset(2); + + addRulesetImportStep(2); + AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmap.RulesetID == 2); + + addRulesetImportStep(0); + addRulesetImportStep(0); + addRulesetImportStep(0); + + BeatmapInfo target = null; + + AddStep("select beatmap/ruleset externally", () => + { + target = manager.GetAllUsableBeatmapSets() + .Last(b => b.Beatmaps.Any(bi => bi.RulesetID == 0)).Beatmaps.Last(); + + Ruleset.Value = rulesets.AvailableRulesets.First(r => r.ID == 0); + Beatmap.Value = manager.GetWorkingBeatmap(target); + }); + + AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmap.Equals(target)); + + // this is an important check, to make sure updateComponentFromBeatmap() was actually run + AddUntilStep("selection shown on wedge", () => songSelect.CurrentBeatmapDetailsBeatmap.BeatmapInfo == target); + } + + [Test] + public void TestPresentNewBeatmapNewRuleset() + { + createSongSelect(); + changeRuleset(2); + + addRulesetImportStep(2); + AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmap.RulesetID == 2); + + addRulesetImportStep(0); + addRulesetImportStep(0); + addRulesetImportStep(0); + + BeatmapInfo target = null; + + AddStep("select beatmap/ruleset externally", () => + { + target = manager.GetAllUsableBeatmapSets() + .Last(b => b.Beatmaps.Any(bi => bi.RulesetID == 0)).Beatmaps.Last(); + + Beatmap.Value = manager.GetWorkingBeatmap(target); + Ruleset.Value = rulesets.AvailableRulesets.First(r => r.ID == 0); + }); + + AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmap.Equals(target)); + + AddUntilStep("has correct ruleset", () => Ruleset.Value.ID == 0); + + // this is an important check, to make sure updateComponentFromBeatmap() was actually run + AddUntilStep("selection shown on wedge", () => songSelect.CurrentBeatmapDetailsBeatmap.BeatmapInfo == target); + } + [Test] public void TestRulesetChangeResetsMods() { diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 5bc2e1aa56..9d3dc58a26 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -448,8 +448,10 @@ namespace osu.Game.Screens.Select { Mods.Value = Array.Empty(); - // required to return once in order to have the carousel in a good state. - // if the ruleset changed, the rest of the selection update will happen via updateSelectedRuleset. + // the ruleset transfer may cause a deselection of the current beatmap (due to incompatibility). + // this can happen via Carousel.FlushPendingFilterOperations(). + // to ensure a good state, re-transfer no-debounce values. + performUpdateSelected(); return; } From 61e3491e603daf0a497ec988318043001a4e068a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 18 Apr 2020 12:57:09 +0900 Subject: [PATCH 17/35] Fix hard crash in editor on legacy modes without encoder implementation --- osu.Game/Screens/Edit/EditorChangeHandler.cs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Edit/EditorChangeHandler.cs b/osu.Game/Screens/Edit/EditorChangeHandler.cs index 1553c2d2ef..ac889500b4 100644 --- a/osu.Game/Screens/Edit/EditorChangeHandler.cs +++ b/osu.Game/Screens/Edit/EditorChangeHandler.cs @@ -82,12 +82,19 @@ namespace osu.Game.Screens.Edit if (savedStates.Count > MAX_SAVED_STATES) savedStates.RemoveAt(0); - using (var stream = new MemoryStream()) + try { - using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true)) - new LegacyBeatmapEncoder(editorBeatmap).Encode(sw); + using (var stream = new MemoryStream()) + { + using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true)) + new LegacyBeatmapEncoder(editorBeatmap).Encode(sw); - savedStates.Add(stream.ToArray()); + savedStates.Add(stream.ToArray()); + } + } + catch (NotImplementedException) + { + // some rulesets don't have encoder implementations yet. } currentState = savedStates.Count - 1; From c00a386ff681a3ba4a037de6654bd1fd199d6770 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 18 Apr 2020 21:46:04 +0900 Subject: [PATCH 18/35] Remove exceptions instead --- .../Beatmaps/Formats/LegacyBeatmapEncoder.cs | 21 +------------------ osu.Game/Screens/Edit/EditorChangeHandler.cs | 15 ++++--------- 2 files changed, 5 insertions(+), 31 deletions(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index 12f2c58e35..8d9dfc318a 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -194,20 +194,7 @@ namespace osu.Game.Beatmaps.Formats handleOsuHitObject(writer, h); break; - case 1: - foreach (var h in beatmap.HitObjects) - handleTaikoHitObject(writer, h); - break; - - case 2: - foreach (var h in beatmap.HitObjects) - handleCatchHitObject(writer, h); - break; - - case 3: - foreach (var h in beatmap.HitObjects) - handleManiaHitObject(writer, h); - break; + // TODO: implement other legacy rulesets } } @@ -328,12 +315,6 @@ namespace osu.Game.Beatmaps.Formats } } - private void handleTaikoHitObject(TextWriter writer, HitObject hitObject) => throw new NotImplementedException(); - - private void handleCatchHitObject(TextWriter writer, HitObject hitObject) => throw new NotImplementedException(); - - private void handleManiaHitObject(TextWriter writer, HitObject hitObject) => throw new NotImplementedException(); - private string getSampleBank(IList samples, bool banksOnly = false, bool zeroBanks = false) { LegacySampleBank normalBank = toLegacySampleBank(samples.SingleOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL)?.Bank); diff --git a/osu.Game/Screens/Edit/EditorChangeHandler.cs b/osu.Game/Screens/Edit/EditorChangeHandler.cs index ac889500b4..1553c2d2ef 100644 --- a/osu.Game/Screens/Edit/EditorChangeHandler.cs +++ b/osu.Game/Screens/Edit/EditorChangeHandler.cs @@ -82,19 +82,12 @@ namespace osu.Game.Screens.Edit if (savedStates.Count > MAX_SAVED_STATES) savedStates.RemoveAt(0); - try + using (var stream = new MemoryStream()) { - using (var stream = new MemoryStream()) - { - using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true)) - new LegacyBeatmapEncoder(editorBeatmap).Encode(sw); + using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true)) + new LegacyBeatmapEncoder(editorBeatmap).Encode(sw); - savedStates.Add(stream.ToArray()); - } - } - catch (NotImplementedException) - { - // some rulesets don't have encoder implementations yet. + savedStates.Add(stream.ToArray()); } currentState = savedStates.Count - 1; From 6b16908c05bafd95e069b986d5efd6d73631875f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 18 Apr 2020 21:51:37 +0900 Subject: [PATCH 19/35] Move todo to appease dotnet-format --- osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index 8d9dfc318a..fe63eec3f9 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -187,14 +187,13 @@ namespace osu.Game.Beatmaps.Formats writer.WriteLine("[HitObjects]"); + // TODO: implement other legacy rulesets switch (beatmap.BeatmapInfo.RulesetID) { case 0: foreach (var h in beatmap.HitObjects) handleOsuHitObject(writer, h); break; - - // TODO: implement other legacy rulesets } } From 1dd471dfcc1ae9465d17c3d21ea2577d7a2b46c0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 19 Apr 2020 15:12:36 +0900 Subject: [PATCH 20/35] Add /np (now playing) command support in chat --- .../Visual/Online/TestNowPlayingCommand.cs | 85 +++++++++++++++++++ osu.Game/Online/Chat/ChannelManager.cs | 6 +- osu.Game/Online/Chat/IChannelPostTarget.cs | 19 +++++ osu.Game/Online/Chat/NowPlayingCommand.cs | 55 ++++++++++++ osu.Game/Online/PollingComponent.cs | 4 +- 5 files changed, 166 insertions(+), 3 deletions(-) create mode 100644 osu.Game.Tests/Visual/Online/TestNowPlayingCommand.cs create mode 100644 osu.Game/Online/Chat/IChannelPostTarget.cs create mode 100644 osu.Game/Online/Chat/NowPlayingCommand.cs diff --git a/osu.Game.Tests/Visual/Online/TestNowPlayingCommand.cs b/osu.Game.Tests/Visual/Online/TestNowPlayingCommand.cs new file mode 100644 index 0000000000..60032ab118 --- /dev/null +++ b/osu.Game.Tests/Visual/Online/TestNowPlayingCommand.cs @@ -0,0 +1,85 @@ +// 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.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Online.Chat; +using osu.Game.Rulesets; +using osu.Game.Users; + +namespace osu.Game.Tests.Visual.Online +{ + [HeadlessTest] + public class TestNowPlayingCommand : OsuTestScene + { + [Cached(typeof(IChannelPostTarget))] + private PostTarget postTarget { get; set; } + + public TestNowPlayingCommand() + { + Add(postTarget = new PostTarget()); + } + + [Test] + public void TestGenericActivity() + { + AddStep("Set activity", () => API.Activity.Value = new UserActivity.InLobby()); + + AddStep("Run command", () => Add(new NowPlayingCommand())); + + AddAssert("Check correct response", () => postTarget.LastMessage.Contains("is listening")); + } + + [Test] + public void TestEditActivity() + { + AddStep("Set activity", () => API.Activity.Value = new UserActivity.Editing(new BeatmapInfo())); + + AddStep("Run command", () => Add(new NowPlayingCommand())); + + AddAssert("Check correct response", () => postTarget.LastMessage.Contains("is editing")); + } + + [Test] + public void TestPlayActivity() + { + AddStep("Set activity", () => API.Activity.Value = new UserActivity.SoloGame(new BeatmapInfo(), new RulesetInfo())); + + AddStep("Run command", () => Add(new NowPlayingCommand())); + + AddAssert("Check correct response", () => postTarget.LastMessage.Contains("is playing")); + } + + [TestCase(true)] + [TestCase(false)] + public void TestLinkPresence(bool hasOnlineId) + { + AddStep("Set activity", () => API.Activity.Value = new UserActivity.InLobby()); + + AddStep("Set beatmap", () => Beatmap.Value = new DummyWorkingBeatmap(null, null) + { + BeatmapInfo = { OnlineBeatmapID = hasOnlineId ? 1234 : (int?)null } + }); + + AddStep("Run command", () => Add(new NowPlayingCommand())); + + if (hasOnlineId) + AddAssert("Check link presence", () => postTarget.LastMessage.Contains("https://osu.ppy.sh/b/1234")); + else + AddAssert("Check link not present", () => !postTarget.LastMessage.Contains("https://")); + } + + public class PostTarget : Component, IChannelPostTarget + { + public void PostMessage(string text, bool isAction = false, Channel target = null) + { + LastMessage = text; + } + + public string LastMessage { get; private set; } + } + } +} diff --git a/osu.Game/Online/Chat/ChannelManager.cs b/osu.Game/Online/Chat/ChannelManager.cs index 2c37216fd6..f53beefeb5 100644 --- a/osu.Game/Online/Chat/ChannelManager.cs +++ b/osu.Game/Online/Chat/ChannelManager.cs @@ -18,7 +18,7 @@ namespace osu.Game.Online.Chat /// /// Manages everything channel related /// - public class ChannelManager : PollingComponent + public class ChannelManager : PollingComponent, IChannelPostTarget { /// /// The channels the player joins on startup @@ -204,6 +204,10 @@ namespace osu.Game.Online.Chat switch (command) { + case "np": + AddInternal(new NowPlayingCommand()); + break; + case "me": if (string.IsNullOrWhiteSpace(content)) { diff --git a/osu.Game/Online/Chat/IChannelPostTarget.cs b/osu.Game/Online/Chat/IChannelPostTarget.cs new file mode 100644 index 0000000000..5697e918f0 --- /dev/null +++ b/osu.Game/Online/Chat/IChannelPostTarget.cs @@ -0,0 +1,19 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; + +namespace osu.Game.Online.Chat +{ + [Cached(typeof(IChannelPostTarget))] + public interface IChannelPostTarget + { + /// + /// Posts a message to the currently opened channel. + /// + /// The message text that is going to be posted + /// Is true if the message is an action, e.g.: user is currently eating + /// An optional target channel. If null, will be used. + void PostMessage(string text, bool isAction = false, Channel target = null); + } +} diff --git a/osu.Game/Online/Chat/NowPlayingCommand.cs b/osu.Game/Online/Chat/NowPlayingCommand.cs new file mode 100644 index 0000000000..c0b54812b6 --- /dev/null +++ b/osu.Game/Online/Chat/NowPlayingCommand.cs @@ -0,0 +1,55 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Game.Beatmaps; +using osu.Game.Online.API; +using osu.Game.Users; + +namespace osu.Game.Online.Chat +{ + public class NowPlayingCommand : Component + { + [Resolved] + private IChannelPostTarget channelManager { get; set; } + + [Resolved] + private IAPIProvider api { get; set; } + + [Resolved] + private Bindable currentBeatmap { get; set; } + + protected override void LoadComplete() + { + base.LoadComplete(); + + string verb; + BeatmapInfo beatmap; + + switch (api.Activity.Value) + { + case UserActivity.SoloGame solo: + verb = "playing"; + beatmap = solo.Beatmap; + break; + + case UserActivity.Editing edit: + verb = "editing"; + beatmap = edit.Beatmap; + break; + + default: + verb = "listening to"; + beatmap = currentBeatmap.Value.BeatmapInfo; + break; + } + + var beatmapString = beatmap.OnlineBeatmapID.HasValue ? $"[https://osu.ppy.sh/b/{beatmap.OnlineBeatmapID} {beatmap}]" : beatmap.ToString(); + + channelManager.PostMessage($"is {verb} {beatmapString}", true); + Expire(); + } + } +} diff --git a/osu.Game/Online/PollingComponent.cs b/osu.Game/Online/PollingComponent.cs index acbb2c39f4..228f147835 100644 --- a/osu.Game/Online/PollingComponent.cs +++ b/osu.Game/Online/PollingComponent.cs @@ -3,7 +3,7 @@ using System; using System.Threading.Tasks; -using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Threading; namespace osu.Game.Online @@ -11,7 +11,7 @@ namespace osu.Game.Online /// /// A component which requires a constant polling process. /// - public abstract class PollingComponent : Component + public abstract class PollingComponent : CompositeDrawable // switch away from Component because InternalChildren are used in usages. { private double? lastTimePolled; From e4d4040afb90a990eb369ced822d65f07acd1e25 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 19 Apr 2020 16:57:47 +0900 Subject: [PATCH 21/35] Rename test to match other classes --- ...TestNowPlayingCommand.cs => TestSceneNowPlayingCommand.cs} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename osu.Game.Tests/Visual/Online/{TestNowPlayingCommand.cs => TestSceneNowPlayingCommand.cs} (96%) diff --git a/osu.Game.Tests/Visual/Online/TestNowPlayingCommand.cs b/osu.Game.Tests/Visual/Online/TestSceneNowPlayingCommand.cs similarity index 96% rename from osu.Game.Tests/Visual/Online/TestNowPlayingCommand.cs rename to osu.Game.Tests/Visual/Online/TestSceneNowPlayingCommand.cs index 60032ab118..103308d34d 100644 --- a/osu.Game.Tests/Visual/Online/TestNowPlayingCommand.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneNowPlayingCommand.cs @@ -13,12 +13,12 @@ using osu.Game.Users; namespace osu.Game.Tests.Visual.Online { [HeadlessTest] - public class TestNowPlayingCommand : OsuTestScene + public class TestSceneNowPlayingCommand : OsuTestScene { [Cached(typeof(IChannelPostTarget))] private PostTarget postTarget { get; set; } - public TestNowPlayingCommand() + public TestSceneNowPlayingCommand() { Add(postTarget = new PostTarget()); } From 07b8ef83c95c209a9953013b3cadc01bc529b7aa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 19 Apr 2020 22:15:07 +0900 Subject: [PATCH 22/35] Add /np to help line --- osu.Game/Online/Chat/ChannelManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/Chat/ChannelManager.cs b/osu.Game/Online/Chat/ChannelManager.cs index f53beefeb5..822f628dd2 100644 --- a/osu.Game/Online/Chat/ChannelManager.cs +++ b/osu.Game/Online/Chat/ChannelManager.cs @@ -238,7 +238,7 @@ namespace osu.Game.Online.Chat break; case "help": - target.AddNewMessages(new InfoMessage("Supported commands: /help, /me [action], /join [channel]")); + target.AddNewMessages(new InfoMessage("Supported commands: /help, /me [action], /join [channel], /np")); break; default: From f3fee734417154e36ef9594a67e3060983455d91 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 20 Apr 2020 09:35:00 +0900 Subject: [PATCH 23/35] Fix DatabasedKeyBindingContainer not using defaults for non-databased ruleset --- .../Visual/Gameplay/TestSceneKeyBindings.cs | 99 +++++++++++++++++++ .../Bindings/DatabasedKeyBindingContainer.cs | 10 +- 2 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Tests/Visual/Gameplay/TestSceneKeyBindings.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneKeyBindings.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneKeyBindings.cs new file mode 100644 index 0000000000..45d9819c0e --- /dev/null +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneKeyBindings.cs @@ -0,0 +1,99 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using NUnit.Framework; +using osu.Framework.Graphics.Containers; +using osu.Framework.Input.Bindings; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Input.Bindings; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Difficulty; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.UI; +using osuTK.Input; + +namespace osu.Game.Tests.Visual.Gameplay +{ + [HeadlessTest] + public class TestSceneKeyBindings : OsuManualInputManagerTestScene + { + private readonly ActionReceiver receiver; + + public TestSceneKeyBindings() + { + Add(new TestKeyBindingContainer + { + Child = receiver = new ActionReceiver() + }); + } + + [Test] + public void TestDefaultsWhenNotDatabased() + { + AddStep("fire key", () => + { + InputManager.PressKey(Key.A); + InputManager.ReleaseKey(Key.A); + }); + + AddAssert("received key", () => receiver.ReceivedAction); + } + + private class TestRuleset : Ruleset + { + public override IEnumerable GetModsFor(ModType type) => + throw new System.NotImplementedException(); + + public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => + throw new System.NotImplementedException(); + + public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => + throw new System.NotImplementedException(); + + public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => + throw new System.NotImplementedException(); + + public override IEnumerable GetDefaultKeyBindings(int variant = 0) + { + return new[] + { + new KeyBinding(InputKey.A, TestAction.Down), + }; + } + + public override string Description => "test"; + public override string ShortName => "test"; + } + + private enum TestAction + { + Down, + } + + private class TestKeyBindingContainer : DatabasedKeyBindingContainer + { + public TestKeyBindingContainer() + : base(new TestRuleset().RulesetInfo, 0) + { + } + } + + private class ActionReceiver : CompositeDrawable, IKeyBindingHandler + { + public bool ReceivedAction; + + public bool OnPressed(TestAction action) + { + ReceivedAction = action == TestAction.Down; + return true; + } + + public void OnReleased(TestAction action) + { + throw new System.NotImplementedException(); + } + } + } +} diff --git a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs index e83d899469..94edc33099 100644 --- a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs +++ b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs @@ -62,6 +62,14 @@ namespace osu.Game.Input.Bindings store.KeyBindingChanged -= ReloadMappings; } - protected override void ReloadMappings() => KeyBindings = store.Query(ruleset?.ID, variant).ToList(); + protected override void ReloadMappings() + { + if (ruleset != null && !ruleset.ID.HasValue) + // if the provided ruleset is not stored to the database, we have no way to retrieve custom bindings. + // fallback to defaults instead. + KeyBindings = DefaultKeyBindings; + else + KeyBindings = store.Query(ruleset?.ID, variant).ToList(); + } } } From 2444dd42d0a931a9b6288813956f435b1bc5d4f2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 20 Apr 2020 09:57:46 +0900 Subject: [PATCH 24/35] Remove not-implemented-exception --- osu.Game.Tests/Visual/Gameplay/TestSceneKeyBindings.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneKeyBindings.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneKeyBindings.cs index 45d9819c0e..db65e91d17 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneKeyBindings.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneKeyBindings.cs @@ -92,7 +92,6 @@ namespace osu.Game.Tests.Visual.Gameplay public void OnReleased(TestAction action) { - throw new System.NotImplementedException(); } } } From 28318a0140a4e3854d81896e7a762a451f8d7637 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 20 Apr 2020 10:59:08 +0900 Subject: [PATCH 25/35] Add mention of notelock in xmldoc (potentially easier to find class) --- osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs b/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs index cd9838e7bf..176402c831 100644 --- a/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs @@ -11,7 +11,7 @@ using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Osu.UI { /// - /// Ensures that s are hit in-order. + /// Ensures that s are hit in-order. Affectionately known as "note lock". /// If a is hit out of order: /// /// The hit is blocked if it occurred earlier than the previous 's start time. From e1acfd26a6849be6cee96edc66ae58c17e9a1fac Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 20 Apr 2020 10:59:44 +0900 Subject: [PATCH 26/35] Simplify return logic --- osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs b/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs index 176402c831..1f027e9726 100644 --- a/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs @@ -51,10 +51,7 @@ namespace osu.Game.Rulesets.Osu.UI // 1. The last blocking hitobject has been judged. // 2. The current time is after the last hitobject's start time. // Hits at exactly the same time as the blocking hitobject are allowed for maps that contain simultaneous hitobjects (e.g. /b/372245). - if (blockingObject.Judged || time >= blockingObject.HitObject.StartTime) - return true; - - return false; + return blockingObject.Judged || time >= blockingObject.HitObject.StartTime; } /// From 8c85602ad013001281d4148b6109d606d6885c8c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 20 Apr 2020 11:00:42 +0900 Subject: [PATCH 27/35] Use foreach for conformity --- osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs b/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs index 1f027e9726..53dd1127d6 100644 --- a/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs @@ -93,12 +93,12 @@ namespace osu.Game.Rulesets.Osu.UI yield return obj; - for (int i = 0; i < obj.NestedHitObjects.Count; i++) + foreach (var nestedObj in obj.NestedHitObjects) { - if (obj.NestedHitObjects[i].HitObject.StartTime >= targetTime) + if (nestedObj.HitObject.StartTime >= targetTime) break; - yield return obj.NestedHitObjects[i]; + yield return nestedObj; } } } From 355e682e24557b3b87d791943c0e2523e3331de6 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 20 Apr 2020 13:23:27 +0900 Subject: [PATCH 28/35] Fix typo in exception --- osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs b/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs index 53dd1127d6..8e4f81347d 100644 --- a/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs @@ -65,7 +65,7 @@ namespace osu.Game.Rulesets.Osu.UI return; if (!IsHittable(hitObject, hitObject.HitObject.StartTime + hitObject.Result.TimeOffset)) - throw new InvalidOperationException($"A {hitObject} was hit before it become hittable!"); + throw new InvalidOperationException($"A {hitObject} was hit before it became hittable!"); foreach (var obj in enumerateHitObjectsUpTo(hitObject.HitObject.StartTime)) { From ee1ccb8bcb14c637e6912c9f278b10b7573348af Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 20 Apr 2020 14:03:55 +0900 Subject: [PATCH 29/35] Fix in a slightly different and hopefully more understandable way --- osu.Game/Screens/Select/SongSelect.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 9d3dc58a26..478c46fb36 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -426,7 +426,7 @@ namespace osu.Game.Screens.Select } /// - /// selection has been changed as the result of a user interaction. + /// Selection has been changed as the result of a user interaction. /// private void performUpdateSelected() { @@ -435,7 +435,7 @@ namespace osu.Game.Screens.Select selectionChangedDebounce?.Cancel(); - if (beatmap == null) + if (beatmapNoDebounce == null) run(); else selectionChangedDebounce = Scheduler.AddDelayed(run, 200); @@ -448,11 +448,11 @@ namespace osu.Game.Screens.Select { Mods.Value = Array.Empty(); - // the ruleset transfer may cause a deselection of the current beatmap (due to incompatibility). - // this can happen via Carousel.FlushPendingFilterOperations(). - // to ensure a good state, re-transfer no-debounce values. - performUpdateSelected(); - return; + // transferRulesetValue() may trigger a refilter. If the current selection does not match the new ruleset, we want to switch away from it. + // The default logic on WorkingBeatmap change is to switch to a matching ruleset (see workingBeatmapChanged()), but we don't want that here. + // We perform an early selection attempt and clear out the beatmap selection to avoid a second ruleset change (revert). + if (beatmap != null && !Carousel.SelectBeatmap(beatmap, false)) + beatmap = null; } // We may be arriving here due to another component changing the bindable Beatmap. @@ -716,7 +716,7 @@ namespace osu.Game.Screens.Select if (decoupledRuleset.Value?.Equals(Ruleset.Value) == true) return false; - Logger.Log($"decoupled ruleset transferred (\"{decoupledRuleset.Value}\" -> \"{Ruleset.Value}\""); + Logger.Log($"decoupled ruleset transferred (\"{decoupledRuleset.Value}\" -> \"{Ruleset.Value}\")"); rulesetNoDebounce = decoupledRuleset.Value = Ruleset.Value; // if we have a pending filter operation, we want to run it now. From 5464746d3d899a136bfa392789d351559d6f5752 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 20 Apr 2020 15:25:58 +0900 Subject: [PATCH 30/35] Switch to using CompositeDrawable --- .../Dashboard/Friends/FriendDisplay.cs | 23 ++++++++----------- osu.Game/Overlays/OverlayView.cs | 14 ++--------- 2 files changed, 12 insertions(+), 25 deletions(-) diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs b/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs index 9764f82199..7c4a0a4164 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs @@ -34,16 +34,17 @@ namespace osu.Game.Overlays.Dashboard.Friends private Drawable currentContent; - private readonly FriendOnlineStreamControl onlineStreamControl; - private readonly Box background; - private readonly Box controlBackground; - private readonly UserListToolbar userListToolbar; - private readonly Container itemsPlaceholder; - private readonly LoadingLayer loading; + private FriendOnlineStreamControl onlineStreamControl; + private Box background; + private Box controlBackground; + private UserListToolbar userListToolbar; + private Container itemsPlaceholder; + private LoadingLayer loading; - public FriendDisplay() + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) { - AddRange(new Drawable[] + InternalChildren = new Drawable[] { new Container { @@ -121,12 +122,8 @@ namespace osu.Game.Overlays.Dashboard.Friends } } } - }); - } + }; - [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider) - { background.Colour = colourProvider.Background4; controlBackground.Colour = colourProvider.Background5; } diff --git a/osu.Game/Overlays/OverlayView.cs b/osu.Game/Overlays/OverlayView.cs index e3a07fc2de..724658f22f 100644 --- a/osu.Game/Overlays/OverlayView.cs +++ b/osu.Game/Overlays/OverlayView.cs @@ -9,29 +9,19 @@ using osu.Game.Online.API; namespace osu.Game.Overlays { /// - /// Drawable which used to represent online content in . + /// A subview containing online content, to be displayed inside a . /// /// Response type - public abstract class OverlayView : Container, IOnlineComponent + public abstract class OverlayView : CompositeDrawable, IOnlineComponent where T : class { [Resolved] protected IAPIProvider API { get; private set; } - protected override Container Content => content; - - private readonly FillFlowContainer content; - protected OverlayView() { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; - - AddInternal(content = new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - }); } protected override void LoadComplete() From 99e13b8ed9c124484b4410eb9b03da4f2be03997 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 20 Apr 2020 15:32:50 +0900 Subject: [PATCH 31/35] Add better xml documentation and extract fetch method --- osu.Game/Overlays/OverlayView.cs | 33 ++++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/osu.Game/Overlays/OverlayView.cs b/osu.Game/Overlays/OverlayView.cs index 724658f22f..3e2c54c726 100644 --- a/osu.Game/Overlays/OverlayView.cs +++ b/osu.Game/Overlays/OverlayView.cs @@ -11,13 +11,18 @@ namespace osu.Game.Overlays /// /// A subview containing online content, to be displayed inside a . /// - /// Response type + /// + /// Automatically performs a data fetch on load. + /// + /// The type of the API response. public abstract class OverlayView : CompositeDrawable, IOnlineComponent where T : class { [Resolved] protected IAPIProvider API { get; private set; } + private APIRequest request; + protected OverlayView() { RelativeSizeAxes = Axes.X; @@ -30,20 +35,36 @@ namespace osu.Game.Overlays API.Register(this); } - private APIRequest request; - + /// + /// Create the API request for fetching data. + /// protected abstract APIRequest CreateRequest(); + /// + /// Fired when results arrive from the main API request. + /// + /// protected abstract void OnSuccess(T response); + /// + /// Force a re-request for data from the API. + /// + protected void PerformFetch() + { + request?.Cancel(); + + request = CreateRequest(); + request.Success += response => Schedule(() => OnSuccess(response)); + + API.Queue(request); + } + public virtual void APIStateChanged(IAPIProvider api, APIState state) { switch (state) { case APIState.Online: - request = CreateRequest(); - request.Success += response => Schedule(() => OnSuccess(response)); - api.Queue(request); + PerformFetch(); break; } } From 6b89c638c9aa2db2744a4d65bad92774e1e49a3a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 20 Apr 2020 15:34:48 +0900 Subject: [PATCH 32/35] Move load to bdl --- osu.Game/Overlays/DashboardOverlay.cs | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/osu.Game/Overlays/DashboardOverlay.cs b/osu.Game/Overlays/DashboardOverlay.cs index 86c0f3bd83..a72c3f4fa5 100644 --- a/osu.Game/Overlays/DashboardOverlay.cs +++ b/osu.Game/Overlays/DashboardOverlay.cs @@ -19,14 +19,19 @@ namespace osu.Game.Overlays { private CancellationTokenSource cancellationToken; - private readonly Box background; - private readonly Container content; - private readonly DashboardOverlayHeader header; - private readonly LoadingLayer loading; - private readonly OverlayScrollContainer scrollFlow; + private Box background; + private Container content; + private DashboardOverlayHeader header; + private LoadingLayer loading; + private OverlayScrollContainer scrollFlow; public DashboardOverlay() : base(OverlayColourScheme.Purple) + { + } + + [BackgroundDependencyLoader] + private void load() { Children = new Drawable[] { @@ -61,17 +66,14 @@ namespace osu.Game.Overlays }, loading = new LoadingLayer(content), }; - } - [BackgroundDependencyLoader] - private void load() - { background.Colour = ColourProvider.Background5; } protected override void LoadComplete() { base.LoadComplete(); + header.Current.BindValueChanged(onTabChanged); } From 801f02a3d7bb28b842a7dfd7e1d39b404b7d9690 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 20 Apr 2020 17:48:02 +0900 Subject: [PATCH 33/35] Fix inline executions of APIRequest.Perform not getting result populated early enough --- osu.Game/Online/API/APIRequest.cs | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/osu.Game/Online/API/APIRequest.cs b/osu.Game/Online/API/APIRequest.cs index 47600e4f68..0bba04cac3 100644 --- a/osu.Game/Online/API/APIRequest.cs +++ b/osu.Game/Online/API/APIRequest.cs @@ -18,24 +18,32 @@ namespace osu.Game.Online.API public T Result { get; private set; } - protected APIRequest() - { - base.Success += () => TriggerSuccess(((OsuJsonWebRequest)WebRequest)?.ResponseObject); - } - /// /// Invoked on successful completion of an API request. /// This will be scheduled to the API's internal scheduler (run on update thread automatically). /// public new event APISuccessHandler Success; + protected override void PostProcess() + { + base.PostProcess(); + Result = ((OsuJsonWebRequest)WebRequest)?.ResponseObject; + } + internal void TriggerSuccess(T result) { if (Result != null) throw new InvalidOperationException("Attempted to trigger success more than once"); Result = result; - Success?.Invoke(result); + + TriggerSuccess(); + } + + internal override void TriggerSuccess() + { + base.TriggerSuccess(); + Success?.Invoke(Result); } } @@ -99,6 +107,8 @@ namespace osu.Game.Online.API if (checkAndScheduleFailure()) return; + PostProcess(); + API.Schedule(delegate { if (cancelled) return; @@ -107,7 +117,14 @@ namespace osu.Game.Online.API }); } - internal void TriggerSuccess() + /// + /// Perform any post-processing actions after a successful request. + /// + protected virtual void PostProcess() + { + } + + internal virtual void TriggerSuccess() { Success?.Invoke(); } From 3f3ff5fdb1b145f7ac885a691f7f0fbe93d1b99c Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 20 Apr 2020 09:24:40 +0000 Subject: [PATCH 34/35] Bump Humanizer from 2.7.9 to 2.8.2 Bumps [Humanizer](https://github.com/Humanizr/Humanizer) from 2.7.9 to 2.8.2. - [Release notes](https://github.com/Humanizr/Humanizer/releases) - [Changelog](https://github.com/Humanizr/Humanizer/blob/master/release_notes.md) - [Commits](https://github.com/Humanizr/Humanizer/compare/v2.7.9...v2.8.2) Signed-off-by: dependabot-preview[bot] --- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 5facb04117..35ee0864e1 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -19,7 +19,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index dda1ee5c42..0200fca9a3 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -76,7 +76,7 @@ - + From b3d4b4a3f42696861d8c17cd395423e4a2656f74 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 20 Apr 2020 18:25:39 +0900 Subject: [PATCH 35/35] Add back missing fill flow --- .../Dashboard/Friends/FriendDisplay.cs | 124 +++++++++--------- 1 file changed, 65 insertions(+), 59 deletions(-) diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs b/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs index 7c4a0a4164..79fda99c73 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs @@ -44,78 +44,84 @@ namespace osu.Game.Overlays.Dashboard.Friends [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider) { - InternalChildren = new Drawable[] + InternalChild = new FillFlowContainer { - new Container + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Children = new Drawable[] { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Children = new Drawable[] + new Container { - controlBackground = new Box + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] { - RelativeSizeAxes = Axes.Both - }, - new Container - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Padding = new MarginPadding + controlBackground = new Box { - Top = 20, - Horizontal = 45 + RelativeSizeAxes = Axes.Both }, - Child = onlineStreamControl = new FriendOnlineStreamControl(), - } - } - }, - new Container - { - Name = "User List", - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Children = new Drawable[] - { - background = new Box - { - RelativeSizeAxes = Axes.Both - }, - new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Margin = new MarginPadding { Bottom = 20 }, - Children = new Drawable[] + new Container { - new Container + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Padding = new MarginPadding - { - Horizontal = 40, - Vertical = 20 - }, - Child = userListToolbar = new UserListToolbar - { - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - } + Top = 20, + Horizontal = 45 }, - new Container + Child = onlineStreamControl = new FriendOnlineStreamControl(), + } + } + }, + new Container + { + Name = "User List", + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + background = new Box + { + RelativeSizeAxes = Axes.Both + }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Margin = new MarginPadding { Bottom = 20 }, + Children = new Drawable[] { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Children = new Drawable[] + new Container { - itemsPlaceholder = new Container + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Horizontal = 50 } + Horizontal = 40, + Vertical = 20 }, - loading = new LoadingLayer(itemsPlaceholder) + Child = userListToolbar = new UserListToolbar + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + } + }, + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + itemsPlaceholder = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Horizontal = 50 } + }, + loading = new LoadingLayer(itemsPlaceholder) + } } } }