diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index a97c0b4a72..6f583d7983 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.Linq; using Humanizer; -using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -18,8 +17,6 @@ using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Objects; -using osu.Game.Screens.Edit.Compose; -using osuTK; using osuTK.Input; namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components @@ -36,11 +33,10 @@ public class PathControlPointVisualiser : CompositeDrawable, IKeyBindingHandler< private InputManager inputManager; - [Resolved(CanBeNull = true)] - private IPlacementHandler placementHandler { get; set; } - private IBindableList controlPoints; + public Action> RemoveControlPointsRequested; + public PathControlPointVisualiser(Slider slider, bool allowSelection) { this.slider = slider; @@ -133,29 +129,7 @@ private bool deleteSelected() if (toRemove.Count == 0) return false; - foreach (var c in toRemove) - { - // The first control point in the slider must have a type, so take it from the previous "first" one - // Todo: Should be handled within SliderPath itself - if (c == slider.Path.ControlPoints[0] && slider.Path.ControlPoints.Count > 1 && slider.Path.ControlPoints[1].Type.Value == null) - slider.Path.ControlPoints[1].Type.Value = slider.Path.ControlPoints[0].Type.Value; - - slider.Path.ControlPoints.Remove(c); - } - - // If there are 0 or 1 remaining control points, the slider is in a degenerate (single point) form and should be deleted - if (slider.Path.ControlPoints.Count <= 1) - { - placementHandler?.Delete(slider); - return true; - } - - // The path will have a non-zero offset if the head is removed, but sliders don't support this behaviour since the head is positioned at the slider's position - // So the slider needs to be offset by this amount instead, and all control points offset backwards such that the path is re-positioned at (0, 0) - Vector2 first = slider.Path.ControlPoints[0].Position.Value; - foreach (var c in slider.Path.ControlPoints) - c.Position.Value -= first; - slider.Position += first; + RemoveControlPointsRequested?.Invoke(toRemove); // Since pieces are re-used, they will not point to the deleted control points while remaining selected foreach (var piece in Pieces) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index 68873093a6..3165c441fb 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.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.Collections.Generic; using System.Diagnostics; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -14,6 +15,7 @@ using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; +using osu.Game.Screens.Edit.Compose; using osuTK; using osuTK.Input; @@ -29,6 +31,9 @@ public class SliderSelectionBlueprint : OsuSelectionBlueprint [Resolved(CanBeNull = true)] private HitObjectComposer composer { get; set; } + [Resolved(CanBeNull = true)] + private IPlacementHandler placementHandler { get; set; } + public SliderSelectionBlueprint(DrawableSlider slider) : base(slider) { @@ -40,6 +45,9 @@ public SliderSelectionBlueprint(DrawableSlider slider) HeadBlueprint = CreateCircleSelectionBlueprint(slider, SliderPosition.Start), TailBlueprint = CreateCircleSelectionBlueprint(slider, SliderPosition.End), ControlPointVisualiser = new PathControlPointVisualiser(sliderObject, true) + { + RemoveControlPointsRequested = removeControlPoints + } }; } @@ -97,6 +105,8 @@ protected override bool OnDragEnd(DragEndEvent e) return true; } + private BindableList controlPoints => HitObject.Path.ControlPoints; + private int addControlPoint(Vector2 position) { position -= HitObject.Position; @@ -104,9 +114,9 @@ private int addControlPoint(Vector2 position) int insertionIndex = 0; float minDistance = float.MaxValue; - for (int i = 0; i < HitObject.Path.ControlPoints.Count - 1; i++) + for (int i = 0; i < controlPoints.Count - 1; i++) { - float dist = new Line(HitObject.Path.ControlPoints[i].Position.Value, HitObject.Path.ControlPoints[i + 1].Position.Value).DistanceToPoint(position); + float dist = new Line(controlPoints[i].Position.Value, controlPoints[i + 1].Position.Value).DistanceToPoint(position); if (dist < minDistance) { @@ -116,11 +126,42 @@ private int addControlPoint(Vector2 position) } // Move the control points from the insertion index onwards to make room for the insertion - HitObject.Path.ControlPoints.Insert(insertionIndex, new PathControlPoint { Position = { Value = position } }); + controlPoints.Insert(insertionIndex, new PathControlPoint { Position = { Value = position } }); return insertionIndex; } + private void removeControlPoints(List toRemove) + { + // Ensure that there are any points to be deleted + if (toRemove.Count == 0) + return; + + foreach (var c in toRemove) + { + // The first control point in the slider must have a type, so take it from the previous "first" one + // Todo: Should be handled within SliderPath itself + if (c == controlPoints[0] && controlPoints.Count > 1 && controlPoints[1].Type.Value == null) + controlPoints[1].Type.Value = controlPoints[0].Type.Value; + + controlPoints.Remove(c); + } + + // If there are 0 or 1 remaining control points, the slider is in a degenerate (single point) form and should be deleted + if (controlPoints.Count <= 1) + { + placementHandler?.Delete(HitObject); + return; + } + + // The path will have a non-zero offset if the head is removed, but sliders don't support this behaviour since the head is positioned at the slider's position + // So the slider needs to be offset by this amount instead, and all control points offset backwards such that the path is re-positioned at (0, 0) + Vector2 first = controlPoints[0].Position.Value; + foreach (var c in controlPoints) + c.Position.Value -= first; + HitObject.Position += first; + } + private void updatePath() { HitObject.Path.ExpectedDistance.Value = composer?.GetSnappedDistanceFromDistance(HitObject.StartTime, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance; diff --git a/osu.Game.Tests/Visual/Online/TestSceneRankingsHeader.cs b/osu.Game.Tests/Visual/Online/TestSceneRankingsHeader.cs index c0da605cdb..e708934bc3 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneRankingsHeader.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneRankingsHeader.cs @@ -68,9 +68,7 @@ public TestSceneRankingsHeader() }; AddStep("Set country", () => countryBindable.Value = country); - AddAssert("Check scope is Performance", () => scope.Value == RankingsScope.Performance); AddStep("Set scope to Score", () => scope.Value = RankingsScope.Score); - AddAssert("Check country is Null", () => countryBindable.Value == null); AddStep("Set country with no flag", () => countryBindable.Value = unknownCountry); } } diff --git a/osu.Game.Tests/Visual/Online/TestSceneRankingsHeaderTitle.cs b/osu.Game.Tests/Visual/Online/TestSceneRankingsHeaderTitle.cs index 849ca2defc..0edf104da0 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneRankingsHeaderTitle.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneRankingsHeaderTitle.cs @@ -43,11 +43,6 @@ public TestSceneRankingsHeaderTitle() FullName = "United States" }; - AddStep("Set country", () => countryBindable.Value = countryA); - AddAssert("Check scope is Performance", () => scope.Value == RankingsScope.Performance); - AddStep("Set scope to Score", () => scope.Value = RankingsScope.Score); - AddAssert("Check country is Null", () => countryBindable.Value == null); - AddStep("Set country 1", () => countryBindable.Value = countryA); AddStep("Set country 2", () => countryBindable.Value = countryB); AddStep("Set null country", () => countryBindable.Value = null); diff --git a/osu.Game.Tests/Visual/Online/TestSceneRankingsOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneRankingsOverlay.cs new file mode 100644 index 0000000000..568e36df4c --- /dev/null +++ b/osu.Game.Tests/Visual/Online/TestSceneRankingsOverlay.cs @@ -0,0 +1,86 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using osu.Game.Overlays.Rankings.Tables; +using osu.Framework.Allocation; +using osu.Game.Overlays; +using NUnit.Framework; +using osu.Game.Users; +using osu.Framework.Bindables; +using osu.Game.Overlays.Rankings; + +namespace osu.Game.Tests.Visual.Online +{ + public class TestSceneRankingsOverlay : OsuTestScene + { + protected override bool UseOnlineAPI => true; + + public override IReadOnlyList RequiredTypes => new[] + { + typeof(PerformanceTable), + typeof(ScoresTable), + typeof(CountriesTable), + typeof(TableRowBackground), + typeof(UserBasedTable), + typeof(RankingsTable<>), + typeof(RankingsOverlay) + }; + + [Cached] + private RankingsOverlay rankingsOverlay; + + private readonly Bindable countryBindable = new Bindable(); + private readonly Bindable scope = new Bindable(); + + public TestSceneRankingsOverlay() + { + Add(rankingsOverlay = new TestRankingsOverlay + { + Country = { BindTarget = countryBindable }, + Scope = { BindTarget = scope }, + }); + } + + [Test] + public void TestShow() + { + AddStep("Show", rankingsOverlay.Show); + } + + [Test] + public void TestFlagScopeDependency() + { + AddStep("Set scope to Score", () => scope.Value = RankingsScope.Score); + AddAssert("Check country is Null", () => countryBindable.Value == null); + AddStep("Set country", () => countryBindable.Value = us_country); + AddAssert("Check scope is Performance", () => scope.Value == RankingsScope.Performance); + } + + [Test] + public void TestShowCountry() + { + AddStep("Show US", () => rankingsOverlay.ShowCountry(us_country)); + } + + [Test] + public void TestHide() + { + AddStep("Hide", rankingsOverlay.Hide); + } + + private static readonly Country us_country = new Country + { + FlagName = "US", + FullName = "United States" + }; + + private class TestRankingsOverlay : RankingsOverlay + { + public new Bindable Country => base.Country; + + public new Bindable Scope => base.Scope; + } + } +} diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSettings.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSettings.cs new file mode 100644 index 0000000000..fc44c5f595 --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSettings.cs @@ -0,0 +1,107 @@ +// 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.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Configuration; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays.Mods; +using osu.Game.Rulesets.Mods; + +namespace osu.Game.Tests.Visual.UserInterface +{ + public class TestSceneModSettings : OsuTestScene + { + private TestModSelectOverlay modSelect; + + [BackgroundDependencyLoader] + private void load() + { + Add(modSelect = new TestModSelectOverlay + { + RelativeSizeAxes = Axes.X, + Origin = Anchor.BottomCentre, + Anchor = Anchor.BottomCentre, + }); + + var testMod = new TestModCustomisable1(); + + AddStep("open", modSelect.Show); + AddAssert("button disabled", () => !modSelect.CustomiseButton.Enabled.Value); + AddUntilStep("wait for button load", () => modSelect.ButtonsLoaded); + AddStep("select mod", () => modSelect.SelectMod(testMod)); + AddAssert("button enabled", () => modSelect.CustomiseButton.Enabled.Value); + AddStep("open Customisation", () => modSelect.CustomiseButton.Click()); + AddStep("deselect mod", () => modSelect.SelectMod(testMod)); + AddAssert("controls hidden", () => modSelect.ModSettingsContainer.Alpha == 0); + } + + private class TestModSelectOverlay : ModSelectOverlay + { + public new Container ModSettingsContainer => base.ModSettingsContainer; + public new TriangleButton CustomiseButton => base.CustomiseButton; + + public bool ButtonsLoaded => ModSectionsContainer.Children.All(c => c.ModIconsLoaded); + + public void SelectMod(Mod mod) => + ModSectionsContainer.Children.Single(s => s.ModType == mod.Type) + .ButtonsContainer.OfType().Single(b => b.Mods.Any(m => m.GetType() == mod.GetType())).SelectNext(1); + + protected override void LoadComplete() + { + base.LoadComplete(); + + foreach (var section in ModSectionsContainer) + { + if (section.ModType == ModType.Conversion) + { + section.Mods = new Mod[] + { + new TestModCustomisable1(), + new TestModCustomisable2() + }; + } + else + section.Mods = Array.Empty(); + } + } + } + + private class TestModCustomisable1 : TestModCustomisable + { + public override string Name => "Customisable Mod 1"; + + public override string Acronym => "CM1"; + } + + private class TestModCustomisable2 : TestModCustomisable + { + public override string Name => "Customisable Mod 2"; + + public override string Acronym => "CM2"; + } + + private abstract class TestModCustomisable : Mod, IApplicableMod + { + public override double ScoreMultiplier => 1.0; + + public override ModType Type => ModType.Conversion; + + [SettingSource("Sample float", "Change something for a mod")] + public BindableFloat SliderBindable { get; } = new BindableFloat + { + MinValue = 0, + MaxValue = 10, + Default = 5, + Value = 7 + }; + + [SettingSource("Sample bool", "Clicking this changes a setting")] + public BindableBool TickBindable { get; } = new BindableBool(); + } + } +} diff --git a/osu.Game/Online/API/Requests/GetUserRankingsRequest.cs b/osu.Game/Online/API/Requests/GetUserRankingsRequest.cs index 9c3eba9fdc..143d21e40d 100644 --- a/osu.Game/Online/API/Requests/GetUserRankingsRequest.cs +++ b/osu.Game/Online/API/Requests/GetUserRankingsRequest.cs @@ -8,13 +8,14 @@ namespace osu.Game.Online.API.Requests { public class GetUserRankingsRequest : GetRankingsRequest { + public readonly UserRankingsType Type; + private readonly string country; - private readonly UserRankingsType type; public GetUserRankingsRequest(RulesetInfo ruleset, UserRankingsType type = UserRankingsType.Performance, int page = 1, string country = null) : base(ruleset, page) { - this.type = type; + Type = type; this.country = country; } @@ -28,7 +29,7 @@ protected override WebRequest CreateWebRequest() return req; } - protected override string TargetPostfix() => type.ToString().ToLowerInvariant(); + protected override string TargetPostfix() => Type.ToString().ToLowerInvariant(); } public enum UserRankingsType diff --git a/osu.Game/Overlays/Mods/ModControlSection.cs b/osu.Game/Overlays/Mods/ModControlSection.cs new file mode 100644 index 0000000000..f4b588ddb3 --- /dev/null +++ b/osu.Game/Overlays/Mods/ModControlSection.cs @@ -0,0 +1,56 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Configuration; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Rulesets.Mods; +using osuTK; + +namespace osu.Game.Overlays.Mods +{ + public class ModControlSection : Container + { + protected FillFlowContainer FlowContent; + protected override Container Content => FlowContent; + + public readonly Mod Mod; + + public ModControlSection(Mod mod) + { + Mod = mod; + + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + FlowContent = new FillFlowContainer + { + Margin = new MarginPadding { Top = 30 }, + Spacing = new Vector2(0, 5), + Direction = FillDirection.Vertical, + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + }; + + AddRange(Mod.CreateSettingsControls()); + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + AddRangeInternal(new Drawable[] + { + new OsuSpriteText + { + Text = Mod.Name, + Font = OsuFont.GetFont(weight: FontWeight.Bold), + Colour = colours.Yellow, + }, + FlowContent + }); + } + } +} diff --git a/osu.Game/Overlays/Mods/ModSection.cs b/osu.Game/Overlays/Mods/ModSection.cs index 9c0a164ad6..c55d1d8f70 100644 --- a/osu.Game/Overlays/Mods/ModSection.cs +++ b/osu.Game/Overlays/Mods/ModSection.cs @@ -57,6 +57,15 @@ public IEnumerable Mods }).ToArray(); modsLoadCts?.Cancel(); + + if (modContainers.Length == 0) + { + ModIconsLoaded = true; + headerLabel.Hide(); + Hide(); + return; + } + ModIconsLoaded = false; LoadComponentsAsync(modContainers, c => @@ -67,17 +76,8 @@ public IEnumerable Mods buttons = modContainers.OfType().ToArray(); - if (value.Any()) - { - headerLabel.FadeIn(200); - this.FadeIn(200); - } - else - { - // transition here looks weird as mods instantly disappear. - headerLabel.Hide(); - Hide(); - } + headerLabel.FadeIn(200); + this.FadeIn(200); } } diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 9ff320841a..e8ea43e3f2 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -13,6 +13,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; +using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Containers; @@ -31,6 +32,7 @@ namespace osu.Game.Overlays.Mods public class ModSelectOverlay : WaveOverlayContainer { protected readonly TriangleButton DeselectAllButton; + protected readonly TriangleButton CustomiseButton; protected readonly TriangleButton CloseButton; protected readonly OsuSpriteText MultiplierLabel; @@ -42,6 +44,10 @@ public class ModSelectOverlay : WaveOverlayContainer protected readonly FillFlowContainer ModSectionsContainer; + protected readonly FillFlowContainer ModSettingsContent; + + protected readonly Container ModSettingsContainer; + protected readonly Bindable> SelectedMods = new Bindable>(Array.Empty()); protected readonly IBindable Ruleset = new Bindable(); @@ -226,6 +232,17 @@ public ModSelectOverlay() Right = 20 } }, + CustomiseButton = new TriangleButton + { + Width = 180, + Text = "Customisation", + Action = () => ModSettingsContainer.Alpha = ModSettingsContainer.Alpha == 1 ? 0 : 1, + Enabled = { Value = false }, + Margin = new MarginPadding + { + Right = 20 + } + }, CloseButton = new TriangleButton { Width = 180, @@ -271,6 +288,36 @@ public ModSelectOverlay() }, }, }, + ModSettingsContainer = new Container + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + Width = 0.25f, + Alpha = 0, + X = -100, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = new Color4(0, 0, 0, 192) + }, + new OsuScrollContainer + { + RelativeSizeAxes = Axes.Both, + Child = ModSettingsContent = new FillFlowContainer + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(0f, 10f), + Padding = new MarginPadding(20), + } + } + } + } }; } @@ -381,12 +428,14 @@ private void rulesetChanged(ValueChangedEvent e) refreshSelectedMods(); } - private void selectedModsChanged(ValueChangedEvent> e) + private void selectedModsChanged(ValueChangedEvent> mods) { foreach (var section in ModSectionsContainer.Children) - section.SelectTypes(e.NewValue.Select(m => m.GetType()).ToList()); + section.SelectTypes(mods.NewValue.Select(m => m.GetType()).ToList()); updateMods(); + + updateModSettings(mods); } private void updateMods() @@ -411,6 +460,25 @@ private void updateMods() UnrankedLabel.FadeTo(ranked ? 0 : 1, 200); } + private void updateModSettings(ValueChangedEvent> selectedMods) + { + foreach (var added in selectedMods.NewValue.Except(selectedMods.OldValue)) + { + var controls = added.CreateSettingsControls().ToList(); + if (controls.Count > 0) + ModSettingsContent.Add(new ModControlSection(added) { Children = controls }); + } + + foreach (var removed in selectedMods.OldValue.Except(selectedMods.NewValue)) + ModSettingsContent.RemoveAll(section => section.Mod == removed); + + bool hasSettings = ModSettingsContent.Children.Count > 0; + CustomiseButton.Enabled.Value = hasSettings; + + if (!hasSettings) + ModSettingsContainer.Hide(); + } + private void modButtonPressed(Mod selectedMod) { if (selectedMod != null) diff --git a/osu.Game/Overlays/Rankings/HeaderTitle.cs b/osu.Game/Overlays/Rankings/HeaderTitle.cs index a1a893fa6b..b08a2a3900 100644 --- a/osu.Game/Overlays/Rankings/HeaderTitle.cs +++ b/osu.Game/Overlays/Rankings/HeaderTitle.cs @@ -74,13 +74,7 @@ protected override void LoadComplete() base.LoadComplete(); } - private void onScopeChanged(ValueChangedEvent scope) - { - scopeText.Text = scope.NewValue.ToString(); - - if (scope.NewValue != RankingsScope.Performance) - Country.Value = null; - } + private void onScopeChanged(ValueChangedEvent scope) => scopeText.Text = scope.NewValue.ToString(); private void onCountryChanged(ValueChangedEvent country) { @@ -90,8 +84,6 @@ private void onCountryChanged(ValueChangedEvent country) return; } - Scope.Value = RankingsScope.Performance; - flag.Country = country.NewValue; flag.Show(); } diff --git a/osu.Game/Overlays/RankingsOverlay.cs b/osu.Game/Overlays/RankingsOverlay.cs new file mode 100644 index 0000000000..c8874ef891 --- /dev/null +++ b/osu.Game/Overlays/RankingsOverlay.cs @@ -0,0 +1,214 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; +using osu.Game.Overlays.Rankings; +using osu.Game.Users; +using osu.Game.Rulesets; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API; +using System.Threading; +using osu.Game.Online.API.Requests; +using osu.Game.Overlays.Rankings.Tables; + +namespace osu.Game.Overlays +{ + public class RankingsOverlay : FullscreenOverlay + { + protected readonly Bindable Country = new Bindable(); + protected readonly Bindable Scope = new Bindable(); + private readonly Bindable ruleset = new Bindable(); + + private readonly BasicScrollContainer scrollFlow; + private readonly Box background; + private readonly Container tableContainer; + private readonly DimmedLoadingLayer loading; + + private APIRequest lastRequest; + private CancellationTokenSource cancellationToken; + + [Resolved] + private IAPIProvider api { get; set; } + + public RankingsOverlay() + { + Children = new Drawable[] + { + background = new Box + { + RelativeSizeAxes = Axes.Both, + }, + scrollFlow = new BasicScrollContainer + { + RelativeSizeAxes = Axes.Both, + ScrollbarVisible = false, + Child = new FillFlowContainer + { + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new RankingsHeader + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Country = { BindTarget = Country }, + Scope = { BindTarget = Scope }, + Ruleset = { BindTarget = ruleset } + }, + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + tableContainer = new Container + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Margin = new MarginPadding { Vertical = 10 } + }, + loading = new DimmedLoadingLayer(), + } + } + } + } + } + }; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colour) + { + Waves.FirstWaveColour = colour.Green; + Waves.SecondWaveColour = colour.GreenLight; + Waves.ThirdWaveColour = colour.GreenDark; + Waves.FourthWaveColour = colour.GreenDarker; + + background.Colour = OsuColour.Gray(0.1f); + } + + protected override void LoadComplete() + { + Country.BindValueChanged(_ => + { + // if a country is requested, force performance scope. + if (Country.Value != null) + Scope.Value = RankingsScope.Performance; + + Scheduler.AddOnce(loadNewContent); + }, true); + + Scope.BindValueChanged(_ => + { + // country filtering is only valid for performance scope. + if (Scope.Value != RankingsScope.Performance) + Country.Value = null; + + Scheduler.AddOnce(loadNewContent); + }, true); + + ruleset.BindValueChanged(_ => Scheduler.AddOnce(loadNewContent), true); + + base.LoadComplete(); + } + + public void ShowCountry(Country requested) + { + if (requested == null) + return; + + Show(); + + Country.Value = requested; + } + + private void loadNewContent() + { + loading.Show(); + + cancellationToken?.Cancel(); + lastRequest?.Cancel(); + + var request = createScopedRequest(); + lastRequest = request; + + if (request == null) + { + loadTable(null); + return; + } + + request.Success += () => loadTable(createTableFromResponse(request)); + request.Failure += _ => loadTable(null); + + api.Queue(request); + } + + private APIRequest createScopedRequest() + { + switch (Scope.Value) + { + case RankingsScope.Performance: + return new GetUserRankingsRequest(ruleset.Value, country: Country.Value?.FlagName); + + case RankingsScope.Country: + return new GetCountryRankingsRequest(ruleset.Value); + + case RankingsScope.Score: + return new GetUserRankingsRequest(ruleset.Value, UserRankingsType.Score); + } + + return null; + } + + private Drawable createTableFromResponse(APIRequest request) + { + switch (request) + { + case GetUserRankingsRequest userRequest: + switch (userRequest.Type) + { + case UserRankingsType.Performance: + return new PerformanceTable(1, userRequest.Result.Users); + + case UserRankingsType.Score: + return new ScoresTable(1, userRequest.Result.Users); + } + + return null; + + case GetCountryRankingsRequest countryRequest: + return new CountriesTable(1, countryRequest.Result.Countries); + } + + return null; + } + + private void loadTable(Drawable table) + { + scrollFlow.ScrollToStart(); + + if (table == null) + { + tableContainer.Clear(); + loading.Hide(); + return; + } + + LoadComponentAsync(table, t => + { + loading.Hide(); + tableContainer.Child = table; + }, (cancellationToken = new CancellationTokenSource()).Token); + } + } +} diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index 023d37497a..1c280c820d 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -69,7 +69,7 @@ public abstract class Mod : IMod, IJsonSerializable /// /// Creates a copy of this initialised to a default state. /// - public virtual Mod CreateCopy() => (Mod)Activator.CreateInstance(GetType()); + public virtual Mod CreateCopy() => (Mod)MemberwiseClone(); public bool Equals(IMod other) => GetType() == other?.GetType(); } diff --git a/osu.Game/Users/Country.cs b/osu.Game/Users/Country.cs index 1dcce6e870..a9fcd69286 100644 --- a/osu.Game/Users/Country.cs +++ b/osu.Game/Users/Country.cs @@ -1,11 +1,12 @@ // 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 Newtonsoft.Json; namespace osu.Game.Users { - public class Country + public class Country : IEquatable { /// /// The name of this country. @@ -18,5 +19,7 @@ public class Country /// [JsonProperty(@"code")] public string FlagName; + + public bool Equals(Country other) => FlagName == other?.FlagName; } } diff --git a/osu.Game/Users/Drawables/UpdateableFlag.cs b/osu.Game/Users/Drawables/UpdateableFlag.cs index abc16b2390..1d30720889 100644 --- a/osu.Game/Users/Drawables/UpdateableFlag.cs +++ b/osu.Game/Users/Drawables/UpdateableFlag.cs @@ -1,8 +1,11 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Input.Events; +using osu.Game.Overlays; namespace osu.Game.Users.Drawables { @@ -34,5 +37,14 @@ protected override Drawable CreateDrawable(Country country) RelativeSizeAxes = Axes.Both, }; } + + [Resolved(canBeNull: true)] + private RankingsOverlay rankingsOverlay { get; set; } + + protected override bool OnClick(ClickEvent e) + { + rankingsOverlay?.ShowCountry(Country); + return true; + } } }