diff --git a/osu.Desktop/Properties/launchSettings.json b/osu.Desktop/Properties/launchSettings.json new file mode 100644 index 0000000000..5e768ec9fa --- /dev/null +++ b/osu.Desktop/Properties/launchSettings.json @@ -0,0 +1,11 @@ +{ + "profiles": { + "osu! Desktop": { + "commandName": "Project" + }, + "osu! Tournament": { + "commandName": "Project", + "commandLineArgs": "--tournament" + } + } +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Catch/MathUtils/FastRandom.cs b/osu.Game.Rulesets.Catch/MathUtils/FastRandom.cs index c721ff862a..46e427e1b7 100644 --- a/osu.Game.Rulesets.Catch/MathUtils/FastRandom.cs +++ b/osu.Game.Rulesets.Catch/MathUtils/FastRandom.cs @@ -12,14 +12,14 @@ namespace osu.Game.Rulesets.Catch.MathUtils { private const double int_to_real = 1.0 / (int.MaxValue + 1.0); private const uint int_mask = 0x7FFFFFFF; - private const uint y = 842502087; - private const uint z = 3579807591; - private const uint w = 273326509; - private uint _x, _y = y, _z = z, _w = w; + private const uint y_initial = 842502087; + private const uint z_initial = 3579807591; + private const uint w_initial = 273326509; + private uint x, y = y_initial, z = z_initial, w = w_initial; public FastRandom(int seed) { - _x = (uint)seed; + x = (uint)seed; } public FastRandom() @@ -33,11 +33,11 @@ namespace osu.Game.Rulesets.Catch.MathUtils /// The random value. public uint NextUInt() { - uint t = _x ^ (_x << 11); - _x = _y; - _y = _z; - _z = _w; - return _w = _w ^ (_w >> 19) ^ t ^ (t >> 8); + uint t = x ^ (x << 11); + x = y; + y = z; + z = w; + return w = w ^ (w >> 19) ^ t ^ (t >> 8); } /// diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs index 4fe02135c4..c2aefac587 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Game.Graphics; using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Objects; using osuTK; using osuTK.Graphics; @@ -20,10 +21,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components { public class PathControlPointPiece : BlueprintPiece { - public Action RequestSelection; + public Action RequestSelection; public readonly BindableBool IsSelected = new BindableBool(); - public readonly int Index; + + public readonly PathControlPoint ControlPoint; private readonly Slider slider; private readonly Path path; @@ -36,10 +38,14 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components [Resolved] private OsuColour colours { get; set; } - public PathControlPointPiece(Slider slider, int index) + private IBindable sliderPosition; + private IBindable pathVersion; + + public PathControlPointPiece(Slider slider, PathControlPoint controlPoint) { this.slider = slider; - Index = index; + + ControlPoint = controlPoint; Origin = Anchor.Centre; AutoSizeAxes = Axes.Both; @@ -85,48 +91,41 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components }; } - protected override void Update() + protected override void LoadComplete() { - base.Update(); + base.LoadComplete(); - Position = slider.StackedPosition + slider.Path.ControlPoints[Index].Position.Value; + sliderPosition = slider.PositionBindable.GetBoundCopy(); + sliderPosition.BindValueChanged(_ => updateDisplay()); + pathVersion = slider.Path.Version.GetBoundCopy(); + pathVersion.BindValueChanged(_ => updateDisplay()); + + IsSelected.BindValueChanged(_ => updateMarkerDisplay()); + + updateDisplay(); + } + + private void updateDisplay() + { updateMarkerDisplay(); updateConnectingPath(); } - /// - /// Updates the state of the circular control point marker. - /// - private void updateMarkerDisplay() - { - markerRing.Alpha = IsSelected.Value ? 1 : 0; - - Color4 colour = slider.Path.ControlPoints[Index].Type.Value.HasValue ? colours.Red : colours.Yellow; - if (IsHovered || IsSelected.Value) - colour = Color4.White; - marker.Colour = colour; - } - - /// - /// Updates the path connecting this control point to the previous one. - /// - private void updateConnectingPath() - { - path.ClearVertices(); - - if (Index != slider.Path.ControlPoints.Count - 1) - { - path.AddVertex(Vector2.Zero); - path.AddVertex(slider.Path.ControlPoints[Index + 1].Position.Value - slider.Path.ControlPoints[Index].Position.Value); - } - - path.OriginPosition = path.PositionInBoundingBox(Vector2.Zero); - } - // The connecting path is excluded from positional input public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => marker.ReceivePositionalInputAt(screenSpacePos); + protected override bool OnHover(HoverEvent e) + { + updateMarkerDisplay(); + return false; + } + + protected override void OnHoverLost(HoverLostEvent e) + { + updateMarkerDisplay(); + } + protected override bool OnMouseDown(MouseDownEvent e) { if (RequestSelection == null) @@ -135,12 +134,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components switch (e.Button) { case MouseButton.Left: - RequestSelection.Invoke(Index, e); + RequestSelection.Invoke(this, e); return true; case MouseButton.Right: if (!IsSelected.Value) - RequestSelection.Invoke(Index, e); + RequestSelection.Invoke(this, e); return false; // Allow context menu to show } @@ -155,7 +154,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components protected override bool OnDrag(DragEvent e) { - if (Index == 0) + if (ControlPoint == slider.Path.ControlPoints[0]) { // Special handling for the head control point - the position of the slider changes which means the snapped position and time have to be taken into account (Vector2 snappedPosition, double snappedTime) = snapProvider?.GetSnappedPosition(e.MousePosition, slider.StartTime) ?? (e.MousePosition, slider.StartTime); @@ -169,11 +168,47 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components slider.Path.ControlPoints[i].Position.Value -= movementDelta; } else - slider.Path.ControlPoints[Index].Position.Value += e.Delta; + ControlPoint.Position.Value += e.Delta; return true; } protected override bool OnDragEnd(DragEndEvent e) => true; + + /// + /// Updates the state of the circular control point marker. + /// + private void updateMarkerDisplay() + { + Position = slider.StackedPosition + ControlPoint.Position.Value; + + markerRing.Alpha = IsSelected.Value ? 1 : 0; + + Color4 colour = ControlPoint.Type.Value != null ? colours.Red : colours.Yellow; + if (IsHovered || IsSelected.Value) + colour = Color4.White; + marker.Colour = colour; + } + + /// + /// Updates the path connecting this control point to the previous one. + /// + private void updateConnectingPath() + { + path.ClearVertices(); + + int index = slider.Path.ControlPoints.IndexOf(ControlPoint); + + if (index == -1) + return; + + if (++index != slider.Path.ControlPoints.Count) + { + path.AddVertex(Vector2.Zero); + path.AddVertex(slider.Path.ControlPoints[index].Position.Value - ControlPoint.Position.Value); + } + + path.OriginPosition = path.PositionInBoundingBox(Vector2.Zero); + } } } 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 ced4af4e28..cd19653a2e 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -1,10 +1,11 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using System.Linq; using Humanizer; -using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; @@ -14,9 +15,8 @@ using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Game.Graphics.UserInterface; 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 @@ -24,13 +24,16 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components public class PathControlPointVisualiser : CompositeDrawable, IKeyBindingHandler, IHasContextMenu { internal readonly Container Pieces; + private readonly Slider slider; + private readonly bool allowSelection; private InputManager inputManager; - [Resolved(CanBeNull = true)] - private IPlacementHandler placementHandler { get; set; } + private IBindableList controlPoints; + + public Action> RemoveControlPointsRequested; public PathControlPointVisualiser(Slider slider, bool allowSelection) { @@ -47,30 +50,40 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components base.LoadComplete(); inputManager = GetContainingInputManager(); + + controlPoints = slider.Path.ControlPoints.GetBoundCopy(); + controlPoints.ItemsAdded += addControlPoints; + controlPoints.ItemsRemoved += removeControlPoints; + + addControlPoints(controlPoints); } - protected override void Update() + private void addControlPoints(IEnumerable controlPoints) { - base.Update(); - - while (slider.Path.ControlPoints.Count > Pieces.Count) + foreach (var point in controlPoints) { - var piece = new PathControlPointPiece(slider, Pieces.Count); + var piece = new PathControlPointPiece(slider, point); if (allowSelection) piece.RequestSelection = selectPiece; Pieces.Add(piece); } + } - while (slider.Path.ControlPoints.Count < Pieces.Count) - Pieces.Remove(Pieces[Pieces.Count - 1]); + private void removeControlPoints(IEnumerable controlPoints) + { + foreach (var point in controlPoints) + Pieces.RemoveAll(p => p.ControlPoint == point); } protected override bool OnClick(ClickEvent e) { foreach (var piece in Pieces) + { piece.IsSelected.Value = false; + } + return false; } @@ -87,48 +100,26 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components public bool OnReleased(PlatformAction action) => action.ActionMethod == PlatformActionMethod.Delete; - private void selectPiece(int index, MouseButtonEvent e) + private void selectPiece(PathControlPointPiece piece, MouseButtonEvent e) { if (e.Button == MouseButton.Left && inputManager.CurrentState.Keyboard.ControlPressed) - Pieces[index].IsSelected.Toggle(); + piece.IsSelected.Toggle(); else { - foreach (var piece in Pieces) - piece.IsSelected.Value = piece.Index == index; + foreach (var p in Pieces) + p.IsSelected.Value = p == piece; } } private bool deleteSelected() { - List toRemove = Pieces.Where(p => p.IsSelected.Value).Select(p => slider.Path.ControlPoints[p.Index]).ToList(); + List toRemove = Pieces.Where(p => p.IsSelected.Value).Select(p => p.ControlPoint).ToList(); // Ensure that there are any points to be deleted 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) @@ -144,16 +135,63 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components if (!Pieces.Any(p => p.IsHovered)) return null; - int selectedPoints = Pieces.Count(p => p.IsSelected.Value); + var selectedPieces = Pieces.Where(p => p.IsSelected.Value).ToList(); + int count = selectedPieces.Count; - if (selectedPoints == 0) + if (count == 0) return null; + List items = new List(); + + if (!selectedPieces.Contains(Pieces[0])) + items.Add(createMenuItemForPathType(null)); + + // todo: hide/disable items which aren't valid for selected points + items.Add(createMenuItemForPathType(PathType.Linear)); + items.Add(createMenuItemForPathType(PathType.PerfectCurve)); + items.Add(createMenuItemForPathType(PathType.Bezier)); + items.Add(createMenuItemForPathType(PathType.Catmull)); + return new MenuItem[] { - new OsuMenuItem($"Delete {"control point".ToQuantity(selectedPoints, selectedPoints > 1 ? ShowQuantityAs.Numeric : ShowQuantityAs.None)}", MenuItemType.Destructive, () => deleteSelected()) + new OsuMenuItem($"Delete {"control point".ToQuantity(count, count > 1 ? ShowQuantityAs.Numeric : ShowQuantityAs.None)}", MenuItemType.Destructive, () => deleteSelected()), + new OsuMenuItem("Curve type") + { + Items = items + } }; } } + + private MenuItem createMenuItemForPathType(PathType? type) + { + int totalCount = Pieces.Count(p => p.IsSelected.Value); + int countOfState = Pieces.Where(p => p.IsSelected.Value).Count(p => p.ControlPoint.Type.Value == type); + + var item = new PathTypeMenuItem(type, () => + { + foreach (var p in Pieces.Where(p => p.IsSelected.Value)) + p.ControlPoint.Type.Value = type; + }); + + if (countOfState == totalCount) + item.State.Value = TernaryState.True; + else if (countOfState > 0) + item.State.Value = TernaryState.Indeterminate; + else + item.State.Value = TernaryState.False; + + return item; + } + + private class PathTypeMenuItem : TernaryStateMenuItem + { + public PathTypeMenuItem(PathType? type, Action action) + : base(type == null ? "Inherit" : type.ToString().Humanize(), changeState, MenuItemType.Standard, _ => action?.Invoke()) + { + } + + private static TernaryState changeState(TernaryState state) => TernaryState.True; + } } } 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.Objects; 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 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders [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 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders HeadBlueprint = CreateCircleSelectionBlueprint(slider, SliderPosition.Start), TailBlueprint = CreateCircleSelectionBlueprint(slider, SliderPosition.End), ControlPointVisualiser = new PathControlPointVisualiser(sliderObject, true) + { + RemoveControlPointsRequested = removeControlPoints + } }; } @@ -97,6 +105,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders return true; } + private BindableList controlPoints => HitObject.Path.ControlPoints; + private int addControlPoint(Vector2 position) { position -= HitObject.Position; @@ -104,9 +114,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders 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 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders } // 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.Rulesets.Osu/Edit/OsuDistanceSnapGrid.cs b/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapGrid.cs index 9b00204d51..bde86a2890 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapGrid.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapGrid.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 JetBrains.Annotations; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Screens.Edit.Compose.Components; @@ -8,8 +9,8 @@ namespace osu.Game.Rulesets.Osu.Edit { public class OsuDistanceSnapGrid : CircularDistanceSnapGrid { - public OsuDistanceSnapGrid(OsuHitObject hitObject, OsuHitObject nextHitObject) - : base(hitObject, nextHitObject, hitObject.StackedEndPosition) + public OsuDistanceSnapGrid(OsuHitObject hitObject, [CanBeNull] OsuHitObject nextHitObject = null) + : base(hitObject.StackedPosition, hitObject.StartTime, nextHitObject?.StartTime) { Masking = true; } diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyCursor.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyCursor.cs index 05b38ae195..02152fa51e 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacyCursor.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyCursor.cs @@ -3,15 +3,14 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Game.Skinning; +using osu.Game.Rulesets.Osu.UI.Cursor; using osuTK; namespace osu.Game.Rulesets.Osu.Skinning { - public class LegacyCursor : CompositeDrawable + public class LegacyCursor : OsuCursorSprite { - private NonPlayfieldSprite cursor; private bool spin; public LegacyCursor() @@ -27,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Skinning { spin = skin.GetConfig(OsuSkinConfiguration.CursorRotate)?.Value ?? true; - InternalChildren = new Drawable[] + InternalChildren = new[] { new NonPlayfieldSprite { @@ -35,7 +34,7 @@ namespace osu.Game.Rulesets.Osu.Skinning Anchor = Anchor.Centre, Origin = Anchor.Centre, }, - cursor = new NonPlayfieldSprite + ExpandTarget = new NonPlayfieldSprite { Texture = skin.GetTexture("cursor"), Anchor = Anchor.Centre, @@ -47,7 +46,7 @@ namespace osu.Game.Rulesets.Osu.Skinning protected override void LoadComplete() { if (spin) - cursor.Spin(10000, RotationDirection.Clockwise); + ExpandTarget.Spin(10000, RotationDirection.Clockwise); } } } diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs index 0aa8661fd3..4f3d07f208 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs @@ -20,7 +20,9 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor private bool cursorExpand; - private Container expandTarget; + private SkinnableDrawable cursorSprite; + + private Drawable expandTarget => (cursorSprite.Drawable as OsuCursorSprite)?.ExpandTarget ?? cursorSprite; public OsuCursor() { @@ -37,12 +39,12 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor [BackgroundDependencyLoader] private void load() { - InternalChild = expandTarget = new Container + InternalChild = new Container { RelativeSizeAxes = Axes.Both, Origin = Anchor.Centre, Anchor = Anchor.Centre, - Child = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.Cursor), _ => new DefaultCursor(), confineMode: ConfineMode.NoScaling) + Child = cursorSprite = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.Cursor), _ => new DefaultCursor(), confineMode: ConfineMode.NoScaling) { Origin = Anchor.Centre, Anchor = Anchor.Centre, @@ -62,7 +64,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor public void Contract() => expandTarget.ScaleTo(released_scale, 100, Easing.OutQuad); - private class DefaultCursor : CompositeDrawable + private class DefaultCursor : OsuCursorSprite { public DefaultCursor() { @@ -71,10 +73,12 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor Anchor = Anchor.Centre; Origin = Anchor.Centre; - InternalChildren = new Drawable[] + InternalChildren = new[] { - new CircularContainer + ExpandTarget = new CircularContainer { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, Masking = true, BorderThickness = size / 6, diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorSprite.cs b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorSprite.cs new file mode 100644 index 0000000000..573c408a78 --- /dev/null +++ b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorSprite.cs @@ -0,0 +1,17 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; + +namespace osu.Game.Rulesets.Osu.UI.Cursor +{ + public abstract class OsuCursorSprite : CompositeDrawable + { + /// + /// The an optional piece of the cursor to expand when in a clicked state. + /// If null, the whole cursor will be affected by expansion. + /// + public Drawable ExpandTarget { get; protected set; } + } +} diff --git a/osu.Game.Tests/Visual/Editor/TestSceneDistanceSnapGrid.cs b/osu.Game.Tests/Visual/Editor/TestSceneDistanceSnapGrid.cs index e4c987923c..39b4bf7218 100644 --- a/osu.Game.Tests/Visual/Editor/TestSceneDistanceSnapGrid.cs +++ b/osu.Game.Tests/Visual/Editor/TestSceneDistanceSnapGrid.cs @@ -7,7 +7,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Edit; -using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Screens.Edit; @@ -44,7 +43,7 @@ namespace osu.Game.Tests.Visual.Editor RelativeSizeAxes = Axes.Both, Colour = Color4.SlateGray }, - new TestDistanceSnapGrid(new HitObject(), grid_position) + new TestDistanceSnapGrid() }; }); @@ -73,7 +72,7 @@ namespace osu.Game.Tests.Visual.Editor RelativeSizeAxes = Axes.Both, Colour = Color4.SlateGray }, - new TestDistanceSnapGrid(new HitObject(), grid_position, new HitObject { StartTime = 100 }) + new TestDistanceSnapGrid(100) }; }); } @@ -82,68 +81,68 @@ namespace osu.Game.Tests.Visual.Editor { public new float DistanceSpacing => base.DistanceSpacing; - public TestDistanceSnapGrid(HitObject hitObject, Vector2 centrePosition, HitObject nextHitObject = null) - : base(hitObject, nextHitObject, centrePosition) + public TestDistanceSnapGrid(double? endTime = null) + : base(grid_position, 0, endTime) { } - protected override void CreateContent(Vector2 centrePosition) + protected override void CreateContent(Vector2 startPosition) { AddInternal(new Circle { Origin = Anchor.Centre, Size = new Vector2(5), - Position = centrePosition + Position = startPosition }); int beatIndex = 0; - for (float s = centrePosition.X + DistanceSpacing; s <= DrawWidth && beatIndex < MaxIntervals; s += DistanceSpacing, beatIndex++) + for (float s = startPosition.X + DistanceSpacing; s <= DrawWidth && beatIndex < MaxIntervals; s += DistanceSpacing, beatIndex++) { AddInternal(new Circle { Origin = Anchor.Centre, Size = new Vector2(5, 10), - Position = new Vector2(s, centrePosition.Y), + Position = new Vector2(s, startPosition.Y), Colour = GetColourForBeatIndex(beatIndex) }); } beatIndex = 0; - for (float s = centrePosition.X - DistanceSpacing; s >= 0 && beatIndex < MaxIntervals; s -= DistanceSpacing, beatIndex++) + for (float s = startPosition.X - DistanceSpacing; s >= 0 && beatIndex < MaxIntervals; s -= DistanceSpacing, beatIndex++) { AddInternal(new Circle { Origin = Anchor.Centre, Size = new Vector2(5, 10), - Position = new Vector2(s, centrePosition.Y), + Position = new Vector2(s, startPosition.Y), Colour = GetColourForBeatIndex(beatIndex) }); } beatIndex = 0; - for (float s = centrePosition.Y + DistanceSpacing; s <= DrawHeight && beatIndex < MaxIntervals; s += DistanceSpacing, beatIndex++) + for (float s = startPosition.Y + DistanceSpacing; s <= DrawHeight && beatIndex < MaxIntervals; s += DistanceSpacing, beatIndex++) { AddInternal(new Circle { Origin = Anchor.Centre, Size = new Vector2(10, 5), - Position = new Vector2(centrePosition.X, s), + Position = new Vector2(startPosition.X, s), Colour = GetColourForBeatIndex(beatIndex) }); } beatIndex = 0; - for (float s = centrePosition.Y - DistanceSpacing; s >= 0 && beatIndex < MaxIntervals; s -= DistanceSpacing, beatIndex++) + for (float s = startPosition.Y - DistanceSpacing; s >= 0 && beatIndex < MaxIntervals; s -= DistanceSpacing, beatIndex++) { AddInternal(new Circle { Origin = Anchor.Centre, Size = new Vector2(10, 5), - Position = new Vector2(centrePosition.X, s), + Position = new Vector2(startPosition.X, s), Colour = GetColourForBeatIndex(beatIndex) }); } diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs index 803cab9325..e04315894e 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs @@ -285,8 +285,6 @@ namespace osu.Game.Tests.Visual.Gameplay protected class PausePlayer : TestPlayer { - public new GameplayClockContainer GameplayClockContainer => base.GameplayClockContainer; - public new ScoreProcessor ScoreProcessor => base.ScoreProcessor; public new HUDOverlay HUDOverlay => base.HUDOverlay; diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePauseWhenInactive.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePauseWhenInactive.cs new file mode 100644 index 0000000000..3513b6c25a --- /dev/null +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePauseWhenInactive.cs @@ -0,0 +1,51 @@ +// 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.Bindables; +using osu.Framework.Platform; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Osu; +using osu.Game.Screens.Play; + +namespace osu.Game.Tests.Visual.Gameplay +{ + [HeadlessTest] // we alter unsafe properties on the game host to test inactive window state. + public class TestScenePauseWhenInactive : PlayerTestScene + { + protected new TestPlayer Player => (TestPlayer)base.Player; + + protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) + { + var beatmap = (Beatmap)base.CreateBeatmap(ruleset); + + beatmap.HitObjects.RemoveAll(h => h.StartTime < 30000); + + return beatmap; + } + + [Resolved] + private GameHost host { get; set; } + + public TestScenePauseWhenInactive() + : base(new OsuRuleset()) + { + } + + [Test] + public void TestDoesntPauseDuringIntro() + { + AddStep("set inactive", () => ((Bindable)host.IsActive).Value = false); + + AddStep("resume player", () => Player.GameplayClockContainer.Start()); + AddAssert("ensure not paused", () => !Player.GameplayClockContainer.IsPaused.Value); + AddUntilStep("wait for pause", () => Player.GameplayClockContainer.IsPaused.Value); + AddAssert("time of pause is after gameplay start time", () => Player.GameplayClockContainer.GameplayClock.CurrentTime >= Player.DrawableRuleset.GameplayStartTime); + } + + protected override Player CreatePlayer(Ruleset ruleset) => new TestPlayer(true, true, true); + } +} 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 @@ namespace osu.Game.Tests.Visual.Online }; 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 @@ namespace osu.Game.Tests.Visual.Online 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/SongSelect/TestSceneBeatmapDetailArea.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapDetailArea.cs index ed9e01a67e..d4805a73e4 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapDetailArea.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapDetailArea.cs @@ -8,6 +8,9 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Beatmaps; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; +using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Select; using osu.Game.Tests.Beatmaps; using osuTK; @@ -20,8 +23,10 @@ namespace osu.Game.Tests.Visual.SongSelect { public override IReadOnlyList RequiredTypes => new[] { typeof(BeatmapDetails) }; + private ModDisplay modDisplay; + [BackgroundDependencyLoader] - private void load(OsuGameBase game) + private void load(OsuGameBase game, RulesetStore rulesets) { BeatmapDetailArea detailsArea; Add(detailsArea = new BeatmapDetailArea @@ -31,6 +36,16 @@ namespace osu.Game.Tests.Visual.SongSelect Size = new Vector2(550f, 450f), }); + Add(modDisplay = new ModDisplay + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + AutoSizeAxes = Axes.Both, + Position = new Vector2(0, 25), + }); + + modDisplay.Current.BindTo(Mods); + AddStep("all metrics", () => detailsArea.Beatmap = new TestWorkingBeatmap(new Beatmap { BeatmapInfo = @@ -163,6 +178,60 @@ namespace osu.Game.Tests.Visual.SongSelect })); AddStep("null beatmap", () => detailsArea.Beatmap = null); + + Ruleset ruleset = rulesets.AvailableRulesets.First().CreateInstance(); + + AddStep("with EZ mod", () => + { + detailsArea.Beatmap = new TestWorkingBeatmap(new Beatmap + { + BeatmapInfo = + { + Version = "Has Easy Mod", + Metadata = new BeatmapMetadata + { + Source = "osu!lazer", + Tags = "this beatmap has the easy mod enabled", + }, + BaseDifficulty = new BeatmapDifficulty + { + CircleSize = 3, + DrainRate = 3, + OverallDifficulty = 3, + ApproachRate = 3, + }, + StarDifficulty = 1f, + } + }); + + Mods.Value = new[] { ruleset.GetAllMods().First(m => m is ModEasy) }; + }); + + AddStep("with HR mod", () => + { + detailsArea.Beatmap = new TestWorkingBeatmap(new Beatmap + { + BeatmapInfo = + { + Version = "Has Hard Rock Mod", + Metadata = new BeatmapMetadata + { + Source = "osu!lazer", + Tags = "this beatmap has the hard rock mod enabled", + }, + BaseDifficulty = new BeatmapDifficulty + { + CircleSize = 3, + DrainRate = 3, + OverallDifficulty = 3, + ApproachRate = 3, + }, + StarDifficulty = 1f, + } + }); + + Mods.Value = new[] { ruleset.GetAllMods().First(m => m is ModHardRock) }; + }); } } } diff --git a/osu.Game.Tournament/Screens/Drawings/Components/ScrollingTeamContainer.cs b/osu.Game.Tournament/Screens/Drawings/Components/ScrollingTeamContainer.cs index a345f93896..3ff4718b75 100644 --- a/osu.Game.Tournament/Screens/Drawings/Components/ScrollingTeamContainer.cs +++ b/osu.Game.Tournament/Screens/Drawings/Components/ScrollingTeamContainer.cs @@ -83,88 +83,81 @@ namespace osu.Game.Tournament.Screens.Drawings.Components }; } - private ScrollState _scrollState; + private ScrollState scrollState; - private ScrollState scrollState + private void setScrollState(ScrollState newstate) { - get => _scrollState; + if (scrollState == newstate) + return; - set + delayedStateChangeDelegate?.Cancel(); + + switch (scrollState = newstate) { - if (_scrollState == value) - return; + case ScrollState.Scrolling: + resetSelected(); - _scrollState = value; + OnScrollStarted?.Invoke(); - delayedStateChangeDelegate?.Cancel(); + speedTo(1000f, 200); + tracker.FadeOut(100); + break; - switch (value) - { - case ScrollState.Scrolling: - resetSelected(); + case ScrollState.Stopping: + speedTo(0f, 2000); + tracker.FadeIn(200); - OnScrollStarted?.Invoke(); + delayedStateChangeDelegate = Scheduler.AddDelayed(() => setScrollState(ScrollState.Stopped), 2300); + break; - speedTo(1000f, 200); - tracker.FadeOut(100); + case ScrollState.Stopped: + // Find closest to center + if (!Children.Any()) break; - case ScrollState.Stopping: - speedTo(0f, 2000); - tracker.FadeIn(200); + ScrollingTeam closest = null; - delayedStateChangeDelegate = Scheduler.AddDelayed(() => scrollState = ScrollState.Stopped, 2300); - break; + foreach (var c in Children) + { + if (!(c is ScrollingTeam stc)) + continue; - case ScrollState.Stopped: - // Find closest to center - if (!Children.Any()) - break; - - ScrollingTeam closest = null; - - foreach (var c in Children) + if (closest == null) { - if (!(c is ScrollingTeam stc)) - continue; - - if (closest == null) - { - closest = stc; - continue; - } - - float o = Math.Abs(c.Position.X + c.DrawWidth / 2f - DrawWidth / 2f); - float lastOffset = Math.Abs(closest.Position.X + closest.DrawWidth / 2f - DrawWidth / 2f); - - if (o < lastOffset) - closest = stc; + closest = stc; + continue; } - Trace.Assert(closest != null, "closest != null"); + float o = Math.Abs(c.Position.X + c.DrawWidth / 2f - DrawWidth / 2f); + float lastOffset = Math.Abs(closest.Position.X + closest.DrawWidth / 2f - DrawWidth / 2f); - // ReSharper disable once PossibleNullReferenceException - offset += DrawWidth / 2f - (closest.Position.X + closest.DrawWidth / 2f); + if (o < lastOffset) + closest = stc; + } - ScrollingTeam st = closest; + Trace.Assert(closest != null, "closest != null"); - availableTeams.RemoveAll(at => at == st.Team); + // ReSharper disable once PossibleNullReferenceException + offset += DrawWidth / 2f - (closest.Position.X + closest.DrawWidth / 2f); - st.Selected = true; - OnSelected?.Invoke(st.Team); + ScrollingTeam st = closest; - delayedStateChangeDelegate = Scheduler.AddDelayed(() => scrollState = ScrollState.Idle, 10000); - break; + availableTeams.RemoveAll(at => at == st.Team); - case ScrollState.Idle: - resetSelected(); + st.Selected = true; + OnSelected?.Invoke(st.Team); - OnScrollStarted?.Invoke(); + delayedStateChangeDelegate = Scheduler.AddDelayed(() => setScrollState(ScrollState.Idle), 10000); + break; - speedTo(40f, 200); - tracker.FadeOut(100); - break; - } + case ScrollState.Idle: + resetSelected(); + + OnScrollStarted?.Invoke(); + + speedTo(40f, 200); + tracker.FadeOut(100); + break; } } @@ -176,7 +169,7 @@ namespace osu.Game.Tournament.Screens.Drawings.Components availableTeams.Add(team); RemoveAll(c => c is ScrollingTeam); - scrollState = ScrollState.Idle; + setScrollState(ScrollState.Idle); } public void AddTeams(IEnumerable teams) @@ -192,7 +185,7 @@ namespace osu.Game.Tournament.Screens.Drawings.Components { availableTeams.Clear(); RemoveAll(c => c is ScrollingTeam); - scrollState = ScrollState.Idle; + setScrollState(ScrollState.Idle); } public void RemoveTeam(TournamentTeam team) @@ -217,7 +210,7 @@ namespace osu.Game.Tournament.Screens.Drawings.Components if (availableTeams.Count == 0) return; - scrollState = ScrollState.Scrolling; + setScrollState(ScrollState.Scrolling); } public void StopScrolling() @@ -232,13 +225,13 @@ namespace osu.Game.Tournament.Screens.Drawings.Components return; } - scrollState = ScrollState.Stopping; + setScrollState(ScrollState.Stopping); } protected override void LoadComplete() { base.LoadComplete(); - scrollState = ScrollState.Idle; + setScrollState(ScrollState.Idle); } protected override void UpdateAfterChildren() @@ -305,7 +298,7 @@ namespace osu.Game.Tournament.Screens.Drawings.Components private void speedTo(float value, double duration = 0, Easing easing = Easing.None) => this.TransformTo(nameof(speed), value, duration, easing); - private enum ScrollState + protected enum ScrollState { None, Idle, 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 @@ namespace osu.Game.Online.API.Requests return req; } - protected override string TargetPostfix() => type.ToString().ToLowerInvariant(); + protected override string TargetPostfix() => Type.ToString().ToLowerInvariant(); } public enum UserRankingsType diff --git a/osu.Game/Online/Multiplayer/PlaylistItem.cs b/osu.Game/Online/Multiplayer/PlaylistItem.cs index e47d497d94..d13e8b31e6 100644 --- a/osu.Game/Online/Multiplayer/PlaylistItem.cs +++ b/osu.Game/Online/Multiplayer/PlaylistItem.cs @@ -45,23 +45,25 @@ namespace osu.Game.Online.Multiplayer [JsonProperty("beatmap")] private APIBeatmap apiBeatmap { get; set; } + private APIMod[] allowedModsBacking; + [JsonProperty("allowed_mods")] private APIMod[] allowedMods { get => AllowedMods.Select(m => new APIMod { Acronym = m.Acronym }).ToArray(); - set => _allowedMods = value; + set => allowedModsBacking = value; } + private APIMod[] requiredModsBacking; + [JsonProperty("required_mods")] private APIMod[] requiredMods { get => RequiredMods.Select(m => new APIMod { Acronym = m.Acronym }).ToArray(); - set => _requiredMods = value; + set => requiredModsBacking = value; } private BeatmapInfo beatmap; - private APIMod[] _allowedMods; - private APIMod[] _requiredMods; public void MapObjects(BeatmapManager beatmaps, RulesetStore rulesets) { @@ -70,20 +72,20 @@ namespace osu.Game.Online.Multiplayer Beatmap = apiBeatmap == null ? beatmaps.QueryBeatmap(b => b.OnlineBeatmapID == BeatmapID) : apiBeatmap.ToBeatmap(rulesets); Ruleset = rulesets.GetRuleset(RulesetID); - if (_allowedMods != null) + if (allowedModsBacking != null) { AllowedMods.Clear(); - AllowedMods.AddRange(Ruleset.CreateInstance().GetAllMods().Where(mod => _allowedMods.Any(m => m.Acronym == mod.Acronym))); + AllowedMods.AddRange(Ruleset.CreateInstance().GetAllMods().Where(mod => allowedModsBacking.Any(m => m.Acronym == mod.Acronym))); - _allowedMods = null; + allowedModsBacking = null; } - if (_requiredMods != null) + if (requiredModsBacking != null) { RequiredMods.Clear(); - RequiredMods.AddRange(Ruleset.CreateInstance().GetAllMods().Where(mod => _requiredMods.Any(m => m.Acronym == mod.Acronym))); + RequiredMods.AddRange(Ruleset.CreateInstance().GetAllMods().Where(mod => requiredModsBacking.Any(m => m.Acronym == mod.Acronym))); - _requiredMods = null; + requiredModsBacking = 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 @@ namespace osu.Game.Overlays.Rankings 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 @@ namespace osu.Game.Overlays.Rankings 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/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs index 91e19f9cc0..23ed10b92d 100644 --- a/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs @@ -5,19 +5,18 @@ using System; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; -using osu.Game.Rulesets.Objects; using osuTK; namespace osu.Game.Screens.Edit.Compose.Components { public abstract class CircularDistanceSnapGrid : DistanceSnapGrid { - protected CircularDistanceSnapGrid(HitObject hitObject, HitObject nextHitObject, Vector2 centrePosition) - : base(hitObject, nextHitObject, centrePosition) + protected CircularDistanceSnapGrid(Vector2 startPosition, double startTime, double? endTime = null) + : base(startPosition, startTime, endTime) { } - protected override void CreateContent(Vector2 centrePosition) + protected override void CreateContent(Vector2 startPosition) { const float crosshair_thickness = 1; const float crosshair_max_size = 10; @@ -27,7 +26,7 @@ namespace osu.Game.Screens.Edit.Compose.Components new Box { Origin = Anchor.Centre, - Position = centrePosition, + Position = startPosition, Width = crosshair_thickness, EdgeSmoothness = new Vector2(1), Height = Math.Min(crosshair_max_size, DistanceSpacing * 2), @@ -35,15 +34,15 @@ namespace osu.Game.Screens.Edit.Compose.Components new Box { Origin = Anchor.Centre, - Position = centrePosition, + Position = startPosition, EdgeSmoothness = new Vector2(1), Width = Math.Min(crosshair_max_size, DistanceSpacing * 2), Height = crosshair_thickness, } }); - float dx = Math.Max(centrePosition.X, DrawWidth - centrePosition.X); - float dy = Math.Max(centrePosition.Y, DrawHeight - centrePosition.Y); + float dx = Math.Max(startPosition.X, DrawWidth - startPosition.X); + float dy = Math.Max(startPosition.Y, DrawHeight - startPosition.Y); float maxDistance = new Vector2(dx, dy).Length; int requiredCircles = Math.Min(MaxIntervals, (int)(maxDistance / DistanceSpacing)); @@ -54,7 +53,7 @@ namespace osu.Game.Screens.Edit.Compose.Components AddInternal(new CircularProgress { Origin = Anchor.Centre, - Position = centrePosition, + Position = startPosition, Current = { Value = 1 }, Size = new Vector2(radius), InnerRadius = 4 * 1f / radius, @@ -66,9 +65,9 @@ namespace osu.Game.Screens.Edit.Compose.Components public override (Vector2 position, double time) GetSnappedPosition(Vector2 position) { if (MaxIntervals == 0) - return (CentrePosition, StartTime); + return (StartPosition, StartTime); - Vector2 direction = position - CentrePosition; + Vector2 direction = position - StartPosition; if (direction == Vector2.Zero) direction = new Vector2(0.001f, 0.001f); @@ -78,9 +77,9 @@ namespace osu.Game.Screens.Edit.Compose.Components int radialCount = Math.Clamp((int)MathF.Round(distance / radius), 1, MaxIntervals); Vector2 normalisedDirection = direction * new Vector2(1f / distance); - Vector2 snappedPosition = CentrePosition + normalisedDirection * radialCount * radius; + Vector2 snappedPosition = StartPosition + normalisedDirection * radialCount * radius; - return (snappedPosition, StartTime + SnapProvider.GetSnappedDurationFromDistance(StartTime, (snappedPosition - CentrePosition).Length)); + return (snappedPosition, StartTime + SnapProvider.GetSnappedDurationFromDistance(StartTime, (snappedPosition - StartPosition).Length)); } } } diff --git a/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs index 9508a2cdf0..00326d04f7 100644 --- a/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Caching; using osu.Framework.Graphics; @@ -9,7 +8,6 @@ using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Game.Graphics; using osu.Game.Rulesets.Edit; -using osu.Game.Rulesets.Objects; using osuTK; namespace osu.Game.Screens.Edit.Compose.Components @@ -24,21 +22,21 @@ namespace osu.Game.Screens.Edit.Compose.Components /// protected float DistanceSpacing { get; private set; } - /// - /// The snapping time at . - /// - protected double StartTime { get; private set; } - /// /// The maximum number of distance snapping intervals allowed. /// protected int MaxIntervals { get; private set; } /// - /// The position which the grid is centred on. - /// The first beat snapping tick is located at + in the desired direction. + /// The position which the grid should start. + /// The first beat snapping tick is located at + away from this point. /// - protected readonly Vector2 CentrePosition; + protected readonly Vector2 StartPosition; + + /// + /// The snapping time at . + /// + protected readonly double StartTime; [Resolved] protected OsuColour Colours { get; private set; } @@ -53,25 +51,23 @@ namespace osu.Game.Screens.Edit.Compose.Components private BindableBeatDivisor beatDivisor { get; set; } private readonly Cached gridCache = new Cached(); - private readonly HitObject hitObject; - private readonly HitObject nextHitObject; + private readonly double? endTime; - protected DistanceSnapGrid(HitObject hitObject, [CanBeNull] HitObject nextHitObject, Vector2 centrePosition) + /// + /// Creates a new . + /// + /// The position at which the grid should start. The first tick is located one distance spacing length away from this point. + /// The snapping time at . + /// The time at which the snapping grid should end. If null, the grid will continue until the bounds of the screen are exceeded. + protected DistanceSnapGrid(Vector2 startPosition, double startTime, double? endTime = null) { - this.hitObject = hitObject; - this.nextHitObject = nextHitObject; - - CentrePosition = centrePosition; + this.endTime = endTime; + StartPosition = startPosition; + StartTime = startTime; RelativeSizeAxes = Axes.Both; } - [BackgroundDependencyLoader] - private void load() - { - StartTime = hitObject.GetEndTime(); - } - protected override void LoadComplete() { base.LoadComplete(); @@ -83,12 +79,12 @@ namespace osu.Game.Screens.Edit.Compose.Components { DistanceSpacing = SnapProvider.GetBeatSnapDistanceAt(StartTime); - if (nextHitObject == null) + if (endTime == null) MaxIntervals = int.MaxValue; else { // +1 is added since a snapped hitobject may have its start time slightly less than the snapped time due to floating point errors - double maxDuration = nextHitObject.StartTime - StartTime + 1; + double maxDuration = endTime.Value - StartTime + 1; MaxIntervals = (int)(maxDuration / SnapProvider.DistanceToDuration(StartTime, DistanceSpacing)); } @@ -110,7 +106,7 @@ namespace osu.Game.Screens.Edit.Compose.Components if (!gridCache.IsValid) { ClearInternal(); - CreateContent(CentrePosition); + CreateContent(StartPosition); gridCache.Validate(); } } @@ -118,7 +114,7 @@ namespace osu.Game.Screens.Edit.Compose.Components /// /// Creates the content which visualises the grid ticks. /// - protected abstract void CreateContent(Vector2 centrePosition); + protected abstract void CreateContent(Vector2 startPosition); /// /// Snaps a position to this grid. diff --git a/osu.Game/Screens/Play/GameplayMenuOverlay.cs b/osu.Game/Screens/Play/GameplayMenuOverlay.cs index f54d638584..adfbe977a4 100644 --- a/osu.Game/Screens/Play/GameplayMenuOverlay.cs +++ b/osu.Game/Screens/Play/GameplayMenuOverlay.cs @@ -188,26 +188,22 @@ namespace osu.Game.Screens.Play InternalButtons.Add(button); } - private int _selectionIndex = -1; + private int selectionIndex = -1; - private int selectionIndex + private void setSelected(int value) { - get => _selectionIndex; - set - { - if (_selectionIndex == value) - return; + if (selectionIndex == value) + return; - // Deselect the previously-selected button - if (_selectionIndex != -1) - InternalButtons[_selectionIndex].Selected.Value = false; + // Deselect the previously-selected button + if (selectionIndex != -1) + InternalButtons[selectionIndex].Selected.Value = false; - _selectionIndex = value; + selectionIndex = value; - // Select the newly-selected button - if (_selectionIndex != -1) - InternalButtons[_selectionIndex].Selected.Value = true; - } + // Select the newly-selected button + if (selectionIndex != -1) + InternalButtons[selectionIndex].Selected.Value = true; } protected override bool OnKeyDown(KeyDownEvent e) @@ -218,16 +214,16 @@ namespace osu.Game.Screens.Play { case Key.Up: if (selectionIndex == -1 || selectionIndex == 0) - selectionIndex = InternalButtons.Count - 1; + setSelected(InternalButtons.Count - 1); else - selectionIndex--; + setSelected(selectionIndex - 1); return true; case Key.Down: if (selectionIndex == -1 || selectionIndex == InternalButtons.Count - 1) - selectionIndex = 0; + setSelected(0); else - selectionIndex++; + setSelected(selectionIndex + 1); return true; } } @@ -266,9 +262,9 @@ namespace osu.Game.Screens.Play private void buttonSelectionChanged(DialogButton button, bool isSelected) { if (!isSelected) - selectionIndex = -1; + setSelected(-1); else - selectionIndex = InternalButtons.IndexOf(button); + setSelected(InternalButtons.IndexOf(button)); } private void updateRetryCount() diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index d40c448452..9feee82989 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -135,7 +135,7 @@ namespace osu.Game.Screens.Play addGameplayComponents(GameplayClockContainer, working); addOverlayComponents(GameplayClockContainer, working); - DrawableRuleset.HasReplayLoaded.BindValueChanged(e => HUDOverlay.HoldToQuit.PauseOnFocusLost = !e.NewValue && PauseOnFocusLost, true); + DrawableRuleset.HasReplayLoaded.BindValueChanged(_ => updatePauseOnFocusLostState(), true); // bind clock into components that require it DrawableRuleset.IsPaused.BindTo(GameplayClockContainer.IsPaused); @@ -146,6 +146,7 @@ namespace osu.Game.Screens.Play foreach (var mod in Mods.Value.OfType()) mod.ApplyToScoreProcessor(ScoreProcessor); + breakOverlay.IsBreakTime.ValueChanged += _ => updatePauseOnFocusLostState(); } private void addUnderlayComponents(Container target) @@ -241,6 +242,11 @@ namespace osu.Game.Screens.Play }); } + private void updatePauseOnFocusLostState() => + HUDOverlay.HoldToQuit.PauseOnFocusLost = PauseOnFocusLost + && !DrawableRuleset.HasReplayLoaded.Value + && !breakOverlay.IsBreakTime.Value; + private WorkingBeatmap loadBeatmap() { WorkingBeatmap working = Beatmap.Value; diff --git a/osu.Game/Screens/Select/Details/AdvancedStats.cs b/osu.Game/Screens/Select/Details/AdvancedStats.cs index c5bdc230d0..3e7d4e244b 100644 --- a/osu.Game/Screens/Select/Details/AdvancedStats.cs +++ b/osu.Game/Screens/Select/Details/AdvancedStats.cs @@ -12,11 +12,18 @@ using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using System; using osu.Game.Beatmaps; +using osu.Framework.Bindables; +using System.Collections.Generic; +using osu.Game.Rulesets.Mods; +using System.Linq; namespace osu.Game.Screens.Select.Details { public class AdvancedStats : Container { + [Resolved] + private IBindable> mods { get; set; } + private readonly StatisticRow firstValue, hpDrain, accuracy, approachRate, starDifficulty; private BeatmapInfo beatmap; @@ -30,22 +37,7 @@ namespace osu.Game.Screens.Select.Details beatmap = value; - //mania specific - if ((Beatmap?.Ruleset?.ID ?? 0) == 3) - { - firstValue.Title = "Key Amount"; - firstValue.Value = (int)MathF.Round(Beatmap?.BaseDifficulty?.CircleSize ?? 0); - } - else - { - firstValue.Title = "Circle Size"; - firstValue.Value = Beatmap?.BaseDifficulty?.CircleSize ?? 0; - } - - hpDrain.Value = Beatmap?.BaseDifficulty?.DrainRate ?? 0; - accuracy.Value = Beatmap?.BaseDifficulty?.OverallDifficulty ?? 0; - approachRate.Value = Beatmap?.BaseDifficulty?.ApproachRate ?? 0; - starDifficulty.Value = (float)(Beatmap?.StarDifficulty ?? 0); + updateStatistics(); } } @@ -71,6 +63,46 @@ namespace osu.Game.Screens.Select.Details private void load(OsuColour colours) { starDifficulty.AccentColour = colours.Yellow; + mods.ValueChanged += _ => updateStatistics(); + } + + private void updateStatistics() + { + BeatmapInfo processed = Beatmap?.Clone(); + + if (processed != null && mods.Value.Any(m => m is IApplicableToDifficulty)) + { + processed.BaseDifficulty = processed.BaseDifficulty.Clone(); + + foreach (var mod in mods.Value.OfType()) + mod.ApplyToDifficulty(processed.BaseDifficulty); + } + + BeatmapDifficulty baseDifficulty = Beatmap?.BaseDifficulty; + BeatmapDifficulty moddedDifficulty = processed?.BaseDifficulty; + + //mania specific + if ((processed?.Ruleset?.ID ?? 0) == 3) + { + firstValue.Title = "Key Amount"; + firstValue.BaseValue = (int)MathF.Round(baseDifficulty?.CircleSize ?? 0); + firstValue.ModdedValue = (int)MathF.Round(moddedDifficulty?.CircleSize ?? 0); + } + else + { + firstValue.Title = "Circle Size"; + firstValue.BaseValue = baseDifficulty?.CircleSize ?? 0; + firstValue.ModdedValue = moddedDifficulty?.CircleSize ?? 0; + } + + hpDrain.BaseValue = baseDifficulty?.DrainRate ?? 0; + accuracy.BaseValue = baseDifficulty?.OverallDifficulty ?? 0; + approachRate.BaseValue = baseDifficulty?.ApproachRate ?? 0; + starDifficulty.BaseValue = (float)(processed?.StarDifficulty ?? 0); + + hpDrain.ModdedValue = moddedDifficulty?.DrainRate ?? 0; + accuracy.ModdedValue = moddedDifficulty?.OverallDifficulty ?? 0; + approachRate.ModdedValue = moddedDifficulty?.ApproachRate ?? 0; } private class StatisticRow : Container, IHasAccentColour @@ -81,7 +113,10 @@ namespace osu.Game.Screens.Select.Details private readonly float maxValue; private readonly bool forceDecimalPlaces; private readonly OsuSpriteText name, value; - private readonly Bar bar; + private readonly Bar bar, modBar; + + [Resolved] + private OsuColour colours { get; set; } public string Title { @@ -89,19 +124,39 @@ namespace osu.Game.Screens.Select.Details set => name.Text = value; } - private float difficultyValue; + private float baseValue; - public float Value + private float moddedValue; + + public float BaseValue { - get => difficultyValue; + get => baseValue; set { - difficultyValue = value; + baseValue = value; bar.Length = value / maxValue; this.value.Text = value.ToString(forceDecimalPlaces ? "0.00" : "0.##"); } } + public float ModdedValue + { + get => moddedValue; + set + { + moddedValue = value; + modBar.Length = value / maxValue; + this.value.Text = value.ToString(forceDecimalPlaces ? "0.00" : "0.##"); + + if (moddedValue > baseValue) + modBar.AccentColour = this.value.Colour = colours.Red; + else if (moddedValue < baseValue) + modBar.AccentColour = this.value.Colour = colours.BlueDark; + else + modBar.AccentColour = this.value.Colour = Color4.White; + } + } + public Color4 AccentColour { get => bar.AccentColour; @@ -135,6 +190,15 @@ namespace osu.Game.Screens.Select.Details BackgroundColour = Color4.White.Opacity(0.5f), Padding = new MarginPadding { Left = name_width + 10, Right = value_width + 10 }, }, + modBar = new Bar + { + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + RelativeSizeAxes = Axes.X, + Alpha = 0.5f, + Height = 5, + Padding = new MarginPadding { Left = name_width + 10, Right = value_width + 10 }, + }, new Container { Anchor = Anchor.TopRight, diff --git a/osu.Game/Tests/Visual/TestPlayer.cs b/osu.Game/Tests/Visual/TestPlayer.cs index 31f6edadec..8e3821f1a0 100644 --- a/osu.Game/Tests/Visual/TestPlayer.cs +++ b/osu.Game/Tests/Visual/TestPlayer.cs @@ -8,13 +8,16 @@ namespace osu.Game.Tests.Visual { public class TestPlayer : Player { - protected override bool PauseOnFocusLost => false; + protected override bool PauseOnFocusLost { get; } public new DrawableRuleset DrawableRuleset => base.DrawableRuleset; - public TestPlayer(bool allowPause = true, bool showResults = true) + public new GameplayClockContainer GameplayClockContainer => base.GameplayClockContainer; + + public TestPlayer(bool allowPause = true, bool showResults = true, bool pauseOnFocusLost = false) : base(allowPause, showResults) { + PauseOnFocusLost = pauseOnFocusLost; } } } 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 @@ namespace osu.Game.Users /// [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 @@ namespace osu.Game.Users.Drawables RelativeSizeAxes = Axes.Both, }; } + + [Resolved(canBeNull: true)] + private RankingsOverlay rankingsOverlay { get; set; } + + protected override bool OnClick(ClickEvent e) + { + rankingsOverlay?.ShowCountry(Country); + return true; + } } }