mirror of
https://github.com/ppy/osu.git
synced 2025-01-28 02:02:53 +08:00
Merge branch 'music-controller-nullability' into audio-ducking-fx
This commit is contained in:
commit
7f84e377ab
@ -15,6 +15,7 @@ using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
{
|
||||
@ -177,6 +178,79 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
addAssertPointPositionChanged(points, i);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestChangingControlPointTypeViaTab()
|
||||
{
|
||||
createVisualiser(true);
|
||||
|
||||
addControlPointStep(new Vector2(200), PathType.LINEAR);
|
||||
addControlPointStep(new Vector2(300));
|
||||
addControlPointStep(new Vector2(500, 300));
|
||||
addControlPointStep(new Vector2(700, 200));
|
||||
addControlPointStep(new Vector2(500, 100));
|
||||
|
||||
AddStep("select first control point", () => visualiser.Pieces[0].IsSelected.Value = true);
|
||||
AddStep("press tab", () => InputManager.Key(Key.Tab));
|
||||
assertControlPointPathType(0, PathType.BEZIER);
|
||||
|
||||
AddStep("press shift-tab", () =>
|
||||
{
|
||||
InputManager.PressKey(Key.LShift);
|
||||
InputManager.Key(Key.Tab);
|
||||
InputManager.ReleaseKey(Key.LShift);
|
||||
});
|
||||
assertControlPointPathType(0, PathType.LINEAR);
|
||||
|
||||
AddStep("press shift-tab", () =>
|
||||
{
|
||||
InputManager.PressKey(Key.LShift);
|
||||
InputManager.Key(Key.Tab);
|
||||
InputManager.ReleaseKey(Key.LShift);
|
||||
});
|
||||
assertControlPointPathType(0, PathType.BSpline(4));
|
||||
|
||||
AddStep("press shift-tab", () =>
|
||||
{
|
||||
InputManager.PressKey(Key.LShift);
|
||||
InputManager.Key(Key.Tab);
|
||||
InputManager.ReleaseKey(Key.LShift);
|
||||
});
|
||||
assertControlPointPathType(0, PathType.PERFECT_CURVE);
|
||||
assertControlPointPathType(2, PathType.BSpline(4));
|
||||
|
||||
AddStep("select third last control point", () =>
|
||||
{
|
||||
visualiser.Pieces[0].IsSelected.Value = false;
|
||||
visualiser.Pieces[2].IsSelected.Value = true;
|
||||
});
|
||||
|
||||
AddStep("press shift-tab", () =>
|
||||
{
|
||||
InputManager.PressKey(Key.LShift);
|
||||
InputManager.Key(Key.Tab);
|
||||
InputManager.ReleaseKey(Key.LShift);
|
||||
});
|
||||
assertControlPointPathType(2, PathType.PERFECT_CURVE);
|
||||
|
||||
AddRepeatStep("press tab", () => InputManager.Key(Key.Tab), 2);
|
||||
assertControlPointPathType(0, PathType.BEZIER);
|
||||
assertControlPointPathType(2, null);
|
||||
|
||||
AddStep("select first and third control points", () =>
|
||||
{
|
||||
visualiser.Pieces[0].IsSelected.Value = true;
|
||||
visualiser.Pieces[2].IsSelected.Value = true;
|
||||
});
|
||||
AddStep("press alt-1", () =>
|
||||
{
|
||||
InputManager.PressKey(Key.AltLeft);
|
||||
InputManager.Key(Key.Number1);
|
||||
InputManager.ReleaseKey(Key.AltLeft);
|
||||
});
|
||||
assertControlPointPathType(0, PathType.LINEAR);
|
||||
assertControlPointPathType(2, PathType.LINEAR);
|
||||
}
|
||||
|
||||
private void addAssertPointPositionChanged(Vector2[] points, int index)
|
||||
{
|
||||
AddAssert($"Point at {points.ElementAt(index)} changed",
|
||||
|
@ -2,13 +2,16 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders;
|
||||
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.Tests.Visual;
|
||||
@ -57,7 +60,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
assertPlaced(true);
|
||||
assertLength(200);
|
||||
assertControlPointCount(2);
|
||||
assertControlPointType(0, PathType.LINEAR);
|
||||
assertFinalControlPointType(0, PathType.LINEAR);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -71,7 +74,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
|
||||
assertPlaced(true);
|
||||
assertControlPointCount(2);
|
||||
assertControlPointType(0, PathType.LINEAR);
|
||||
assertFinalControlPointType(0, PathType.LINEAR);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -89,7 +92,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
assertPlaced(true);
|
||||
assertControlPointCount(3);
|
||||
assertControlPointPosition(1, new Vector2(100, 0));
|
||||
assertControlPointType(0, PathType.PERFECT_CURVE);
|
||||
assertFinalControlPointType(0, PathType.PERFECT_CURVE);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -111,7 +114,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
assertControlPointCount(4);
|
||||
assertControlPointPosition(1, new Vector2(100, 0));
|
||||
assertControlPointPosition(2, new Vector2(100, 100));
|
||||
assertControlPointType(0, PathType.BEZIER);
|
||||
assertFinalControlPointType(0, PathType.BEZIER);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -130,8 +133,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
assertPlaced(true);
|
||||
assertControlPointCount(3);
|
||||
assertControlPointPosition(1, new Vector2(100, 0));
|
||||
assertControlPointType(0, PathType.LINEAR);
|
||||
assertControlPointType(1, PathType.LINEAR);
|
||||
assertFinalControlPointType(0, PathType.LINEAR);
|
||||
assertFinalControlPointType(1, PathType.LINEAR);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -149,7 +152,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
|
||||
assertPlaced(true);
|
||||
assertControlPointCount(2);
|
||||
assertControlPointType(0, PathType.LINEAR);
|
||||
assertFinalControlPointType(0, PathType.LINEAR);
|
||||
assertLength(100);
|
||||
}
|
||||
|
||||
@ -171,7 +174,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
|
||||
assertPlaced(true);
|
||||
assertControlPointCount(3);
|
||||
assertControlPointType(0, PathType.PERFECT_CURVE);
|
||||
assertFinalControlPointType(0, PathType.PERFECT_CURVE);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -195,7 +198,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
|
||||
assertPlaced(true);
|
||||
assertControlPointCount(4);
|
||||
assertControlPointType(0, PathType.BEZIER);
|
||||
assertFinalControlPointType(0, PathType.BEZIER);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -215,8 +218,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
assertControlPointCount(3);
|
||||
assertControlPointPosition(1, new Vector2(100, 0));
|
||||
assertControlPointPosition(2, new Vector2(100));
|
||||
assertControlPointType(0, PathType.LINEAR);
|
||||
assertControlPointType(1, PathType.LINEAR);
|
||||
assertFinalControlPointType(0, PathType.LINEAR);
|
||||
assertFinalControlPointType(1, PathType.LINEAR);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -239,8 +242,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
assertControlPointCount(4);
|
||||
assertControlPointPosition(1, new Vector2(100, 0));
|
||||
assertControlPointPosition(2, new Vector2(100));
|
||||
assertControlPointType(0, PathType.LINEAR);
|
||||
assertControlPointType(1, PathType.PERFECT_CURVE);
|
||||
assertFinalControlPointType(0, PathType.LINEAR);
|
||||
assertFinalControlPointType(1, PathType.PERFECT_CURVE);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -268,8 +271,46 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
assertControlPointPosition(2, new Vector2(100));
|
||||
assertControlPointPosition(3, new Vector2(200, 100));
|
||||
assertControlPointPosition(4, new Vector2(200));
|
||||
assertControlPointType(0, PathType.PERFECT_CURVE);
|
||||
assertControlPointType(2, PathType.PERFECT_CURVE);
|
||||
assertFinalControlPointType(0, PathType.PERFECT_CURVE);
|
||||
assertFinalControlPointType(2, PathType.PERFECT_CURVE);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestManualPathTypeControlViaKeyboard()
|
||||
{
|
||||
addMovementStep(new Vector2(200));
|
||||
addClickStep(MouseButton.Left);
|
||||
|
||||
addMovementStep(new Vector2(300, 200));
|
||||
addClickStep(MouseButton.Left);
|
||||
|
||||
addMovementStep(new Vector2(300));
|
||||
|
||||
assertControlPointTypeDuringPlacement(0, PathType.PERFECT_CURVE);
|
||||
|
||||
AddRepeatStep("press tab", () => InputManager.Key(Key.Tab), 2);
|
||||
assertControlPointTypeDuringPlacement(0, PathType.LINEAR);
|
||||
|
||||
AddStep("press shift-tab", () =>
|
||||
{
|
||||
InputManager.PressKey(Key.ShiftLeft);
|
||||
InputManager.Key(Key.Tab);
|
||||
InputManager.ReleaseKey(Key.ShiftLeft);
|
||||
});
|
||||
assertControlPointTypeDuringPlacement(0, PathType.BSpline(4));
|
||||
|
||||
AddStep("start new segment via S", () => InputManager.Key(Key.S));
|
||||
assertControlPointTypeDuringPlacement(2, PathType.LINEAR);
|
||||
|
||||
addMovementStep(new Vector2(400, 300));
|
||||
addClickStep(MouseButton.Left);
|
||||
|
||||
addMovementStep(new Vector2(400));
|
||||
addClickStep(MouseButton.Right);
|
||||
|
||||
assertPlaced(true);
|
||||
assertFinalControlPointType(0, PathType.BSpline(4));
|
||||
assertFinalControlPointType(2, PathType.PERFECT_CURVE);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -293,7 +334,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
addClickStep(MouseButton.Right);
|
||||
assertPlaced(true);
|
||||
|
||||
assertControlPointType(0, PathType.BEZIER);
|
||||
assertFinalControlPointType(0, PathType.BEZIER);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -312,11 +353,11 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
assertPlaced(true);
|
||||
assertLength(808, tolerance: 10);
|
||||
assertControlPointCount(5);
|
||||
assertControlPointType(0, PathType.BSpline(4));
|
||||
assertControlPointType(1, null);
|
||||
assertControlPointType(2, null);
|
||||
assertControlPointType(3, null);
|
||||
assertControlPointType(4, null);
|
||||
assertFinalControlPointType(0, PathType.BSpline(4));
|
||||
assertFinalControlPointType(1, null);
|
||||
assertFinalControlPointType(2, null);
|
||||
assertFinalControlPointType(3, null);
|
||||
assertFinalControlPointType(4, null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -337,10 +378,10 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
assertPlaced(true);
|
||||
assertLength(600, tolerance: 10);
|
||||
assertControlPointCount(4);
|
||||
assertControlPointType(0, PathType.BSpline(4));
|
||||
assertControlPointType(1, PathType.BSpline(4));
|
||||
assertControlPointType(2, PathType.BSpline(4));
|
||||
assertControlPointType(3, null);
|
||||
assertFinalControlPointType(0, PathType.BSpline(4));
|
||||
assertFinalControlPointType(1, PathType.BSpline(4));
|
||||
assertFinalControlPointType(2, PathType.BSpline(4));
|
||||
assertFinalControlPointType(3, null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -359,7 +400,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
|
||||
assertPlaced(true);
|
||||
assertControlPointCount(3);
|
||||
assertControlPointType(0, PathType.BEZIER);
|
||||
assertFinalControlPointType(0, PathType.BEZIER);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -379,7 +420,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
|
||||
assertPlaced(true);
|
||||
assertControlPointCount(3);
|
||||
assertControlPointType(0, PathType.PERFECT_CURVE);
|
||||
assertFinalControlPointType(0, PathType.PERFECT_CURVE);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -400,7 +441,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
|
||||
assertPlaced(true);
|
||||
assertControlPointCount(3);
|
||||
assertControlPointType(0, PathType.PERFECT_CURVE);
|
||||
assertFinalControlPointType(0, PathType.PERFECT_CURVE);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -421,7 +462,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
|
||||
assertPlaced(true);
|
||||
assertControlPointCount(3);
|
||||
assertControlPointType(0, PathType.BEZIER);
|
||||
assertFinalControlPointType(0, PathType.BEZIER);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -438,7 +479,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
|
||||
assertPlaced(true);
|
||||
assertControlPointCount(3);
|
||||
assertControlPointType(0, PathType.PERFECT_CURVE);
|
||||
assertFinalControlPointType(0, PathType.PERFECT_CURVE);
|
||||
}
|
||||
|
||||
private void addMovementStep(Vector2 position) => AddStep($"move mouse to {position}", () => InputManager.MoveMouseTo(InputManager.ToScreenSpace(position)));
|
||||
@ -454,7 +495,10 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
|
||||
private void assertControlPointCount(int expected) => AddAssert($"has {expected} control points", () => getSlider()!.Path.ControlPoints.Count, () => Is.EqualTo(expected));
|
||||
|
||||
private void assertControlPointType(int index, PathType? type) => AddAssert($"control point {index} is {type?.ToString() ?? "inherit"}", () => getSlider()!.Path.ControlPoints[index].Type, () => Is.EqualTo(type));
|
||||
private void assertControlPointTypeDuringPlacement(int index, PathType? type) => AddAssert($"control point {index} is {type?.ToString() ?? "inherit"}",
|
||||
() => this.ChildrenOfType<PathControlPointPiece<Slider>>().ElementAt(index).ControlPoint.Type, () => Is.EqualTo(type));
|
||||
|
||||
private void assertFinalControlPointType(int index, PathType? type) => AddAssert($"control point {index} is {type?.ToString() ?? "inherit"}", () => getSlider()!.Path.ControlPoints[index].Type, () => Is.EqualTo(type));
|
||||
|
||||
private void assertControlPointPosition(int index, Vector2 position) =>
|
||||
AddAssert($"control point {index} at {position}", () => Precision.AlmostEquals(position, getSlider()!.Path.ControlPoints[index].Position, 1));
|
||||
|
@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
public partial class PathControlPointVisualiser<T> : CompositeDrawable, IKeyBindingHandler<PlatformAction>, IHasContextMenu
|
||||
where T : OsuHitObject, IHasPath
|
||||
{
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; // allow context menu to appear outside of the playfield.
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; // allow context menu to appear outside the playfield.
|
||||
|
||||
internal readonly Container<PathControlPointPiece<T>> Pieces;
|
||||
|
||||
@ -196,6 +196,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
if (allowSelection)
|
||||
d.RequestSelection = selectionRequested;
|
||||
|
||||
d.ControlPoint.Changed += controlPointChanged;
|
||||
d.DragStarted = DragStarted;
|
||||
d.DragInProgress = DragInProgress;
|
||||
d.DragEnded = DragEnded;
|
||||
@ -209,6 +210,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
|
||||
foreach (var point in e.OldItems.Cast<PathControlPoint>())
|
||||
{
|
||||
point.Changed -= controlPointChanged;
|
||||
|
||||
foreach (var piece in Pieces.Where(p => p.ControlPoint == point).ToArray())
|
||||
piece.RemoveAndDisposeImmediately();
|
||||
}
|
||||
@ -217,6 +220,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
}
|
||||
}
|
||||
|
||||
private void controlPointChanged() => updateCurveMenuItems();
|
||||
|
||||
protected override bool OnClick(ClickEvent e)
|
||||
{
|
||||
if (Pieces.Any(piece => piece.IsHovered))
|
||||
@ -245,6 +250,86 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
{
|
||||
}
|
||||
|
||||
// ReSharper disable once StaticMemberInGenericType
|
||||
private static readonly PathType?[] path_types =
|
||||
[
|
||||
PathType.LINEAR,
|
||||
PathType.BEZIER,
|
||||
PathType.PERFECT_CURVE,
|
||||
PathType.BSpline(4),
|
||||
null,
|
||||
];
|
||||
|
||||
protected override bool OnKeyDown(KeyDownEvent e)
|
||||
{
|
||||
if (e.Repeat)
|
||||
return false;
|
||||
|
||||
switch (e.Key)
|
||||
{
|
||||
case Key.Tab:
|
||||
{
|
||||
var selectedPieces = Pieces.Where(p => p.IsSelected.Value).ToArray();
|
||||
if (selectedPieces.Length != 1)
|
||||
return false;
|
||||
|
||||
var selectedPiece = selectedPieces.Single();
|
||||
var selectedPoint = selectedPiece.ControlPoint;
|
||||
|
||||
var validTypes = path_types;
|
||||
|
||||
if (selectedPoint == controlPoints[0])
|
||||
validTypes = validTypes.Where(t => t != null).ToArray();
|
||||
|
||||
int currentTypeIndex = Array.IndexOf(validTypes, selectedPoint.Type);
|
||||
|
||||
if (currentTypeIndex < 0 && e.ShiftPressed)
|
||||
currentTypeIndex = 0;
|
||||
|
||||
changeHandler?.BeginChange();
|
||||
|
||||
do
|
||||
{
|
||||
currentTypeIndex = (validTypes.Length + currentTypeIndex + (e.ShiftPressed ? -1 : 1)) % validTypes.Length;
|
||||
|
||||
updatePathTypeOfSelectedPieces(validTypes[currentTypeIndex]);
|
||||
} while (selectedPoint.Type != validTypes[currentTypeIndex]);
|
||||
|
||||
changeHandler?.EndChange();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
case Key.Number1:
|
||||
case Key.Number2:
|
||||
case Key.Number3:
|
||||
case Key.Number4:
|
||||
case Key.Number5:
|
||||
{
|
||||
if (!e.AltPressed)
|
||||
return false;
|
||||
|
||||
var type = path_types[e.Key - Key.Number1];
|
||||
|
||||
if (Pieces[0].IsSelected.Value && type == null)
|
||||
return false;
|
||||
|
||||
updatePathTypeOfSelectedPieces(type);
|
||||
return true;
|
||||
}
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
foreach (var p in Pieces)
|
||||
p.ControlPoint.Changed -= controlPointChanged;
|
||||
}
|
||||
|
||||
private void selectionRequested(PathControlPointPiece<T> piece, MouseButtonEvent e)
|
||||
{
|
||||
if (e.Button == MouseButton.Left && inputManager.CurrentState.Keyboard.ControlPressed)
|
||||
@ -254,30 +339,38 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to set the given control point piece to the given path type.
|
||||
/// If that would fail, try to change the path such that it instead succeeds
|
||||
/// Attempts to set all selected control point pieces to the given path type.
|
||||
/// If that fails, try to change the path such that it instead succeeds
|
||||
/// in a UX-friendly way.
|
||||
/// </summary>
|
||||
/// <param name="piece">The control point piece that we want to change the path type of.</param>
|
||||
/// <param name="type">The path type we want to assign to the given control point piece.</param>
|
||||
private void updatePathType(PathControlPointPiece<T> piece, PathType? type)
|
||||
private void updatePathTypeOfSelectedPieces(PathType? type)
|
||||
{
|
||||
var pointsInSegment = hitObject.Path.PointsInSegment(piece.ControlPoint);
|
||||
int indexInSegment = pointsInSegment.IndexOf(piece.ControlPoint);
|
||||
changeHandler?.BeginChange();
|
||||
|
||||
if (type?.Type == SplineType.PerfectCurve)
|
||||
foreach (var p in Pieces.Where(p => p.IsSelected.Value))
|
||||
{
|
||||
// Can't always create a circular arc out of 4 or more points,
|
||||
// so we split the segment into one 3-point circular arc segment
|
||||
// and one segment of the previous type.
|
||||
int thirdPointIndex = indexInSegment + 2;
|
||||
var pointsInSegment = hitObject.Path.PointsInSegment(p.ControlPoint);
|
||||
int indexInSegment = pointsInSegment.IndexOf(p.ControlPoint);
|
||||
|
||||
if (pointsInSegment.Count > thirdPointIndex + 1)
|
||||
pointsInSegment[thirdPointIndex].Type = pointsInSegment[0].Type;
|
||||
if (type?.Type == SplineType.PerfectCurve)
|
||||
{
|
||||
// Can't always create a circular arc out of 4 or more points,
|
||||
// so we split the segment into one 3-point circular arc segment
|
||||
// and one segment of the previous type.
|
||||
int thirdPointIndex = indexInSegment + 2;
|
||||
|
||||
if (pointsInSegment.Count > thirdPointIndex + 1)
|
||||
pointsInSegment[thirdPointIndex].Type = pointsInSegment[0].Type;
|
||||
}
|
||||
|
||||
hitObject.Path.ExpectedDistance.Value = null;
|
||||
p.ControlPoint.Type = type;
|
||||
}
|
||||
|
||||
hitObject.Path.ExpectedDistance.Value = null;
|
||||
piece.ControlPoint.Type = type;
|
||||
EnsureValidPathTypes();
|
||||
|
||||
changeHandler?.EndChange();
|
||||
}
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
@ -290,6 +383,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
private int draggedControlPointIndex;
|
||||
private HashSet<PathControlPoint> selectedControlPoints;
|
||||
|
||||
private List<MenuItem> curveTypeItems;
|
||||
|
||||
public void DragStarted(PathControlPoint controlPoint)
|
||||
{
|
||||
dragStartPositions = hitObject.Path.ControlPoints.Select(point => point.Position).ToArray();
|
||||
@ -386,22 +481,27 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
var splittablePieces = selectedPieces.Where(isSplittable).ToList();
|
||||
int splittableCount = splittablePieces.Count;
|
||||
|
||||
List<MenuItem> curveTypeItems = new List<MenuItem>();
|
||||
curveTypeItems = new List<MenuItem>();
|
||||
|
||||
if (!selectedPieces.Contains(Pieces[0]))
|
||||
foreach (PathType? type in path_types)
|
||||
{
|
||||
curveTypeItems.Add(createMenuItemForPathType(null));
|
||||
curveTypeItems.Add(new OsuMenuItemSpacer());
|
||||
// special inherit case
|
||||
if (type == null)
|
||||
{
|
||||
if (selectedPieces.Contains(Pieces[0]))
|
||||
continue;
|
||||
|
||||
curveTypeItems.Add(new OsuMenuItemSpacer());
|
||||
}
|
||||
|
||||
curveTypeItems.Add(createMenuItemForPathType(type));
|
||||
}
|
||||
|
||||
// todo: hide/disable items which aren't valid for selected points
|
||||
curveTypeItems.Add(createMenuItemForPathType(PathType.LINEAR));
|
||||
curveTypeItems.Add(createMenuItemForPathType(PathType.PERFECT_CURVE));
|
||||
curveTypeItems.Add(createMenuItemForPathType(PathType.BEZIER));
|
||||
curveTypeItems.Add(createMenuItemForPathType(PathType.BSpline(4)));
|
||||
|
||||
if (selectedPieces.Any(piece => piece.ControlPoint.Type?.Type == SplineType.Catmull))
|
||||
{
|
||||
curveTypeItems.Add(new OsuMenuItemSpacer());
|
||||
curveTypeItems.Add(createMenuItemForPathType(PathType.CATMULL));
|
||||
}
|
||||
|
||||
var menuItems = new List<MenuItem>
|
||||
{
|
||||
@ -424,35 +524,42 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
() => DeleteSelected())
|
||||
);
|
||||
|
||||
updateCurveMenuItems();
|
||||
|
||||
return menuItems.ToArray();
|
||||
|
||||
CurveTypeMenuItem createMenuItemForPathType(PathType? type) => new CurveTypeMenuItem(type, _ => updatePathTypeOfSelectedPieces(type));
|
||||
}
|
||||
}
|
||||
|
||||
private MenuItem createMenuItemForPathType(PathType? type)
|
||||
private void updateCurveMenuItems()
|
||||
{
|
||||
int totalCount = Pieces.Count(p => p.IsSelected.Value);
|
||||
int countOfState = Pieces.Where(p => p.IsSelected.Value).Count(p => p.ControlPoint.Type == type);
|
||||
if (curveTypeItems == null)
|
||||
return;
|
||||
|
||||
var item = new TernaryStateRadioMenuItem(type?.Description ?? "Inherit", MenuItemType.Standard, _ =>
|
||||
foreach (var item in curveTypeItems.OfType<CurveTypeMenuItem>())
|
||||
{
|
||||
changeHandler?.BeginChange();
|
||||
int totalCount = Pieces.Count(p => p.IsSelected.Value);
|
||||
int countOfState = Pieces.Where(p => p.IsSelected.Value).Count(p => p.ControlPoint.Type == item.PathType);
|
||||
|
||||
foreach (var p in Pieces.Where(p => p.IsSelected.Value))
|
||||
updatePathType(p, type);
|
||||
if (countOfState == totalCount)
|
||||
item.State.Value = TernaryState.True;
|
||||
else if (countOfState > 0)
|
||||
item.State.Value = TernaryState.Indeterminate;
|
||||
else
|
||||
item.State.Value = TernaryState.False;
|
||||
}
|
||||
}
|
||||
|
||||
EnsureValidPathTypes();
|
||||
private class CurveTypeMenuItem : TernaryStateRadioMenuItem
|
||||
{
|
||||
public readonly PathType? PathType;
|
||||
|
||||
changeHandler?.EndChange();
|
||||
});
|
||||
|
||||
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;
|
||||
public CurveTypeMenuItem(PathType? pathType, Action<TernaryState> action)
|
||||
: base(pathType?.Description ?? "Inherit", MenuItemType.Standard, action)
|
||||
{
|
||||
PathType = pathType;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -40,6 +40,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
private PathControlPoint segmentStart;
|
||||
private PathControlPoint cursor;
|
||||
private int currentSegmentLength;
|
||||
private bool usingCustomSegmentType;
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
[CanBeNull]
|
||||
@ -149,21 +150,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
|
||||
case SliderPlacementState.ControlPoints:
|
||||
if (canPlaceNewControlPoint(out var lastPoint))
|
||||
{
|
||||
// Place a new point by detatching the current cursor.
|
||||
updateCursor();
|
||||
cursor = null;
|
||||
}
|
||||
placeNewControlPoint();
|
||||
else
|
||||
{
|
||||
// Transform the last point into a new segment.
|
||||
Debug.Assert(lastPoint != null);
|
||||
|
||||
segmentStart = lastPoint;
|
||||
segmentStart.Type = PathType.LINEAR;
|
||||
|
||||
currentSegmentLength = 1;
|
||||
}
|
||||
beginNewSegment(lastPoint);
|
||||
|
||||
break;
|
||||
}
|
||||
@ -171,6 +160,18 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
return true;
|
||||
}
|
||||
|
||||
private void beginNewSegment(PathControlPoint lastPoint)
|
||||
{
|
||||
// Transform the last point into a new segment.
|
||||
Debug.Assert(lastPoint != null);
|
||||
|
||||
segmentStart = lastPoint;
|
||||
segmentStart.Type = PathType.LINEAR;
|
||||
|
||||
currentSegmentLength = 1;
|
||||
usingCustomSegmentType = false;
|
||||
}
|
||||
|
||||
protected override bool OnDragStart(DragStartEvent e)
|
||||
{
|
||||
if (e.Button != MouseButton.Left)
|
||||
@ -223,6 +224,72 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
base.OnMouseUp(e);
|
||||
}
|
||||
|
||||
private static readonly PathType[] path_types =
|
||||
[
|
||||
PathType.LINEAR,
|
||||
PathType.BEZIER,
|
||||
PathType.PERFECT_CURVE,
|
||||
PathType.BSpline(4),
|
||||
];
|
||||
|
||||
protected override bool OnKeyDown(KeyDownEvent e)
|
||||
{
|
||||
if (e.Repeat)
|
||||
return false;
|
||||
|
||||
if (state != SliderPlacementState.ControlPoints)
|
||||
return false;
|
||||
|
||||
switch (e.Key)
|
||||
{
|
||||
case Key.S:
|
||||
{
|
||||
if (!canPlaceNewControlPoint(out _))
|
||||
return false;
|
||||
|
||||
placeNewControlPoint();
|
||||
var last = HitObject.Path.ControlPoints.Last(p => p != cursor);
|
||||
beginNewSegment(last);
|
||||
return true;
|
||||
}
|
||||
|
||||
case Key.Number1:
|
||||
case Key.Number2:
|
||||
case Key.Number3:
|
||||
case Key.Number4:
|
||||
{
|
||||
if (!e.AltPressed)
|
||||
return false;
|
||||
|
||||
usingCustomSegmentType = true;
|
||||
segmentStart.Type = path_types[e.Key - Key.Number1];
|
||||
controlPointVisualiser.EnsureValidPathTypes();
|
||||
return true;
|
||||
}
|
||||
|
||||
case Key.Tab:
|
||||
{
|
||||
usingCustomSegmentType = true;
|
||||
|
||||
int currentTypeIndex = segmentStart.Type.HasValue ? Array.IndexOf(path_types, segmentStart.Type.Value) : -1;
|
||||
|
||||
if (currentTypeIndex < 0 && e.ShiftPressed)
|
||||
currentTypeIndex = 0;
|
||||
|
||||
do
|
||||
{
|
||||
currentTypeIndex = (path_types.Length + currentTypeIndex + (e.ShiftPressed ? -1 : 1)) % path_types.Length;
|
||||
segmentStart.Type = path_types[currentTypeIndex];
|
||||
controlPointVisualiser.EnsureValidPathTypes();
|
||||
} while (segmentStart.Type != path_types[currentTypeIndex]);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
@ -246,6 +313,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
|
||||
private void updatePathType()
|
||||
{
|
||||
if (usingCustomSegmentType)
|
||||
{
|
||||
controlPointVisualiser.EnsureValidPathTypes();
|
||||
return;
|
||||
}
|
||||
|
||||
if (state == SliderPlacementState.Drawing)
|
||||
{
|
||||
segmentStart.Type = PathType.BSpline(4);
|
||||
@ -316,6 +389,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
return lastPiece.IsHovered != true;
|
||||
}
|
||||
|
||||
private void placeNewControlPoint()
|
||||
{
|
||||
// Place a new point by detatching the current cursor.
|
||||
updateCursor();
|
||||
cursor = null;
|
||||
}
|
||||
|
||||
private void updateSlider()
|
||||
{
|
||||
if (state == SliderPlacementState.Drawing)
|
||||
|
@ -15,6 +15,13 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
public SliderCompositionTool()
|
||||
: base(nameof(Slider))
|
||||
{
|
||||
TooltipText = """
|
||||
Left click for new point.
|
||||
Left click twice or S key for new segment.
|
||||
Tab, Shift-Tab, or Alt-1~4 to change current segment type.
|
||||
Right click to finish.
|
||||
Click and drag for drawing mode.
|
||||
""";
|
||||
}
|
||||
|
||||
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders);
|
||||
|
@ -11,10 +11,11 @@ using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Screens.OnlinePlay.DailyChallenge;
|
||||
using osu.Game.Tests.Resources;
|
||||
using osu.Game.Screens.OnlinePlay.DailyChallenge.Events;
|
||||
|
||||
namespace osu.Game.Tests.Visual.DailyChallenge
|
||||
{
|
||||
@ -129,19 +130,27 @@ namespace osu.Game.Tests.Visual.DailyChallenge
|
||||
});
|
||||
AddStep("add normal score", () =>
|
||||
{
|
||||
var testScore = TestResources.CreateTestScoreInfo();
|
||||
testScore.TotalScore = RNG.Next(1_000_000);
|
||||
var ev = new NewScoreEvent(1, new APIUser
|
||||
{
|
||||
Id = 2,
|
||||
Username = "peppy",
|
||||
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
|
||||
}, RNG.Next(1_000_000), null);
|
||||
|
||||
feed.AddNewScore(new DailyChallengeEventFeed.NewScoreEvent(testScore, null));
|
||||
breakdown.AddNewScore(testScore);
|
||||
feed.AddNewScore(ev);
|
||||
breakdown.AddNewScore(ev);
|
||||
});
|
||||
AddStep("add new user best", () =>
|
||||
{
|
||||
var testScore = TestResources.CreateTestScoreInfo();
|
||||
testScore.TotalScore = RNG.Next(1_000_000);
|
||||
var ev = new NewScoreEvent(1, new APIUser
|
||||
{
|
||||
Id = 2,
|
||||
Username = "peppy",
|
||||
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
|
||||
}, RNG.Next(1_000_000), RNG.Next(1, 1000));
|
||||
|
||||
feed.AddNewScore(new DailyChallengeEventFeed.NewScoreEvent(testScore, RNG.Next(1, 1000)));
|
||||
breakdown.AddNewScore(testScore);
|
||||
feed.AddNewScore(ev);
|
||||
breakdown.AddNewScore(ev);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -7,8 +7,10 @@ using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Screens.OnlinePlay.DailyChallenge;
|
||||
using osu.Game.Screens.OnlinePlay.DailyChallenge.Events;
|
||||
using osu.Game.Tests.Resources;
|
||||
|
||||
namespace osu.Game.Tests.Visual.DailyChallenge
|
||||
@ -50,26 +52,41 @@ namespace osu.Game.Tests.Visual.DailyChallenge
|
||||
|
||||
AddStep("add normal score", () =>
|
||||
{
|
||||
var testScore = TestResources.CreateTestScoreInfo();
|
||||
testScore.TotalScore = RNG.Next(1_000_000);
|
||||
var ev = new NewScoreEvent(1, new APIUser
|
||||
{
|
||||
Id = 2,
|
||||
Username = "peppy",
|
||||
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
|
||||
}, RNG.Next(1_000_000), null);
|
||||
|
||||
feed.AddNewScore(new DailyChallengeEventFeed.NewScoreEvent(testScore, null));
|
||||
feed.AddNewScore(ev);
|
||||
});
|
||||
|
||||
AddStep("add new user best", () =>
|
||||
{
|
||||
var ev = new NewScoreEvent(1, new APIUser
|
||||
{
|
||||
Id = 2,
|
||||
Username = "peppy",
|
||||
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
|
||||
}, RNG.Next(1_000_000), RNG.Next(11, 1000));
|
||||
|
||||
var testScore = TestResources.CreateTestScoreInfo();
|
||||
testScore.TotalScore = RNG.Next(1_000_000);
|
||||
|
||||
feed.AddNewScore(new DailyChallengeEventFeed.NewScoreEvent(testScore, RNG.Next(1, 1000)));
|
||||
feed.AddNewScore(ev);
|
||||
});
|
||||
|
||||
AddStep("add top 10 score", () =>
|
||||
{
|
||||
var testScore = TestResources.CreateTestScoreInfo();
|
||||
testScore.TotalScore = RNG.Next(1_000_000);
|
||||
var ev = new NewScoreEvent(1, new APIUser
|
||||
{
|
||||
Id = 2,
|
||||
Username = "peppy",
|
||||
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
|
||||
}, RNG.Next(1_000_000), RNG.Next(1, 10));
|
||||
|
||||
feed.AddNewScore(new DailyChallengeEventFeed.NewScoreEvent(testScore, RNG.Next(1, 10)));
|
||||
feed.AddNewScore(ev);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -7,9 +7,10 @@ using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Screens.OnlinePlay.DailyChallenge;
|
||||
using osu.Game.Tests.Resources;
|
||||
using osu.Game.Screens.OnlinePlay.DailyChallenge.Events;
|
||||
|
||||
namespace osu.Game.Tests.Visual.DailyChallenge
|
||||
{
|
||||
@ -51,10 +52,14 @@ namespace osu.Game.Tests.Visual.DailyChallenge
|
||||
AddStep("set initial data", () => breakdown.SetInitialCounts([1, 4, 9, 16, 25, 36, 49, 36, 25, 16, 9, 4, 1]));
|
||||
AddStep("add new score", () =>
|
||||
{
|
||||
var testScore = TestResources.CreateTestScoreInfo();
|
||||
testScore.TotalScore = RNG.Next(1_000_000);
|
||||
var ev = new NewScoreEvent(1, new APIUser
|
||||
{
|
||||
Id = 2,
|
||||
Username = "peppy",
|
||||
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
|
||||
}, RNG.Next(1_000_000), null);
|
||||
|
||||
breakdown.AddNewScore(testScore);
|
||||
breakdown.AddNewScore(ev);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -12,7 +12,9 @@ using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Overlays.Dialog;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Catch;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Taiko;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Storyboards;
|
||||
@ -169,6 +171,24 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
AddAssert("stack empty", () => Stack.CurrentScreen == null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSwitchToDifficultyOfAnotherRuleset()
|
||||
{
|
||||
BeatmapInfo targetDifficulty = null;
|
||||
|
||||
AddAssert("ruleset is catch", () => Ruleset.Value.CreateInstance() is CatchRuleset);
|
||||
|
||||
AddStep("set taiko difficulty", () => targetDifficulty = importedBeatmapSet.Beatmaps.First(b => b.Ruleset.OnlineID == 1));
|
||||
switchToDifficulty(() => targetDifficulty);
|
||||
confirmEditingBeatmap(() => targetDifficulty);
|
||||
|
||||
AddAssert("ruleset switched to taiko", () => Ruleset.Value.CreateInstance() is TaikoRuleset);
|
||||
|
||||
AddStep("exit editor forcefully", () => Stack.Exit());
|
||||
// ensure editor loader didn't resume.
|
||||
AddAssert("stack empty", () => Stack.CurrentScreen == null);
|
||||
}
|
||||
|
||||
private void switchToDifficulty(Func<BeatmapInfo> difficulty) => AddStep("switch to difficulty", () => Editor.SwitchToDifficulty(difficulty.Invoke()));
|
||||
|
||||
private void confirmEditingBeatmap(Func<BeatmapInfo> targetDifficulty)
|
||||
|
@ -5,11 +5,13 @@ using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Input.Bindings;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
@ -102,5 +104,56 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
AddStep("change tool to circle", () => InputManager.Key(Key.Number2));
|
||||
AddAssert("slider placed", () => EditorBeatmap.HitObjects.Count, () => Is.EqualTo(1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAutomaticBankAssignment()
|
||||
{
|
||||
AddStep("add object with soft bank", () => EditorBeatmap.Add(new HitCircle
|
||||
{
|
||||
StartTime = 0,
|
||||
Samples =
|
||||
{
|
||||
new HitSampleInfo(name: HitSampleInfo.HIT_NORMAL, bank: HitSampleInfo.BANK_SOFT, volume: 70),
|
||||
new HitSampleInfo(name: HitSampleInfo.HIT_WHISTLE, bank: HitSampleInfo.BANK_SOFT, volume: 70),
|
||||
}
|
||||
}));
|
||||
AddStep("seek to 500", () => EditorClock.Seek(500));
|
||||
AddStep("enable automatic bank assignment", () =>
|
||||
{
|
||||
InputManager.PressKey(Key.LShift);
|
||||
InputManager.Key(Key.Q);
|
||||
InputManager.ReleaseKey(Key.LShift);
|
||||
});
|
||||
AddStep("select circle placement tool", () => InputManager.Key(Key.Number2));
|
||||
AddStep("move mouse to center of playfield", () => InputManager.MoveMouseTo(this.ChildrenOfType<Playfield>().Single()));
|
||||
AddStep("place circle", () => InputManager.Click(MouseButton.Left));
|
||||
AddAssert("circle has soft bank", () => EditorBeatmap.HitObjects[1].Samples.All(s => s.Bank == HitSampleInfo.BANK_SOFT));
|
||||
AddAssert("circle inherited volume", () => EditorBeatmap.HitObjects[1].Samples.All(s => s.Volume == 70));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestVolumeIsInheritedFromLastObject()
|
||||
{
|
||||
AddStep("add object with soft bank", () => EditorBeatmap.Add(new HitCircle
|
||||
{
|
||||
StartTime = 0,
|
||||
Samples =
|
||||
{
|
||||
new HitSampleInfo(name: HitSampleInfo.HIT_NORMAL, bank: HitSampleInfo.BANK_SOFT, volume: 70),
|
||||
}
|
||||
}));
|
||||
AddStep("seek to 500", () => EditorClock.Seek(500));
|
||||
AddStep("select drum bank", () =>
|
||||
{
|
||||
InputManager.PressKey(Key.LShift);
|
||||
InputManager.Key(Key.R);
|
||||
InputManager.ReleaseKey(Key.LShift);
|
||||
});
|
||||
AddStep("select circle placement tool", () => InputManager.Key(Key.Number2));
|
||||
AddStep("move mouse to center of playfield", () => InputManager.MoveMouseTo(this.ChildrenOfType<Playfield>().Single()));
|
||||
AddStep("place circle", () => InputManager.Click(MouseButton.Left));
|
||||
AddAssert("circle has drum bank", () => EditorBeatmap.HitObjects[1].Samples.All(s => s.Bank == HitSampleInfo.BANK_DRUM));
|
||||
AddAssert("circle inherited volume", () => EditorBeatmap.HitObjects[1].Samples.All(s => s.Volume == 70));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -256,7 +256,7 @@ namespace osu.Game.Online.Metadata
|
||||
throw new OperationCanceledException();
|
||||
|
||||
Debug.Assert(connection != null);
|
||||
await connection.InvokeAsync(nameof(IMetadataServer.EndWatchingMultiplayerRoom)).ConfigureAwait(false);
|
||||
await connection.InvokeAsync(nameof(IMetadataServer.EndWatchingMultiplayerRoom), id).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public override async Task DisconnectRequested()
|
||||
|
@ -1,12 +1,9 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Audio.Track;
|
||||
@ -30,7 +27,7 @@ namespace osu.Game.Overlays
|
||||
public partial class MusicController : CompositeDrawable
|
||||
{
|
||||
[Resolved]
|
||||
private BeatmapManager beatmaps { get; set; }
|
||||
private BeatmapManager beatmaps { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Point in time after which the current track will be restarted on triggering a "previous track" action.
|
||||
@ -51,19 +48,18 @@ namespace osu.Game.Overlays
|
||||
/// Fired when the global <see cref="WorkingBeatmap"/> has changed.
|
||||
/// Includes direction information for display purposes.
|
||||
/// </summary>
|
||||
public event Action<WorkingBeatmap, TrackChangeDirection> TrackChanged;
|
||||
public event Action<WorkingBeatmap, TrackChangeDirection>? TrackChanged;
|
||||
|
||||
[Resolved]
|
||||
private IBindable<WorkingBeatmap> beatmap { get; set; }
|
||||
private IBindable<WorkingBeatmap> beatmap { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private IBindable<IReadOnlyList<Mod>> mods { get; set; }
|
||||
private IBindable<IReadOnlyList<Mod>> mods { get; set; } = null!;
|
||||
|
||||
[NotNull]
|
||||
public DrawableTrack CurrentTrack { get; private set; } = new DrawableTrack(new TrackVirtual(1000));
|
||||
|
||||
[Resolved]
|
||||
private RealmAccess realm { get; set; }
|
||||
private RealmAccess realm { get; set; } = null!;
|
||||
|
||||
private AudioFilter audioDuckFilter;
|
||||
private readonly BindableDouble audioDuckVolume = new BindableDouble(1);
|
||||
@ -80,7 +76,11 @@ namespace osu.Game.Overlays
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
beatmap.BindValueChanged(b => changeBeatmap(b.NewValue), true);
|
||||
beatmap.BindValueChanged(b =>
|
||||
{
|
||||
if (b.NewValue != null)
|
||||
changeBeatmap(b.NewValue);
|
||||
}, true);
|
||||
mods.BindValueChanged(_ => ResetTrackAdjustments(), true);
|
||||
}
|
||||
|
||||
@ -89,6 +89,9 @@ namespace osu.Game.Overlays
|
||||
/// </summary>
|
||||
public void ReloadCurrentTrack()
|
||||
{
|
||||
if (current == null)
|
||||
return;
|
||||
|
||||
changeTrack();
|
||||
TrackChanged?.Invoke(current, TrackChangeDirection.None);
|
||||
}
|
||||
@ -103,7 +106,7 @@ namespace osu.Game.Overlays
|
||||
/// </summary>
|
||||
public bool TrackLoaded => CurrentTrack.TrackLoaded;
|
||||
|
||||
private ScheduledDelegate seekDelegate;
|
||||
private ScheduledDelegate? seekDelegate;
|
||||
|
||||
public void SeekTo(double position)
|
||||
{
|
||||
@ -205,7 +208,7 @@ namespace osu.Game.Overlays
|
||||
/// Play the previous track or restart the current track if it's current time below <see cref="restart_cutoff_point"/>.
|
||||
/// </summary>
|
||||
/// <param name="onSuccess">Invoked when the operation has been performed successfully.</param>
|
||||
public void PreviousTrack(Action<PreviousTrackResult> onSuccess = null) => Schedule(() =>
|
||||
public void PreviousTrack(Action<PreviousTrackResult>? onSuccess = null) => Schedule(() =>
|
||||
{
|
||||
PreviousTrackResult res = prev();
|
||||
if (res != PreviousTrackResult.None)
|
||||
@ -231,7 +234,7 @@ namespace osu.Game.Overlays
|
||||
|
||||
queuedDirection = TrackChangeDirection.Prev;
|
||||
|
||||
var playableSet = getBeatmapSets().AsEnumerable().TakeWhile(i => !i.Equals(current.BeatmapSetInfo)).LastOrDefault()
|
||||
var playableSet = getBeatmapSets().AsEnumerable().TakeWhile(i => !i.Equals(current?.BeatmapSetInfo)).LastOrDefault()
|
||||
?? getBeatmapSets().LastOrDefault();
|
||||
|
||||
if (playableSet != null)
|
||||
@ -249,7 +252,7 @@ namespace osu.Game.Overlays
|
||||
/// </summary>
|
||||
/// <param name="onSuccess">Invoked when the operation has been performed successfully.</param>
|
||||
/// <returns>A <see cref="ScheduledDelegate"/> of the operation.</returns>
|
||||
public void NextTrack(Action onSuccess = null) => Schedule(() =>
|
||||
public void NextTrack(Action? onSuccess = null) => Schedule(() =>
|
||||
{
|
||||
bool res = next();
|
||||
if (res)
|
||||
@ -320,7 +323,7 @@ namespace osu.Game.Overlays
|
||||
|
||||
queuedDirection = TrackChangeDirection.Next;
|
||||
|
||||
var playableSet = getBeatmapSets().AsEnumerable().SkipWhile(i => !i.Equals(current.BeatmapSetInfo)).ElementAtOrDefault(1)
|
||||
var playableSet = getBeatmapSets().AsEnumerable().SkipWhile(i => !i.Equals(current?.BeatmapSetInfo)).ElementAtOrDefault(1)
|
||||
?? getBeatmapSets().FirstOrDefault();
|
||||
|
||||
var playableBeatmap = playableSet?.Beatmaps.FirstOrDefault();
|
||||
@ -342,7 +345,7 @@ namespace osu.Game.Overlays
|
||||
Schedule(() => CurrentTrack.RestartAsync());
|
||||
}
|
||||
|
||||
private WorkingBeatmap current;
|
||||
private WorkingBeatmap? current;
|
||||
|
||||
private TrackChangeDirection? queuedDirection;
|
||||
|
||||
@ -359,7 +362,7 @@ namespace osu.Game.Overlays
|
||||
|
||||
TrackChangeDirection direction = TrackChangeDirection.None;
|
||||
|
||||
bool audioEquals = newWorking?.BeatmapInfo?.AudioEquals(current?.BeatmapInfo) == true;
|
||||
bool audioEquals = newWorking.BeatmapInfo?.AudioEquals(current?.BeatmapInfo) == true;
|
||||
|
||||
if (current != null)
|
||||
{
|
||||
@ -374,7 +377,7 @@ namespace osu.Game.Overlays
|
||||
{
|
||||
// figure out the best direction based on order in playlist.
|
||||
int last = getBeatmapSets().AsEnumerable().TakeWhile(b => !b.Equals(current.BeatmapSetInfo)).Count();
|
||||
int next = newWorking == null ? -1 : getBeatmapSets().AsEnumerable().TakeWhile(b => !b.Equals(newWorking.BeatmapSetInfo)).Count();
|
||||
int next = getBeatmapSets().AsEnumerable().TakeWhile(b => !b.Equals(newWorking.BeatmapSetInfo)).Count();
|
||||
|
||||
direction = last > next ? TrackChangeDirection.Prev : TrackChangeDirection.Next;
|
||||
}
|
||||
@ -431,7 +434,7 @@ namespace osu.Game.Overlays
|
||||
{
|
||||
// Important to keep this in its own method to avoid inadvertently capturing unnecessary variables in the callback.
|
||||
// Can lead to leaks.
|
||||
var queuedTrack = new DrawableTrack(current.LoadTrack());
|
||||
var queuedTrack = new DrawableTrack(current!.LoadTrack());
|
||||
queuedTrack.Completed += onTrackCompleted;
|
||||
return queuedTrack;
|
||||
}
|
||||
@ -460,7 +463,7 @@ namespace osu.Game.Overlays
|
||||
}
|
||||
}
|
||||
|
||||
private AudioAdjustments modTrackAdjustments;
|
||||
private AudioAdjustments? modTrackAdjustments;
|
||||
|
||||
/// <summary>
|
||||
/// Resets the adjustments currently applied on <see cref="CurrentTrack"/> and applies the mod adjustments if <see cref="ApplyModTrackAdjustments"/> is <c>true</c>.
|
||||
|
@ -215,14 +215,14 @@ namespace osu.Game.Rulesets.Edit
|
||||
|
||||
toolboxCollection.Items = CompositionTools
|
||||
.Prepend(new SelectTool())
|
||||
.Select(t => new RadioButton(t.Name, () => toolSelected(t), t.CreateIcon))
|
||||
.Select(t => new HitObjectCompositionToolButton(t, () => toolSelected(t)))
|
||||
.ToList();
|
||||
|
||||
foreach (var item in toolboxCollection.Items)
|
||||
{
|
||||
item.Selected.DisabledChanged += isDisabled =>
|
||||
{
|
||||
item.TooltipText = isDisabled ? "Add at least one timing point first!" : string.Empty;
|
||||
item.TooltipText = isDisabled ? "Add at least one timing point first!" : ((HitObjectCompositionToolButton)item).TooltipText;
|
||||
};
|
||||
}
|
||||
|
||||
|
22
osu.Game/Rulesets/Edit/HitObjectCompositionToolButton.cs
Normal file
22
osu.Game/Rulesets/Edit/HitObjectCompositionToolButton.cs
Normal file
@ -0,0 +1,22 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Game.Rulesets.Edit.Tools;
|
||||
using osu.Game.Screens.Edit.Components.RadioButtons;
|
||||
|
||||
namespace osu.Game.Rulesets.Edit
|
||||
{
|
||||
public class HitObjectCompositionToolButton : RadioButton
|
||||
{
|
||||
public HitObjectCompositionTool Tool { get; }
|
||||
|
||||
public HitObjectCompositionToolButton(HitObjectCompositionTool tool, Action? action)
|
||||
: base(tool.Name, action, tool.CreateIcon)
|
||||
{
|
||||
Tool = tool;
|
||||
|
||||
TooltipText = tool.TooltipText;
|
||||
}
|
||||
}
|
||||
}
|
@ -156,12 +156,21 @@ namespace osu.Game.Rulesets.Edit
|
||||
comboInformation.UpdateComboInformation(getPreviousHitObject() as IHasComboInformation);
|
||||
}
|
||||
|
||||
if (AutomaticBankAssignment)
|
||||
var lastHitNormal = getPreviousHitObject()?.Samples?.FirstOrDefault(o => o.Name == HitSampleInfo.HIT_NORMAL);
|
||||
|
||||
if (lastHitNormal != null)
|
||||
{
|
||||
// Take the hitnormal sample of the last hit object
|
||||
var lastHitNormal = getPreviousHitObject()?.Samples?.FirstOrDefault(o => o.Name == HitSampleInfo.HIT_NORMAL);
|
||||
if (lastHitNormal != null)
|
||||
if (AutomaticBankAssignment)
|
||||
{
|
||||
// Take the hitnormal sample of the last hit object
|
||||
HitObject.Samples[0] = lastHitNormal;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Only inherit the volume from the previous hit object
|
||||
for (int i = 0; i < HitObject.Samples.Count; i++)
|
||||
HitObject.Samples[i] = HitObject.Samples[i].With(newVolume: lastHitNormal.Volume);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,9 +1,8 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Localisation;
|
||||
|
||||
namespace osu.Game.Rulesets.Edit.Tools
|
||||
{
|
||||
@ -11,14 +10,16 @@ namespace osu.Game.Rulesets.Edit.Tools
|
||||
{
|
||||
public readonly string Name;
|
||||
|
||||
public LocalisableString TooltipText { get; init; }
|
||||
|
||||
protected HitObjectCompositionTool(string name)
|
||||
{
|
||||
Name = name;
|
||||
}
|
||||
|
||||
public abstract PlacementBlueprint CreatePlacementBlueprint();
|
||||
public abstract PlacementBlueprint? CreatePlacementBlueprint();
|
||||
|
||||
public virtual Drawable CreateIcon() => null;
|
||||
public virtual Drawable? CreateIcon() => null;
|
||||
|
||||
public override string ToString() => Name;
|
||||
}
|
||||
|
@ -10,9 +10,11 @@ using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
@ -25,14 +27,16 @@ namespace osu.Game.Screens.Edit.Components
|
||||
public partial class PlaybackControl : BottomBarContainer
|
||||
{
|
||||
private IconButton playButton = null!;
|
||||
private PlaybackSpeedControl playbackSpeedControl = null!;
|
||||
|
||||
[Resolved]
|
||||
private EditorClock editorClock { get; set; } = null!;
|
||||
|
||||
private readonly BindableNumber<double> freqAdjust = new BindableDouble(1);
|
||||
private readonly Bindable<EditorScreenMode> currentScreenMode = new Bindable<EditorScreenMode>();
|
||||
private readonly BindableNumber<double> tempoAdjustment = new BindableDouble(1);
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OverlayColourProvider colourProvider)
|
||||
private void load(OverlayColourProvider colourProvider, Editor? editor)
|
||||
{
|
||||
Background.Colour = colourProvider.Background4;
|
||||
|
||||
@ -47,31 +51,61 @@ namespace osu.Game.Screens.Edit.Components
|
||||
Icon = FontAwesome.Regular.PlayCircle,
|
||||
Action = togglePause,
|
||||
},
|
||||
new OsuSpriteText
|
||||
playbackSpeedControl = new PlaybackSpeedControl
|
||||
{
|
||||
Origin = Anchor.BottomLeft,
|
||||
Text = EditorStrings.PlaybackSpeed,
|
||||
RelativePositionAxes = Axes.Y,
|
||||
Y = 0.5f,
|
||||
Padding = new MarginPadding { Left = 45 }
|
||||
},
|
||||
new Container
|
||||
{
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Height = 0.5f,
|
||||
Padding = new MarginPadding { Left = 45 },
|
||||
Child = new PlaybackTabControl { Current = freqAdjust },
|
||||
AutoSizeAxes = Axes.Y,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Padding = new MarginPadding { Left = 45, },
|
||||
Anchor = Anchor.CentreRight,
|
||||
Origin = Anchor.CentreRight,
|
||||
Direction = FillDirection.Vertical,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new OsuSpriteText
|
||||
{
|
||||
Text = EditorStrings.PlaybackSpeed,
|
||||
},
|
||||
new PlaybackTabControl
|
||||
{
|
||||
Current = tempoAdjustment,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = 16,
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Track.BindValueChanged(tr => tr.NewValue?.AddAdjustment(AdjustableProperty.Frequency, freqAdjust), true);
|
||||
Track.BindValueChanged(tr => tr.NewValue?.AddAdjustment(AdjustableProperty.Tempo, tempoAdjustment), true);
|
||||
|
||||
if (editor != null)
|
||||
currentScreenMode.BindTo(editor.Mode);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
currentScreenMode.BindValueChanged(_ =>
|
||||
{
|
||||
if (currentScreenMode.Value == EditorScreenMode.Timing)
|
||||
{
|
||||
tempoAdjustment.Value = 1;
|
||||
tempoAdjustment.Disabled = true;
|
||||
playbackSpeedControl.FadeTo(0.5f, 400, Easing.OutQuint);
|
||||
playbackSpeedControl.TooltipText = "Speed adjustment is unavailable in timing mode. Timing at slower speeds is inaccurate due to resampling artifacts.";
|
||||
}
|
||||
else
|
||||
{
|
||||
tempoAdjustment.Disabled = false;
|
||||
playbackSpeedControl.FadeTo(1, 400, Easing.OutQuint);
|
||||
playbackSpeedControl.TooltipText = default;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
Track.Value?.RemoveAdjustment(AdjustableProperty.Frequency, freqAdjust);
|
||||
Track.Value?.RemoveAdjustment(AdjustableProperty.Frequency, tempoAdjustment);
|
||||
|
||||
base.Dispose(isDisposing);
|
||||
}
|
||||
@ -109,6 +143,11 @@ namespace osu.Game.Screens.Edit.Components
|
||||
playButton.Icon = editorClock.IsRunning ? pause_icon : play_icon;
|
||||
}
|
||||
|
||||
private partial class PlaybackSpeedControl : FillFlowContainer, IHasTooltip
|
||||
{
|
||||
public LocalisableString TooltipText { get; set; }
|
||||
}
|
||||
|
||||
private partial class PlaybackTabControl : OsuTabControl<double>
|
||||
{
|
||||
private static readonly double[] tempo_values = { 0.25, 0.5, 0.75, 1 };
|
||||
@ -174,7 +213,7 @@ namespace osu.Game.Screens.Edit.Components
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
{
|
||||
updateState();
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
protected override void OnHoverLost(HoverLostEvent e) => updateState();
|
||||
|
@ -24,11 +24,11 @@ namespace osu.Game.Screens.Edit.Components.RadioButtons
|
||||
/// <summary>
|
||||
/// A function which creates a drawable icon to represent this item. If null, a sane default should be used.
|
||||
/// </summary>
|
||||
public readonly Func<Drawable>? CreateIcon;
|
||||
public readonly Func<Drawable?>? CreateIcon;
|
||||
|
||||
private readonly Action? action;
|
||||
|
||||
public RadioButton(string label, Action? action, Func<Drawable>? createIcon = null)
|
||||
public RadioButton(string label, Action? action, Func<Drawable?>? createIcon = null)
|
||||
{
|
||||
Label = label;
|
||||
CreateIcon = createIcon;
|
||||
|
@ -121,7 +121,11 @@ namespace osu.Game.Screens.Edit
|
||||
|
||||
scheduledDifficultySwitch = Schedule(() =>
|
||||
{
|
||||
Beatmap.Value = nextBeatmap.Invoke();
|
||||
var workingBeatmap = nextBeatmap.Invoke();
|
||||
|
||||
Ruleset.Value = workingBeatmap.BeatmapInfo.Ruleset;
|
||||
Beatmap.Value = workingBeatmap;
|
||||
|
||||
state = editorState;
|
||||
|
||||
// This screen is a weird exception to the rule that nothing after song select changes the global beatmap.
|
||||
|
@ -8,21 +8,29 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Audio.Sample;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Extensions.LocalisationExtensions;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Metadata;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Screens.OnlinePlay.Components;
|
||||
using osu.Game.Screens.OnlinePlay.DailyChallenge.Events;
|
||||
using osu.Game.Screens.OnlinePlay.Match;
|
||||
using osu.Game.Screens.OnlinePlay.Match.Components;
|
||||
using osu.Game.Screens.OnlinePlay.Playlists;
|
||||
@ -47,6 +55,9 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
|
||||
private Sample? sampleStart;
|
||||
private IDisposable? userModsSelectOverlayRegistration;
|
||||
|
||||
private DailyChallengeScoreBreakdown breakdown = null!;
|
||||
private DailyChallengeEventFeed feed = null!;
|
||||
|
||||
[Cached]
|
||||
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Plum);
|
||||
|
||||
@ -68,6 +79,12 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
|
||||
[Resolved]
|
||||
private IOverlayManager? overlayManager { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private MetadataClient metadataClient { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private UserLookupCache userLookupCache { get; set; } = null!;
|
||||
|
||||
public override bool DisallowExternalBeatmapRulesetChanges => true;
|
||||
|
||||
public DailyChallenge(Room room)
|
||||
@ -162,9 +179,39 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
|
||||
{
|
||||
new Drawable?[]
|
||||
{
|
||||
new DailyChallengeTimeRemainingRing
|
||||
new GridContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RowDimensions =
|
||||
[
|
||||
new Dimension(),
|
||||
new Dimension()
|
||||
],
|
||||
Content = new[]
|
||||
{
|
||||
new Drawable[]
|
||||
{
|
||||
new DailyChallengeCarousel
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new DailyChallengeTimeRemainingRing(),
|
||||
breakdown = new DailyChallengeScoreBreakdown(),
|
||||
}
|
||||
}
|
||||
},
|
||||
[
|
||||
feed = new DailyChallengeEventFeed
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
}
|
||||
],
|
||||
},
|
||||
},
|
||||
null,
|
||||
// Middle column (leaderboard)
|
||||
@ -275,6 +322,33 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
|
||||
var allowedMods = playlistItem.AllowedMods.Select(m => m.ToMod(rulesetInstance));
|
||||
userModsSelectOverlay.IsValidMod = m => allowedMods.Any(a => a.GetType() == m.GetType());
|
||||
}
|
||||
|
||||
metadataClient.MultiplayerRoomScoreSet += onRoomScoreSet;
|
||||
}
|
||||
|
||||
private void onRoomScoreSet(MultiplayerRoomScoreSetEvent e)
|
||||
{
|
||||
if (e.RoomID != room.RoomID.Value || e.PlaylistItemID != playlistItem.ID)
|
||||
return;
|
||||
|
||||
userLookupCache.GetUserAsync(e.UserID).ContinueWith(t =>
|
||||
{
|
||||
if (t.Exception != null)
|
||||
{
|
||||
Logger.Log($@"Could not display room score set event: {t.Exception}", LoggingTarget.Network);
|
||||
return;
|
||||
}
|
||||
|
||||
APIUser? user = t.GetResultSafely();
|
||||
if (user == null) return;
|
||||
|
||||
var ev = new NewScoreEvent(e.ScoreID, user, e.TotalScore, e.NewRank);
|
||||
Schedule(() =>
|
||||
{
|
||||
breakdown.AddNewScore(ev);
|
||||
feed.AddNewScore(ev);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
@ -294,6 +368,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
|
||||
var beatmap = beatmapManager.QueryBeatmap(b => b.OnlineID == playlistItem.Beatmap.OnlineID);
|
||||
Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmap); // this will gracefully fall back to dummy beatmap if missing locally.
|
||||
Ruleset.Value = rulesets.GetRuleset(playlistItem.RulesetID);
|
||||
applyLoopingToTrack();
|
||||
}
|
||||
|
||||
public override void OnEntering(ScreenTransitionEvent e)
|
||||
@ -303,6 +378,25 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
|
||||
waves.Show();
|
||||
roomManager.JoinRoom(room);
|
||||
applyLoopingToTrack();
|
||||
|
||||
metadataClient.BeginWatchingMultiplayerRoom(room.RoomID.Value!.Value).ContinueWith(t =>
|
||||
{
|
||||
if (t.Exception != null)
|
||||
{
|
||||
Logger.Error(t.Exception, @"Failed to subscribe to room updates", LoggingTarget.Network);
|
||||
return;
|
||||
}
|
||||
|
||||
MultiplayerPlaylistItemStats[] stats = t.GetResultSafely();
|
||||
var itemStats = stats.SingleOrDefault(item => item.PlaylistItemID == playlistItem.ID);
|
||||
if (itemStats == null) return;
|
||||
|
||||
Schedule(() => breakdown.SetInitialCounts(itemStats.TotalScoreDistribution));
|
||||
});
|
||||
|
||||
beatmapAvailabilityTracker.SelectedItem.Value = playlistItem;
|
||||
beatmapAvailabilityTracker.Availability.BindValueChanged(_ => trySetDailyChallengeBeatmap(), true);
|
||||
userModsSelectOverlay.SelectedItem.Value = playlistItem;
|
||||
}
|
||||
|
||||
public override void OnResuming(ScreenTransitionEvent e)
|
||||
@ -327,6 +421,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
|
||||
this.Delay(WaveContainer.DISAPPEAR_DURATION).FadeOut();
|
||||
|
||||
roomManager.PartRoom();
|
||||
metadataClient.EndWatchingMultiplayerRoom(room.RoomID.Value!.Value).FireAndForget();
|
||||
|
||||
return base.OnExiting(e);
|
||||
}
|
||||
@ -375,6 +470,9 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
userModsSelectOverlayRegistration?.Dispose();
|
||||
|
||||
if (metadataClient.IsNotNull())
|
||||
metadataClient.MultiplayerRoomScoreSet -= onRoomScoreSet;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,8 +9,7 @@ using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.OnlinePlay.DailyChallenge.Events;
|
||||
using osu.Game.Users.Drawables;
|
||||
using osuTK;
|
||||
|
||||
@ -70,8 +69,6 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
|
||||
}
|
||||
}
|
||||
|
||||
public record NewScoreEvent(IScoreInfo Score, int? NewRank);
|
||||
|
||||
private partial class DailyChallengeEventFeedFlow : FillFlowContainer
|
||||
{
|
||||
public override IEnumerable<Drawable> FlowingChildren => base.FlowingChildren.Reverse();
|
||||
@ -98,8 +95,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
// TODO: cast is temporary, will be removed later
|
||||
new ClickableAvatar((APIUser)newScore.Score.User)
|
||||
new ClickableAvatar(newScore.User)
|
||||
{
|
||||
Size = new Vector2(16),
|
||||
Masking = true,
|
||||
@ -117,9 +113,9 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
|
||||
}
|
||||
};
|
||||
|
||||
text.AddUserLink(newScore.Score.User);
|
||||
text.AddUserLink(newScore.User);
|
||||
text.AddText(" got ");
|
||||
text.AddLink($"{newScore.Score.TotalScore:N0} points", () => { }); // TODO: present the score here
|
||||
text.AddLink($"{newScore.TotalScore:N0} points", () => { }); // TODO: present the score here
|
||||
|
||||
if (newScore.NewRank != null)
|
||||
text.AddText($" and achieved rank #{newScore.NewRank.Value:N0}");
|
||||
|
@ -13,7 +13,7 @@ using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.Metadata;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.OnlinePlay.DailyChallenge.Events;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.DailyChallenge
|
||||
@ -67,15 +67,15 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
|
||||
}
|
||||
}
|
||||
|
||||
public void AddNewScore(IScoreInfo scoreInfo)
|
||||
public void AddNewScore(NewScoreEvent newScoreEvent)
|
||||
{
|
||||
int targetBin = (int)Math.Clamp(Math.Floor((float)scoreInfo.TotalScore / 100000), 0, bin_count - 1);
|
||||
int targetBin = (int)Math.Clamp(Math.Floor((float)newScoreEvent.TotalScore / 100000), 0, bin_count - 1);
|
||||
bins[targetBin] += 1;
|
||||
updateCounts();
|
||||
|
||||
var text = new OsuSpriteText
|
||||
{
|
||||
Text = scoreInfo.TotalScore.ToString(@"N0"),
|
||||
Text = newScoreEvent.TotalScore.ToString(@"N0"),
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.BottomCentre,
|
||||
Font = OsuFont.Default.With(size: 30),
|
||||
@ -108,7 +108,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
|
||||
|
||||
private void updateCounts()
|
||||
{
|
||||
long max = bins.Max();
|
||||
long max = Math.Max(bins.Max(), 1);
|
||||
for (int i = 0; i < bin_count; ++i)
|
||||
barsContainer[i].UpdateCounts(bins[i], max);
|
||||
}
|
||||
|
@ -0,0 +1,23 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.DailyChallenge.Events
|
||||
{
|
||||
public class NewScoreEvent
|
||||
{
|
||||
public NewScoreEvent(long scoreID, APIUser user, long totalScore, int? newRank)
|
||||
{
|
||||
ScoreID = scoreID;
|
||||
User = user;
|
||||
TotalScore = totalScore;
|
||||
NewRank = newRank;
|
||||
}
|
||||
|
||||
public long ScoreID { get; }
|
||||
public APIUser User { get; }
|
||||
public long TotalScore { get; }
|
||||
public int? NewRank { get; }
|
||||
}
|
||||
}
|
@ -1,8 +1,6 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
@ -17,9 +15,9 @@ namespace osu.Game.Skinning
|
||||
/// </summary>
|
||||
public partial class BeatmapSkinProvidingContainer : SkinProvidingContainer
|
||||
{
|
||||
private Bindable<bool> beatmapSkins;
|
||||
private Bindable<bool> beatmapColours;
|
||||
private Bindable<bool> beatmapHitsounds;
|
||||
private Bindable<bool> beatmapSkins = null!;
|
||||
private Bindable<bool> beatmapColours = null!;
|
||||
private Bindable<bool> beatmapHitsounds = null!;
|
||||
|
||||
protected override bool AllowConfigurationLookup
|
||||
{
|
||||
@ -68,11 +66,15 @@ namespace osu.Game.Skinning
|
||||
}
|
||||
|
||||
private readonly ISkin skin;
|
||||
private readonly ISkin? classicFallback;
|
||||
|
||||
public BeatmapSkinProvidingContainer(ISkin skin)
|
||||
private Bindable<Skin> currentSkin = null!;
|
||||
|
||||
public BeatmapSkinProvidingContainer(ISkin skin, ISkin? classicFallback = null)
|
||||
: base(skin)
|
||||
{
|
||||
this.skin = skin;
|
||||
this.classicFallback = classicFallback;
|
||||
}
|
||||
|
||||
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
|
||||
@ -93,15 +95,27 @@ namespace osu.Game.Skinning
|
||||
beatmapColours.BindValueChanged(_ => TriggerSourceChanged());
|
||||
beatmapHitsounds.BindValueChanged(_ => TriggerSourceChanged());
|
||||
|
||||
// If the beatmap skin looks to have skinnable resources, add the default classic skin as a fallback opportunity.
|
||||
if (skin is LegacySkinTransformer legacySkin && legacySkin.IsProvidingLegacyResources)
|
||||
currentSkin = skins.CurrentSkin.GetBoundCopy();
|
||||
currentSkin.BindValueChanged(_ =>
|
||||
{
|
||||
SetSources(new[]
|
||||
{
|
||||
skin,
|
||||
skins.DefaultClassicSkin
|
||||
});
|
||||
}
|
||||
bool userSkinIsLegacy = skins.CurrentSkin.Value is LegacySkin;
|
||||
bool beatmapProvidingResources = skin is LegacySkinTransformer legacySkin && legacySkin.IsProvidingLegacyResources;
|
||||
|
||||
// Some beatmaps provide a limited selection of skin elements to add some visual flair.
|
||||
// In stable, these elements will take lookup priority over the selected skin (whether that be a user skin or default).
|
||||
//
|
||||
// To replicate this we need to pay special attention to the fallback order.
|
||||
// If a user has a non-legacy skin (argon, triangles) selected, the game won't normally fall back to a legacy skin.
|
||||
// In turn this can create an unexpected visual experience.
|
||||
//
|
||||
// So here, check what skin the user has selected. If it's already a legacy skin then we don't need to do anything special.
|
||||
// If it isn't, we insert the classic default. Note that this is only done if the beatmap seems to be providing skin elements,
|
||||
// as we only want to override the user's (non-legacy) skin choice when required for beatmap skin visuals.
|
||||
if (!userSkinIsLegacy && beatmapProvidingResources && classicFallback != null)
|
||||
SetSources(new[] { skin, classicFallback });
|
||||
else
|
||||
SetSources(new[] { skin });
|
||||
}, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -28,25 +28,33 @@ namespace osu.Game.Skinning
|
||||
protected readonly Ruleset Ruleset;
|
||||
protected readonly IBeatmap Beatmap;
|
||||
|
||||
[CanBeNull]
|
||||
private readonly ISkin beatmapSkin;
|
||||
|
||||
/// <remarks>
|
||||
/// This container already re-exposes all parent <see cref="ISkinSource"/> sources in a ruleset-usable form.
|
||||
/// Therefore disallow falling back to any parent <see cref="ISkinSource"/> any further.
|
||||
/// </remarks>
|
||||
protected override bool AllowFallingBackToParent => false;
|
||||
|
||||
protected override Container<Drawable> Content { get; }
|
||||
protected override Container<Drawable> Content { get; } = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
};
|
||||
|
||||
public RulesetSkinProvidingContainer(Ruleset ruleset, IBeatmap beatmap, [CanBeNull] ISkin beatmapSkin)
|
||||
{
|
||||
Ruleset = ruleset;
|
||||
Beatmap = beatmap;
|
||||
this.beatmapSkin = beatmapSkin;
|
||||
}
|
||||
|
||||
InternalChild = new BeatmapSkinProvidingContainer(GetRulesetTransformedSkin(beatmapSkin))
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(SkinManager skinManager)
|
||||
{
|
||||
InternalChild = new BeatmapSkinProvidingContainer(GetRulesetTransformedSkin(beatmapSkin), GetRulesetTransformedSkin(skinManager.DefaultClassicSkin))
|
||||
{
|
||||
Child = Content = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
}
|
||||
Child = Content,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -7,6 +7,7 @@ using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio.Sample;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Extensions.TypeExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
@ -201,7 +202,10 @@ namespace osu.Game.Skinning
|
||||
source.SourceChanged -= TriggerSourceChanged;
|
||||
}
|
||||
|
||||
skinSources = sources.Select(skin => (skin, new DisableableSkinSource(skin, this))).ToArray();
|
||||
skinSources = sources
|
||||
// Shouldn't be required after NRT is applied to all calling sources.
|
||||
.Where(skin => skin.IsNotNull())
|
||||
.Select(skin => (skin, new DisableableSkinSource(skin, this))).ToArray();
|
||||
|
||||
foreach (var skin in skinSources)
|
||||
{
|
||||
|
Loading…
Reference in New Issue
Block a user