1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-26 19:32:55 +08:00

Merge branch 'master' into nodesample-inherit

This commit is contained in:
Bartłomiej Dach 2024-07-05 08:12:47 +02:00 committed by GitHub
commit 6907d3d3b5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
38 changed files with 1399 additions and 217 deletions

1
.gitignore vendored
View File

@ -266,6 +266,7 @@ __pycache__/
.idea/**/dictionaries .idea/**/dictionaries
.idea/**/shelf .idea/**/shelf
.idea/*/.idea/projectSettingsUpdater.xml .idea/*/.idea/projectSettingsUpdater.xml
.idea/*/.idea/encodings.xml
# Generated files # Generated files
.idea/**/contentModel.xml .idea/**/contentModel.xml

View File

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding" addBOMForNewFiles="with NO BOM" />
</project>

View File

@ -0,0 +1,36 @@
// 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.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Rulesets.Catch.Beatmaps;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Scoring;
using osu.Game.Tests.Visual;
using osuTK.Input;
namespace osu.Game.Rulesets.Catch.Tests
{
public partial class TestSceneCatchReplayHandling : OsuManualInputManagerTestScene
{
[Test]
public void TestReplayDetach()
{
DrawableCatchRuleset drawableRuleset = null!;
float catcherPosition = 0;
AddStep("create drawable ruleset", () => Child = drawableRuleset = new DrawableCatchRuleset(new CatchRuleset(), new CatchBeatmap(), []));
AddStep("attach replay", () => drawableRuleset.SetReplayScore(new Score()));
AddStep("store catcher position", () => catcherPosition = drawableRuleset.ChildrenOfType<Catcher>().Single().X);
AddStep("hold down left", () => InputManager.PressKey(Key.Left));
AddAssert("catcher didn't move", () => drawableRuleset.ChildrenOfType<Catcher>().Single().X, () => Is.EqualTo(catcherPosition));
AddStep("release left", () => InputManager.ReleaseKey(Key.Left));
AddStep("detach replay", () => drawableRuleset.SetReplayScore(null));
AddStep("hold down left", () => InputManager.PressKey(Key.Left));
AddUntilStep("catcher moved", () => drawableRuleset.ChildrenOfType<Catcher>().Single().X, () => Is.Not.EqualTo(catcherPosition));
AddStep("release left", () => InputManager.ReleaseKey(Key.Left));
}
}
}

View File

@ -192,12 +192,12 @@ namespace osu.Game.Rulesets.Mania.UI
if (press) if (press)
{ {
inputManager?.KeyBindingContainer?.TriggerPressed(Action.Value); inputManager?.KeyBindingContainer.TriggerPressed(Action.Value);
highlightOverlay.FadeTo(0.1f, 80, Easing.OutQuint); highlightOverlay.FadeTo(0.1f, 80, Easing.OutQuint);
} }
else else
{ {
inputManager?.KeyBindingContainer?.TriggerReleased(Action.Value); inputManager?.KeyBindingContainer.TriggerReleased(Action.Value);
highlightOverlay.FadeTo(0, 400, Easing.OutQuint); highlightOverlay.FadeTo(0, 400, Easing.OutQuint);
} }
} }

View File

@ -15,6 +15,7 @@ using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
using osuTK; using osuTK;
using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Tests.Editor namespace osu.Game.Rulesets.Osu.Tests.Editor
{ {
@ -177,6 +178,79 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
addAssertPointPositionChanged(points, i); 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) private void addAssertPointPositionChanged(Vector2[] points, int index)
{ {
AddAssert($"Point at {points.ElementAt(index)} changed", AddAssert($"Point at {points.ElementAt(index)} changed",

View File

@ -2,13 +2,16 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Testing;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders; 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;
using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
@ -57,7 +60,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true); assertPlaced(true);
assertLength(200); assertLength(200);
assertControlPointCount(2); assertControlPointCount(2);
assertControlPointType(0, PathType.LINEAR); assertFinalControlPointType(0, PathType.LINEAR);
} }
[Test] [Test]
@ -71,7 +74,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true); assertPlaced(true);
assertControlPointCount(2); assertControlPointCount(2);
assertControlPointType(0, PathType.LINEAR); assertFinalControlPointType(0, PathType.LINEAR);
} }
[Test] [Test]
@ -89,7 +92,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true); assertPlaced(true);
assertControlPointCount(3); assertControlPointCount(3);
assertControlPointPosition(1, new Vector2(100, 0)); assertControlPointPosition(1, new Vector2(100, 0));
assertControlPointType(0, PathType.PERFECT_CURVE); assertFinalControlPointType(0, PathType.PERFECT_CURVE);
} }
[Test] [Test]
@ -111,7 +114,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertControlPointCount(4); assertControlPointCount(4);
assertControlPointPosition(1, new Vector2(100, 0)); assertControlPointPosition(1, new Vector2(100, 0));
assertControlPointPosition(2, new Vector2(100, 100)); assertControlPointPosition(2, new Vector2(100, 100));
assertControlPointType(0, PathType.BEZIER); assertFinalControlPointType(0, PathType.BEZIER);
} }
[Test] [Test]
@ -130,8 +133,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true); assertPlaced(true);
assertControlPointCount(3); assertControlPointCount(3);
assertControlPointPosition(1, new Vector2(100, 0)); assertControlPointPosition(1, new Vector2(100, 0));
assertControlPointType(0, PathType.LINEAR); assertFinalControlPointType(0, PathType.LINEAR);
assertControlPointType(1, PathType.LINEAR); assertFinalControlPointType(1, PathType.LINEAR);
} }
[Test] [Test]
@ -149,7 +152,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true); assertPlaced(true);
assertControlPointCount(2); assertControlPointCount(2);
assertControlPointType(0, PathType.LINEAR); assertFinalControlPointType(0, PathType.LINEAR);
assertLength(100); assertLength(100);
} }
@ -171,7 +174,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true); assertPlaced(true);
assertControlPointCount(3); assertControlPointCount(3);
assertControlPointType(0, PathType.PERFECT_CURVE); assertFinalControlPointType(0, PathType.PERFECT_CURVE);
} }
[Test] [Test]
@ -195,7 +198,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true); assertPlaced(true);
assertControlPointCount(4); assertControlPointCount(4);
assertControlPointType(0, PathType.BEZIER); assertFinalControlPointType(0, PathType.BEZIER);
} }
[Test] [Test]
@ -215,8 +218,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertControlPointCount(3); assertControlPointCount(3);
assertControlPointPosition(1, new Vector2(100, 0)); assertControlPointPosition(1, new Vector2(100, 0));
assertControlPointPosition(2, new Vector2(100)); assertControlPointPosition(2, new Vector2(100));
assertControlPointType(0, PathType.LINEAR); assertFinalControlPointType(0, PathType.LINEAR);
assertControlPointType(1, PathType.LINEAR); assertFinalControlPointType(1, PathType.LINEAR);
} }
[Test] [Test]
@ -239,8 +242,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertControlPointCount(4); assertControlPointCount(4);
assertControlPointPosition(1, new Vector2(100, 0)); assertControlPointPosition(1, new Vector2(100, 0));
assertControlPointPosition(2, new Vector2(100)); assertControlPointPosition(2, new Vector2(100));
assertControlPointType(0, PathType.LINEAR); assertFinalControlPointType(0, PathType.LINEAR);
assertControlPointType(1, PathType.PERFECT_CURVE); assertFinalControlPointType(1, PathType.PERFECT_CURVE);
} }
[Test] [Test]
@ -268,8 +271,46 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertControlPointPosition(2, new Vector2(100)); assertControlPointPosition(2, new Vector2(100));
assertControlPointPosition(3, new Vector2(200, 100)); assertControlPointPosition(3, new Vector2(200, 100));
assertControlPointPosition(4, new Vector2(200)); assertControlPointPosition(4, new Vector2(200));
assertControlPointType(0, PathType.PERFECT_CURVE); assertFinalControlPointType(0, PathType.PERFECT_CURVE);
assertControlPointType(2, 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] [Test]
@ -293,7 +334,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
addClickStep(MouseButton.Right); addClickStep(MouseButton.Right);
assertPlaced(true); assertPlaced(true);
assertControlPointType(0, PathType.BEZIER); assertFinalControlPointType(0, PathType.BEZIER);
} }
[Test] [Test]
@ -312,11 +353,11 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true); assertPlaced(true);
assertLength(808, tolerance: 10); assertLength(808, tolerance: 10);
assertControlPointCount(5); assertControlPointCount(5);
assertControlPointType(0, PathType.BSpline(4)); assertFinalControlPointType(0, PathType.BSpline(4));
assertControlPointType(1, null); assertFinalControlPointType(1, null);
assertControlPointType(2, null); assertFinalControlPointType(2, null);
assertControlPointType(3, null); assertFinalControlPointType(3, null);
assertControlPointType(4, null); assertFinalControlPointType(4, null);
} }
[Test] [Test]
@ -337,10 +378,10 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true); assertPlaced(true);
assertLength(600, tolerance: 10); assertLength(600, tolerance: 10);
assertControlPointCount(4); assertControlPointCount(4);
assertControlPointType(0, PathType.BSpline(4)); assertFinalControlPointType(0, PathType.BSpline(4));
assertControlPointType(1, PathType.BSpline(4)); assertFinalControlPointType(1, PathType.BSpline(4));
assertControlPointType(2, PathType.BSpline(4)); assertFinalControlPointType(2, PathType.BSpline(4));
assertControlPointType(3, null); assertFinalControlPointType(3, null);
} }
[Test] [Test]
@ -359,7 +400,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true); assertPlaced(true);
assertControlPointCount(3); assertControlPointCount(3);
assertControlPointType(0, PathType.BEZIER); assertFinalControlPointType(0, PathType.BEZIER);
} }
[Test] [Test]
@ -379,7 +420,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true); assertPlaced(true);
assertControlPointCount(3); assertControlPointCount(3);
assertControlPointType(0, PathType.PERFECT_CURVE); assertFinalControlPointType(0, PathType.PERFECT_CURVE);
} }
[Test] [Test]
@ -400,7 +441,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true); assertPlaced(true);
assertControlPointCount(3); assertControlPointCount(3);
assertControlPointType(0, PathType.PERFECT_CURVE); assertFinalControlPointType(0, PathType.PERFECT_CURVE);
} }
[Test] [Test]
@ -421,7 +462,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true); assertPlaced(true);
assertControlPointCount(3); assertControlPointCount(3);
assertControlPointType(0, PathType.BEZIER); assertFinalControlPointType(0, PathType.BEZIER);
} }
[Test] [Test]
@ -438,7 +479,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true); assertPlaced(true);
assertControlPointCount(3); 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))); 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 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) => private void assertControlPointPosition(int index, Vector2 position) =>
AddAssert($"control point {index} at {position}", () => Precision.AlmostEquals(position, getSlider()!.Path.ControlPoints[index].Position, 1)); AddAssert($"control point {index} at {position}", () => Precision.AlmostEquals(position, getSlider()!.Path.ControlPoints[index].Position, 1));

View File

@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
public partial class PathControlPointVisualiser<T> : CompositeDrawable, IKeyBindingHandler<PlatformAction>, IHasContextMenu public partial class PathControlPointVisualiser<T> : CompositeDrawable, IKeyBindingHandler<PlatformAction>, IHasContextMenu
where T : OsuHitObject, IHasPath 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; internal readonly Container<PathControlPointPiece<T>> Pieces;
@ -196,6 +196,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
if (allowSelection) if (allowSelection)
d.RequestSelection = selectionRequested; d.RequestSelection = selectionRequested;
d.ControlPoint.Changed += controlPointChanged;
d.DragStarted = DragStarted; d.DragStarted = DragStarted;
d.DragInProgress = DragInProgress; d.DragInProgress = DragInProgress;
d.DragEnded = DragEnded; d.DragEnded = DragEnded;
@ -209,6 +210,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
foreach (var point in e.OldItems.Cast<PathControlPoint>()) foreach (var point in e.OldItems.Cast<PathControlPoint>())
{ {
point.Changed -= controlPointChanged;
foreach (var piece in Pieces.Where(p => p.ControlPoint == point).ToArray()) foreach (var piece in Pieces.Where(p => p.ControlPoint == point).ToArray())
piece.RemoveAndDisposeImmediately(); 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) protected override bool OnClick(ClickEvent e)
{ {
if (Pieces.Any(piece => piece.IsHovered)) 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) private void selectionRequested(PathControlPointPiece<T> piece, MouseButtonEvent e)
{ {
if (e.Button == MouseButton.Left && inputManager.CurrentState.Keyboard.ControlPressed) if (e.Button == MouseButton.Left && inputManager.CurrentState.Keyboard.ControlPressed)
@ -254,30 +339,38 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
} }
/// <summary> /// <summary>
/// Attempts to set the given control point piece to the given path type. /// Attempts to set all selected control point pieces to the given path type.
/// If that would fail, try to change the path such that it instead succeeds /// If that fails, try to change the path such that it instead succeeds
/// in a UX-friendly way. /// in a UX-friendly way.
/// </summary> /// </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> /// <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); changeHandler?.BeginChange();
int indexInSegment = pointsInSegment.IndexOf(piece.ControlPoint);
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, var pointsInSegment = hitObject.Path.PointsInSegment(p.ControlPoint);
// so we split the segment into one 3-point circular arc segment int indexInSegment = pointsInSegment.IndexOf(p.ControlPoint);
// and one segment of the previous type.
int thirdPointIndex = indexInSegment + 2;
if (pointsInSegment.Count > thirdPointIndex + 1) if (type?.Type == SplineType.PerfectCurve)
pointsInSegment[thirdPointIndex].Type = pointsInSegment[0].Type; {
// 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; EnsureValidPathTypes();
piece.ControlPoint.Type = type;
changeHandler?.EndChange();
} }
[Resolved(CanBeNull = true)] [Resolved(CanBeNull = true)]
@ -290,6 +383,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
private int draggedControlPointIndex; private int draggedControlPointIndex;
private HashSet<PathControlPoint> selectedControlPoints; private HashSet<PathControlPoint> selectedControlPoints;
private List<MenuItem> curveTypeItems;
public void DragStarted(PathControlPoint controlPoint) public void DragStarted(PathControlPoint controlPoint)
{ {
dragStartPositions = hitObject.Path.ControlPoints.Select(point => point.Position).ToArray(); 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(); var splittablePieces = selectedPieces.Where(isSplittable).ToList();
int splittableCount = splittablePieces.Count; 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)); // special inherit case
curveTypeItems.Add(new OsuMenuItemSpacer()); 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)) if (selectedPieces.Any(piece => piece.ControlPoint.Type?.Type == SplineType.Catmull))
{
curveTypeItems.Add(new OsuMenuItemSpacer());
curveTypeItems.Add(createMenuItemForPathType(PathType.CATMULL)); curveTypeItems.Add(createMenuItemForPathType(PathType.CATMULL));
}
var menuItems = new List<MenuItem> var menuItems = new List<MenuItem>
{ {
@ -424,35 +524,42 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
() => DeleteSelected()) () => DeleteSelected())
); );
updateCurveMenuItems();
return menuItems.ToArray(); 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); if (curveTypeItems == null)
int countOfState = Pieces.Where(p => p.IsSelected.Value).Count(p => p.ControlPoint.Type == type); 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)) if (countOfState == totalCount)
updatePathType(p, type); 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(); public CurveTypeMenuItem(PathType? pathType, Action<TernaryState> action)
}); : base(pathType?.Description ?? "Inherit", MenuItemType.Standard, action)
{
if (countOfState == totalCount) PathType = pathType;
item.State.Value = TernaryState.True; }
else if (countOfState > 0)
item.State.Value = TernaryState.Indeterminate;
else
item.State.Value = TernaryState.False;
return item;
} }
} }
} }

View File

@ -40,6 +40,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private PathControlPoint segmentStart; private PathControlPoint segmentStart;
private PathControlPoint cursor; private PathControlPoint cursor;
private int currentSegmentLength; private int currentSegmentLength;
private bool usingCustomSegmentType;
[Resolved(CanBeNull = true)] [Resolved(CanBeNull = true)]
[CanBeNull] [CanBeNull]
@ -149,21 +150,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
case SliderPlacementState.ControlPoints: case SliderPlacementState.ControlPoints:
if (canPlaceNewControlPoint(out var lastPoint)) if (canPlaceNewControlPoint(out var lastPoint))
{ placeNewControlPoint();
// Place a new point by detatching the current cursor.
updateCursor();
cursor = null;
}
else else
{ beginNewSegment(lastPoint);
// Transform the last point into a new segment.
Debug.Assert(lastPoint != null);
segmentStart = lastPoint;
segmentStart.Type = PathType.LINEAR;
currentSegmentLength = 1;
}
break; break;
} }
@ -171,6 +160,18 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
return true; 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) protected override bool OnDragStart(DragStartEvent e)
{ {
if (e.Button != MouseButton.Left) if (e.Button != MouseButton.Left)
@ -223,6 +224,72 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
base.OnMouseUp(e); 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() protected override void Update()
{ {
base.Update(); base.Update();
@ -246,6 +313,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private void updatePathType() private void updatePathType()
{ {
if (usingCustomSegmentType)
{
controlPointVisualiser.EnsureValidPathTypes();
return;
}
if (state == SliderPlacementState.Drawing) if (state == SliderPlacementState.Drawing)
{ {
segmentStart.Type = PathType.BSpline(4); segmentStart.Type = PathType.BSpline(4);
@ -316,6 +389,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
return lastPiece.IsHovered != true; return lastPiece.IsHovered != true;
} }
private void placeNewControlPoint()
{
// Place a new point by detatching the current cursor.
updateCursor();
cursor = null;
}
private void updateSlider() private void updateSlider()
{ {
if (state == SliderPlacementState.Drawing) if (state == SliderPlacementState.Drawing)

View File

@ -15,6 +15,13 @@ namespace osu.Game.Rulesets.Osu.Edit
public SliderCompositionTool() public SliderCompositionTool()
: base(nameof(Slider)) : 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); public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders);

View File

@ -0,0 +1,235 @@
// 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.Linq;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Checks;
using osu.Game.Rulesets.Objects;
using osu.Game.Tests.Beatmaps;
namespace osu.Game.Tests.Editing.Checks
{
[TestFixture]
public class CheckTitleMarkersTest
{
private CheckTitleMarkers check = null!;
private IBeatmap beatmap = null!;
[SetUp]
public void Setup()
{
check = new CheckTitleMarkers();
beatmap = new Beatmap<HitObject>
{
BeatmapInfo = new BeatmapInfo
{
Metadata = new BeatmapMetadata
{
Title = "Egao no Kanata",
TitleUnicode = "エガオノカナタ"
}
}
};
}
[Test]
public void TestNoTitleMarkers()
{
var issues = check.Run(getContext(beatmap)).ToList();
Assert.That(issues, Has.Count.EqualTo(0));
}
[Test]
public void TestTvSizeMarker()
{
beatmap.BeatmapInfo.Metadata.Title += " (TV Size)";
beatmap.BeatmapInfo.Metadata.TitleUnicode += " (TV Size)";
var issues = check.Run(getContext(beatmap)).ToList();
Assert.That(issues, Has.Count.EqualTo(0));
}
[Test]
public void TestMalformedTvSizeMarker()
{
beatmap.BeatmapInfo.Metadata.Title += " (tv size)";
beatmap.BeatmapInfo.Metadata.TitleUnicode += " (tv size)";
var issues = check.Run(getContext(beatmap)).ToList();
Assert.That(issues, Has.Count.EqualTo(2));
Assert.That(issues.Any(issue => issue.Template is CheckTitleMarkers.IssueTemplateIncorrectMarker));
}
[Test]
public void TestGameVerMarker()
{
beatmap.BeatmapInfo.Metadata.Title += " (Game Ver.)";
beatmap.BeatmapInfo.Metadata.TitleUnicode += " (Game Ver.)";
var issues = check.Run(getContext(beatmap)).ToList();
Assert.That(issues, Has.Count.EqualTo(0));
}
[Test]
public void TestMalformedGameVerMarker()
{
beatmap.BeatmapInfo.Metadata.Title += " (game ver.)";
beatmap.BeatmapInfo.Metadata.TitleUnicode += " (game ver.)";
var issues = check.Run(getContext(beatmap)).ToList();
Assert.That(issues, Has.Count.EqualTo(2));
Assert.That(issues.Any(issue => issue.Template is CheckTitleMarkers.IssueTemplateIncorrectMarker));
}
[Test]
public void TestShortVerMarker()
{
beatmap.BeatmapInfo.Metadata.Title += " (Short Ver.)";
beatmap.BeatmapInfo.Metadata.TitleUnicode += " (Short Ver.)";
var issues = check.Run(getContext(beatmap)).ToList();
Assert.That(issues, Has.Count.EqualTo(0));
}
[Test]
public void TestMalformedShortVerMarker()
{
beatmap.BeatmapInfo.Metadata.Title += " (short ver.)";
beatmap.BeatmapInfo.Metadata.TitleUnicode += " (short ver.)";
var issues = check.Run(getContext(beatmap)).ToList();
Assert.That(issues, Has.Count.EqualTo(2));
Assert.That(issues.Any(issue => issue.Template is CheckTitleMarkers.IssueTemplateIncorrectMarker));
}
[Test]
public void TestCutVerMarker()
{
beatmap.BeatmapInfo.Metadata.Title += " (Cut Ver.)";
beatmap.BeatmapInfo.Metadata.TitleUnicode += " (Cut Ver.)";
var issues = check.Run(getContext(beatmap)).ToList();
Assert.That(issues, Has.Count.EqualTo(0));
}
[Test]
public void TestMalformedCutVerMarker()
{
beatmap.BeatmapInfo.Metadata.Title += " (cut ver.)";
beatmap.BeatmapInfo.Metadata.TitleUnicode += " (cut ver.)";
var issues = check.Run(getContext(beatmap)).ToList();
Assert.That(issues, Has.Count.EqualTo(2));
Assert.That(issues.Any(issue => issue.Template is CheckTitleMarkers.IssueTemplateIncorrectMarker));
}
[Test]
public void TestSpedUpVerMarker()
{
beatmap.BeatmapInfo.Metadata.Title += " (Sped Up Ver.)";
beatmap.BeatmapInfo.Metadata.TitleUnicode += " (Sped Up Ver.)";
var issues = check.Run(getContext(beatmap)).ToList();
Assert.That(issues, Has.Count.EqualTo(0));
}
[Test]
public void TestMalformedSpedUpVerMarker()
{
beatmap.BeatmapInfo.Metadata.Title += " (sped up ver.)";
beatmap.BeatmapInfo.Metadata.TitleUnicode += " (sped up ver.)";
var issues = check.Run(getContext(beatmap)).ToList();
Assert.That(issues, Has.Count.EqualTo(2));
Assert.That(issues.Any(issue => issue.Template is CheckTitleMarkers.IssueTemplateIncorrectMarker));
}
[Test]
public void TestNightcoreMixMarker()
{
beatmap.BeatmapInfo.Metadata.Title += " (Nightcore Mix)";
beatmap.BeatmapInfo.Metadata.TitleUnicode += " (Nightcore Mix)";
var issues = check.Run(getContext(beatmap)).ToList();
Assert.That(issues, Has.Count.EqualTo(0));
}
[Test]
public void TestMalformedNightcoreMixMarker()
{
beatmap.BeatmapInfo.Metadata.Title += " (nightcore mix)";
beatmap.BeatmapInfo.Metadata.TitleUnicode += " (nightcore mix)";
var issues = check.Run(getContext(beatmap)).ToList();
Assert.That(issues, Has.Count.EqualTo(2));
Assert.That(issues.Any(issue => issue.Template is CheckTitleMarkers.IssueTemplateIncorrectMarker));
}
[Test]
public void TestSpedUpCutVerMarker()
{
beatmap.BeatmapInfo.Metadata.Title += " (Sped Up & Cut Ver.)";
beatmap.BeatmapInfo.Metadata.TitleUnicode += " (Sped Up & Cut Ver.)";
var issues = check.Run(getContext(beatmap)).ToList();
Assert.That(issues, Has.Count.EqualTo(0));
}
[Test]
public void TestMalformedSpedUpCutVerMarker()
{
beatmap.BeatmapInfo.Metadata.Title += " (sped up & cut ver.)";
beatmap.BeatmapInfo.Metadata.TitleUnicode += " (sped up & cut ver.)";
var issues = check.Run(getContext(beatmap)).ToList();
Assert.That(issues, Has.Count.EqualTo(2));
Assert.That(issues.Any(issue => issue.Template is CheckTitleMarkers.IssueTemplateIncorrectMarker));
}
[Test]
public void TestNightcoreCutVerMarker()
{
beatmap.BeatmapInfo.Metadata.Title += " (Nightcore & Cut Ver.)";
beatmap.BeatmapInfo.Metadata.TitleUnicode += " (Nightcore & Cut Ver.)";
var issues = check.Run(getContext(beatmap)).ToList();
Assert.That(issues, Has.Count.EqualTo(0));
}
[Test]
public void TestMalformedNightcoreCutVerMarker()
{
beatmap.BeatmapInfo.Metadata.Title += " (nightcore & cut ver.)";
beatmap.BeatmapInfo.Metadata.TitleUnicode += " (nightcore & cut ver.)";
var issues = check.Run(getContext(beatmap)).ToList();
Assert.That(issues, Has.Count.EqualTo(2));
Assert.That(issues.Any(issue => issue.Template is CheckTitleMarkers.IssueTemplateIncorrectMarker));
}
private BeatmapVerifierContext getContext(IBeatmap beatmap)
{
return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
}
}
}

View File

@ -11,10 +11,11 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Rooms; using osu.Game.Online.Rooms;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Screens.OnlinePlay.DailyChallenge; using osu.Game.Screens.OnlinePlay.DailyChallenge;
using osu.Game.Tests.Resources; using osu.Game.Screens.OnlinePlay.DailyChallenge.Events;
namespace osu.Game.Tests.Visual.DailyChallenge namespace osu.Game.Tests.Visual.DailyChallenge
{ {
@ -129,19 +130,27 @@ namespace osu.Game.Tests.Visual.DailyChallenge
}); });
AddStep("add normal score", () => AddStep("add normal score", () =>
{ {
var testScore = TestResources.CreateTestScoreInfo(); var ev = new NewScoreEvent(1, new APIUser
testScore.TotalScore = RNG.Next(1_000_000); {
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);
breakdown.AddNewScore(testScore); breakdown.AddNewScore(ev);
}); });
AddStep("add new user best", () => AddStep("add new user best", () =>
{ {
var testScore = TestResources.CreateTestScoreInfo(); var ev = new NewScoreEvent(1, new APIUser
testScore.TotalScore = RNG.Next(1_000_000); {
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))); feed.AddNewScore(ev);
breakdown.AddNewScore(testScore); breakdown.AddNewScore(ev);
}); });
} }

View File

@ -7,8 +7,10 @@ using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Screens.OnlinePlay.DailyChallenge; using osu.Game.Screens.OnlinePlay.DailyChallenge;
using osu.Game.Screens.OnlinePlay.DailyChallenge.Events;
using osu.Game.Tests.Resources; using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Visual.DailyChallenge namespace osu.Game.Tests.Visual.DailyChallenge
@ -50,26 +52,41 @@ namespace osu.Game.Tests.Visual.DailyChallenge
AddStep("add normal score", () => AddStep("add normal score", () =>
{ {
var testScore = TestResources.CreateTestScoreInfo(); var ev = new NewScoreEvent(1, new APIUser
testScore.TotalScore = RNG.Next(1_000_000); {
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", () => 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(); var testScore = TestResources.CreateTestScoreInfo();
testScore.TotalScore = RNG.Next(1_000_000); 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", () => AddStep("add top 10 score", () =>
{ {
var testScore = TestResources.CreateTestScoreInfo(); var ev = new NewScoreEvent(1, new APIUser
testScore.TotalScore = RNG.Next(1_000_000); {
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);
}); });
} }
} }

View File

@ -7,9 +7,10 @@ using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Screens.OnlinePlay.DailyChallenge; using osu.Game.Screens.OnlinePlay.DailyChallenge;
using osu.Game.Tests.Resources; using osu.Game.Screens.OnlinePlay.DailyChallenge.Events;
namespace osu.Game.Tests.Visual.DailyChallenge 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("set initial data", () => breakdown.SetInitialCounts([1, 4, 9, 16, 25, 36, 49, 36, 25, 16, 9, 4, 1]));
AddStep("add new score", () => AddStep("add new score", () =>
{ {
var testScore = TestResources.CreateTestScoreInfo(); var ev = new NewScoreEvent(1, new APIUser
testScore.TotalScore = RNG.Next(1_000_000); {
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);
}); });
} }
} }

View File

@ -12,7 +12,9 @@ using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Overlays.Dialog; using osu.Game.Overlays.Dialog;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Taiko;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Screens.Edit; using osu.Game.Screens.Edit;
using osu.Game.Storyboards; using osu.Game.Storyboards;
@ -169,6 +171,24 @@ namespace osu.Game.Tests.Visual.Editing
AddAssert("stack empty", () => Stack.CurrentScreen == null); 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 switchToDifficulty(Func<BeatmapInfo> difficulty) => AddStep("switch to difficulty", () => Editor.SwitchToDifficulty(difficulty.Invoke()));
private void confirmEditingBeatmap(Func<BeatmapInfo> targetDifficulty) private void confirmEditingBeatmap(Func<BeatmapInfo> targetDifficulty)

View File

@ -16,6 +16,7 @@ using osu.Game.Configuration;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Backgrounds; using osu.Game.Screens.Backgrounds;
using osu.Game.Screens.Edit; using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Components.Timelines.Summary; using osu.Game.Screens.Edit.Components.Timelines.Summary;
@ -224,6 +225,116 @@ namespace osu.Game.Tests.Visual.Editing
AddAssert("time reverted to 00:01:00", () => EditorClock.CurrentTime, () => Is.EqualTo(60_000)); AddAssert("time reverted to 00:01:00", () => EditorClock.CurrentTime, () => Is.EqualTo(60_000));
} }
[Test]
public void TestAutoplayToggle()
{
AddStep("click test gameplay button", () =>
{
var button = Editor.ChildrenOfType<TestGameplayButton>().Single();
InputManager.MoveMouseTo(button);
InputManager.Click(MouseButton.Left);
});
EditorPlayer editorPlayer = null;
AddUntilStep("player pushed", () => (editorPlayer = Stack.CurrentScreen as EditorPlayer) != null);
AddUntilStep("no replay active", () => editorPlayer.ChildrenOfType<DrawableRuleset>().Single().ReplayScore, () => Is.Null);
AddStep("press Tab", () => InputManager.Key(Key.Tab));
AddUntilStep("replay active", () => editorPlayer.ChildrenOfType<DrawableRuleset>().Single().ReplayScore, () => Is.Not.Null);
AddStep("press Tab", () => InputManager.Key(Key.Tab));
AddUntilStep("no replay active", () => editorPlayer.ChildrenOfType<DrawableRuleset>().Single().ReplayScore, () => Is.Null);
AddStep("exit player", () => editorPlayer.Exit());
AddUntilStep("current screen is editor", () => Stack.CurrentScreen is Editor);
}
[Test]
public void TestQuickPause()
{
AddStep("click test gameplay button", () =>
{
var button = Editor.ChildrenOfType<TestGameplayButton>().Single();
InputManager.MoveMouseTo(button);
InputManager.Click(MouseButton.Left);
});
EditorPlayer editorPlayer = null;
AddUntilStep("player pushed", () => (editorPlayer = Stack.CurrentScreen as EditorPlayer) != null);
AddUntilStep("clock running", () => editorPlayer.ChildrenOfType<GameplayClockContainer>().Single().IsPaused.Value, () => Is.False);
AddStep("press Ctrl-P", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.Key(Key.P);
InputManager.ReleaseKey(Key.ControlLeft);
});
AddUntilStep("clock not running", () => editorPlayer.ChildrenOfType<GameplayClockContainer>().Single().IsPaused.Value, () => Is.True);
AddStep("press Ctrl-P", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.Key(Key.P);
InputManager.ReleaseKey(Key.ControlLeft);
});
AddUntilStep("clock running", () => editorPlayer.ChildrenOfType<GameplayClockContainer>().Single().IsPaused.Value, () => Is.False);
AddStep("exit player", () => editorPlayer.Exit());
AddUntilStep("current screen is editor", () => Stack.CurrentScreen is Editor);
}
[Test]
public void TestQuickExitAtInitialPosition()
{
AddStep("seek to 00:01:00", () => EditorClock.Seek(60_000));
AddStep("click test gameplay button", () =>
{
var button = Editor.ChildrenOfType<TestGameplayButton>().Single();
InputManager.MoveMouseTo(button);
InputManager.Click(MouseButton.Left);
});
EditorPlayer editorPlayer = null;
AddUntilStep("player pushed", () => (editorPlayer = Stack.CurrentScreen as EditorPlayer) != null);
GameplayClockContainer gameplayClockContainer = null;
AddStep("fetch gameplay clock", () => gameplayClockContainer = editorPlayer.ChildrenOfType<GameplayClockContainer>().First());
AddUntilStep("gameplay clock running", () => gameplayClockContainer.IsRunning);
// when the gameplay test is entered, the clock is expected to continue from where it was in the main editor...
AddAssert("gameplay time past 00:01:00", () => gameplayClockContainer.CurrentTime >= 60_000);
AddWaitStep("wait some", 5);
AddStep("exit player", () => InputManager.PressKey(Key.F1));
AddUntilStep("current screen is editor", () => Stack.CurrentScreen is Editor);
AddAssert("time reverted to 00:01:00", () => EditorClock.CurrentTime, () => Is.EqualTo(60_000));
}
[Test]
public void TestQuickExitAtCurrentPosition()
{
AddStep("seek to 00:01:00", () => EditorClock.Seek(60_000));
AddStep("click test gameplay button", () =>
{
var button = Editor.ChildrenOfType<TestGameplayButton>().Single();
InputManager.MoveMouseTo(button);
InputManager.Click(MouseButton.Left);
});
EditorPlayer editorPlayer = null;
AddUntilStep("player pushed", () => (editorPlayer = Stack.CurrentScreen as EditorPlayer) != null);
GameplayClockContainer gameplayClockContainer = null;
AddStep("fetch gameplay clock", () => gameplayClockContainer = editorPlayer.ChildrenOfType<GameplayClockContainer>().First());
AddUntilStep("gameplay clock running", () => gameplayClockContainer.IsRunning);
// when the gameplay test is entered, the clock is expected to continue from where it was in the main editor...
AddAssert("gameplay time past 00:01:00", () => gameplayClockContainer.CurrentTime >= 60_000);
AddWaitStep("wait some", 5);
AddStep("exit player", () => InputManager.PressKey(Key.F2));
AddUntilStep("current screen is editor", () => Stack.CurrentScreen is Editor);
AddAssert("time moved forward", () => EditorClock.CurrentTime, () => Is.GreaterThan(60_000));
}
public override void TearDownSteps() public override void TearDownSteps()
{ {
base.TearDownSteps(); base.TearDownSteps();

View File

@ -34,6 +34,7 @@ namespace osu.Game.Input.Bindings
/// </remarks> /// </remarks>
public override IEnumerable<IKeyBinding> DefaultKeyBindings => globalKeyBindings public override IEnumerable<IKeyBinding> DefaultKeyBindings => globalKeyBindings
.Concat(editorKeyBindings) .Concat(editorKeyBindings)
.Concat(editorTestPlayKeyBindings)
.Concat(inGameKeyBindings) .Concat(inGameKeyBindings)
.Concat(replayKeyBindings) .Concat(replayKeyBindings)
.Concat(songSelectKeyBindings) .Concat(songSelectKeyBindings)
@ -68,6 +69,9 @@ namespace osu.Game.Input.Bindings
case GlobalActionCategory.Overlays: case GlobalActionCategory.Overlays:
return overlayKeyBindings; return overlayKeyBindings;
case GlobalActionCategory.EditorTestPlay:
return editorTestPlayKeyBindings;
default: default:
throw new ArgumentOutOfRangeException(nameof(category), category, $"Unexpected {nameof(GlobalActionCategory)}"); throw new ArgumentOutOfRangeException(nameof(category), category, $"Unexpected {nameof(GlobalActionCategory)}");
} }
@ -100,7 +104,6 @@ namespace osu.Game.Input.Bindings
new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.F }, GlobalAction.ToggleFPSDisplay), new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.F }, GlobalAction.ToggleFPSDisplay),
new KeyBinding(new[] { InputKey.Control, InputKey.T }, GlobalAction.ToggleToolbar), new KeyBinding(new[] { InputKey.Control, InputKey.T }, GlobalAction.ToggleToolbar),
new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.S }, GlobalAction.ToggleSkinEditor), new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.S }, GlobalAction.ToggleSkinEditor),
new KeyBinding(new[] { InputKey.Control, InputKey.P }, GlobalAction.ToggleProfile),
new KeyBinding(new[] { InputKey.Control, InputKey.Alt, InputKey.R }, GlobalAction.ResetInputSettings), new KeyBinding(new[] { InputKey.Control, InputKey.Alt, InputKey.R }, GlobalAction.ResetInputSettings),
@ -118,6 +121,7 @@ namespace osu.Game.Input.Bindings
new KeyBinding(new[] { InputKey.Control, InputKey.B }, GlobalAction.ToggleBeatmapListing), new KeyBinding(new[] { InputKey.Control, InputKey.B }, GlobalAction.ToggleBeatmapListing),
new KeyBinding(new[] { InputKey.Control, InputKey.O }, GlobalAction.ToggleSettings), new KeyBinding(new[] { InputKey.Control, InputKey.O }, GlobalAction.ToggleSettings),
new KeyBinding(new[] { InputKey.Control, InputKey.N }, GlobalAction.ToggleNotifications), new KeyBinding(new[] { InputKey.Control, InputKey.N }, GlobalAction.ToggleNotifications),
new KeyBinding(new[] { InputKey.Control, InputKey.P }, GlobalAction.ToggleProfile),
}; };
private static IEnumerable<KeyBinding> editorKeyBindings => new[] private static IEnumerable<KeyBinding> editorKeyBindings => new[]
@ -145,6 +149,14 @@ namespace osu.Game.Input.Bindings
new KeyBinding(new[] { InputKey.Control, InputKey.E }, GlobalAction.EditorToggleScaleControl), new KeyBinding(new[] { InputKey.Control, InputKey.E }, GlobalAction.EditorToggleScaleControl),
}; };
private static IEnumerable<KeyBinding> editorTestPlayKeyBindings => new[]
{
new KeyBinding(new[] { InputKey.Tab }, GlobalAction.EditorTestPlayToggleAutoplay),
new KeyBinding(new[] { InputKey.Control, InputKey.P }, GlobalAction.EditorTestPlayToggleQuickPause),
new KeyBinding(new[] { InputKey.F1 }, GlobalAction.EditorTestPlayQuickExitToInitialTime),
new KeyBinding(new[] { InputKey.F2 }, GlobalAction.EditorTestPlayQuickExitToCurrentTime),
};
private static IEnumerable<KeyBinding> inGameKeyBindings => new[] private static IEnumerable<KeyBinding> inGameKeyBindings => new[]
{ {
new KeyBinding(InputKey.Space, GlobalAction.SkipCutscene), new KeyBinding(InputKey.Space, GlobalAction.SkipCutscene),
@ -432,6 +444,18 @@ namespace osu.Game.Input.Bindings
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorToggleScaleControl))] [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorToggleScaleControl))]
EditorToggleScaleControl, EditorToggleScaleControl,
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorTestPlayToggleAutoplay))]
EditorTestPlayToggleAutoplay,
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorTestPlayToggleQuickPause))]
EditorTestPlayToggleQuickPause,
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorTestPlayQuickExitToInitialTime))]
EditorTestPlayQuickExitToInitialTime,
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorTestPlayQuickExitToCurrentTime))]
EditorTestPlayQuickExitToCurrentTime,
} }
public enum GlobalActionCategory public enum GlobalActionCategory
@ -442,6 +466,7 @@ namespace osu.Game.Input.Bindings
Replay, Replay,
SongSelect, SongSelect,
AudioControl, AudioControl,
Overlays Overlays,
EditorTestPlay,
} }
} }

View File

@ -374,6 +374,26 @@ namespace osu.Game.Localisation
/// </summary> /// </summary>
public static LocalisableString EditorToggleScaleControl => new TranslatableString(getKey(@"editor_toggle_scale_control"), @"Toggle scale control"); public static LocalisableString EditorToggleScaleControl => new TranslatableString(getKey(@"editor_toggle_scale_control"), @"Toggle scale control");
/// <summary>
/// "Toggle autoplay"
/// </summary>
public static LocalisableString EditorTestPlayToggleAutoplay => new TranslatableString(getKey(@"editor_test_play_toggle_autoplay"), @"Toggle autoplay");
/// <summary>
/// "Toggle quick pause"
/// </summary>
public static LocalisableString EditorTestPlayToggleQuickPause => new TranslatableString(getKey(@"editor_test_play_toggle_quick_pause"), @"Toggle quick pause");
/// <summary>
/// "Quick exit to initial time"
/// </summary>
public static LocalisableString EditorTestPlayQuickExitToInitialTime => new TranslatableString(getKey(@"editor_test_play_quick_exit_to_initial_time"), @"Quick exit to initial time");
/// <summary>
/// "Quick exit to current time"
/// </summary>
public static LocalisableString EditorTestPlayQuickExitToCurrentTime => new TranslatableString(getKey(@"editor_test_play_quick_exit_to_current_time"), @"Quick exit to current time");
/// <summary> /// <summary>
/// "Increase mod speed" /// "Increase mod speed"
/// </summary> /// </summary>

View File

@ -49,6 +49,11 @@ namespace osu.Game.Localisation
/// </summary> /// </summary>
public static LocalisableString EditorSection => new TranslatableString(getKey(@"editor_section"), @"Editor"); public static LocalisableString EditorSection => new TranslatableString(getKey(@"editor_section"), @"Editor");
/// <summary>
/// "Editor: Test play"
/// </summary>
public static LocalisableString EditorTestPlaySection => new TranslatableString(getKey(@"editor_test_play_section"), @"Editor: Test play");
/// <summary> /// <summary>
/// "Reset all bindings in section" /// "Reset all bindings in section"
/// </summary> /// </summary>

View File

@ -256,7 +256,7 @@ namespace osu.Game.Online.Metadata
throw new OperationCanceledException(); throw new OperationCanceledException();
Debug.Assert(connection != null); 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() public override async Task DisconnectRequested()

View File

@ -1,12 +1,9 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Audio.Track; using osu.Framework.Audio.Track;
@ -28,7 +25,7 @@ namespace osu.Game.Overlays
public partial class MusicController : CompositeDrawable public partial class MusicController : CompositeDrawable
{ {
[Resolved] [Resolved]
private BeatmapManager beatmaps { get; set; } private BeatmapManager beatmaps { get; set; } = null!;
/// <summary> /// <summary>
/// Point in time after which the current track will be restarted on triggering a "previous track" action. /// Point in time after which the current track will be restarted on triggering a "previous track" action.
@ -49,25 +46,28 @@ namespace osu.Game.Overlays
/// Fired when the global <see cref="WorkingBeatmap"/> has changed. /// Fired when the global <see cref="WorkingBeatmap"/> has changed.
/// Includes direction information for display purposes. /// Includes direction information for display purposes.
/// </summary> /// </summary>
public event Action<WorkingBeatmap, TrackChangeDirection> TrackChanged; public event Action<WorkingBeatmap, TrackChangeDirection>? TrackChanged;
[Resolved] [Resolved]
private IBindable<WorkingBeatmap> beatmap { get; set; } private IBindable<WorkingBeatmap> beatmap { get; set; } = null!;
[Resolved] [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)); public DrawableTrack CurrentTrack { get; private set; } = new DrawableTrack(new TrackVirtual(1000));
[Resolved] [Resolved]
private RealmAccess realm { get; set; } private RealmAccess realm { get; set; } = null!;
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.LoadComplete(); base.LoadComplete();
beatmap.BindValueChanged(b => changeBeatmap(b.NewValue), true); beatmap.BindValueChanged(b =>
{
if (b.NewValue != null)
changeBeatmap(b.NewValue);
}, true);
mods.BindValueChanged(_ => ResetTrackAdjustments(), true); mods.BindValueChanged(_ => ResetTrackAdjustments(), true);
} }
@ -76,6 +76,9 @@ namespace osu.Game.Overlays
/// </summary> /// </summary>
public void ReloadCurrentTrack() public void ReloadCurrentTrack()
{ {
if (current == null)
return;
changeTrack(); changeTrack();
TrackChanged?.Invoke(current, TrackChangeDirection.None); TrackChanged?.Invoke(current, TrackChangeDirection.None);
} }
@ -90,7 +93,7 @@ namespace osu.Game.Overlays
/// </summary> /// </summary>
public bool TrackLoaded => CurrentTrack.TrackLoaded; public bool TrackLoaded => CurrentTrack.TrackLoaded;
private ScheduledDelegate seekDelegate; private ScheduledDelegate? seekDelegate;
public void SeekTo(double position) public void SeekTo(double position)
{ {
@ -192,7 +195,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"/>. /// Play the previous track or restart the current track if it's current time below <see cref="restart_cutoff_point"/>.
/// </summary> /// </summary>
/// <param name="onSuccess">Invoked when the operation has been performed successfully.</param> /// <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(); PreviousTrackResult res = prev();
if (res != PreviousTrackResult.None) if (res != PreviousTrackResult.None)
@ -218,7 +221,7 @@ namespace osu.Game.Overlays
queuedDirection = TrackChangeDirection.Prev; 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(); ?? getBeatmapSets().LastOrDefault();
if (playableSet != null) if (playableSet != null)
@ -236,7 +239,7 @@ namespace osu.Game.Overlays
/// </summary> /// </summary>
/// <param name="onSuccess">Invoked when the operation has been performed successfully.</param> /// <param name="onSuccess">Invoked when the operation has been performed successfully.</param>
/// <returns>A <see cref="ScheduledDelegate"/> of the operation.</returns> /// <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(); bool res = next();
if (res) if (res)
@ -250,7 +253,7 @@ namespace osu.Game.Overlays
queuedDirection = TrackChangeDirection.Next; 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(); ?? getBeatmapSets().FirstOrDefault();
var playableBeatmap = playableSet?.Beatmaps.FirstOrDefault(); var playableBeatmap = playableSet?.Beatmaps.FirstOrDefault();
@ -272,7 +275,7 @@ namespace osu.Game.Overlays
Schedule(() => CurrentTrack.RestartAsync()); Schedule(() => CurrentTrack.RestartAsync());
} }
private WorkingBeatmap current; private WorkingBeatmap? current;
private TrackChangeDirection? queuedDirection; private TrackChangeDirection? queuedDirection;
@ -289,7 +292,7 @@ namespace osu.Game.Overlays
TrackChangeDirection direction = TrackChangeDirection.None; TrackChangeDirection direction = TrackChangeDirection.None;
bool audioEquals = newWorking?.BeatmapInfo?.AudioEquals(current?.BeatmapInfo) == true; bool audioEquals = newWorking.BeatmapInfo?.AudioEquals(current?.BeatmapInfo) == true;
if (current != null) if (current != null)
{ {
@ -304,7 +307,7 @@ namespace osu.Game.Overlays
{ {
// figure out the best direction based on order in playlist. // figure out the best direction based on order in playlist.
int last = getBeatmapSets().AsEnumerable().TakeWhile(b => !b.Equals(current.BeatmapSetInfo)).Count(); 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; direction = last > next ? TrackChangeDirection.Prev : TrackChangeDirection.Next;
} }
@ -361,7 +364,7 @@ namespace osu.Game.Overlays
{ {
// Important to keep this in its own method to avoid inadvertently capturing unnecessary variables in the callback. // Important to keep this in its own method to avoid inadvertently capturing unnecessary variables in the callback.
// Can lead to leaks. // Can lead to leaks.
var queuedTrack = new DrawableTrack(current.LoadTrack()); var queuedTrack = new DrawableTrack(current!.LoadTrack());
queuedTrack.Completed += onTrackCompleted; queuedTrack.Completed += onTrackCompleted;
return queuedTrack; return queuedTrack;
} }
@ -390,7 +393,7 @@ namespace osu.Game.Overlays
} }
} }
private AudioAdjustments modTrackAdjustments; private AudioAdjustments? modTrackAdjustments;
/// <summary> /// <summary>
/// Resets the adjustments currently applied on <see cref="CurrentTrack"/> and applies the mod adjustments if <see cref="ApplyModTrackAdjustments"/> is <c>true</c>. /// Resets the adjustments currently applied on <see cref="CurrentTrack"/> and applies the mod adjustments if <see cref="ApplyModTrackAdjustments"/> is <c>true</c>.

View File

@ -31,6 +31,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
new GlobalKeyBindingsSubsection(InputSettingsStrings.InGameSection, GlobalActionCategory.InGame), new GlobalKeyBindingsSubsection(InputSettingsStrings.InGameSection, GlobalActionCategory.InGame),
new GlobalKeyBindingsSubsection(InputSettingsStrings.ReplaySection, GlobalActionCategory.Replay), new GlobalKeyBindingsSubsection(InputSettingsStrings.ReplaySection, GlobalActionCategory.Replay),
new GlobalKeyBindingsSubsection(InputSettingsStrings.EditorSection, GlobalActionCategory.Editor), new GlobalKeyBindingsSubsection(InputSettingsStrings.EditorSection, GlobalActionCategory.Editor),
new GlobalKeyBindingsSubsection(InputSettingsStrings.EditorTestPlaySection, GlobalActionCategory.EditorTestPlay),
}); });
} }
} }

View File

@ -46,6 +46,9 @@ namespace osu.Game.Rulesets.Edit
// Events // Events
new CheckBreaks(), new CheckBreaks(),
// Metadata
new CheckTitleMarkers(),
}; };
public IEnumerable<Issue> Run(BeatmapVerifierContext context) public IEnumerable<Issue> Run(BeatmapVerifierContext context)

View File

@ -0,0 +1,71 @@
// 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 System.Collections.Generic;
using System.Text.RegularExpressions;
using osu.Game.Rulesets.Edit.Checks.Components;
namespace osu.Game.Rulesets.Edit.Checks
{
public class CheckTitleMarkers : ICheck
{
public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Metadata, "Checks for incorrect formats of (TV Size) / (Game Ver.) / (Short Ver.) / (Cut Ver.) / (Sped Up Ver.) / etc in title.");
public IEnumerable<IssueTemplate> PossibleTemplates => new IssueTemplate[]
{
new IssueTemplateIncorrectMarker(this),
};
private readonly IEnumerable<MarkerCheck> markerChecks =
[
new MarkerCheck(@"(TV Size)", @"(?i)(tv (size|ver))"),
new MarkerCheck(@"(Game Ver.)", @"(?i)(game (size|ver))"),
new MarkerCheck(@"(Short Ver.)", @"(?i)(short (size|ver))"),
new MarkerCheck(@"(Cut Ver.)", @"(?i)(?<!& )(cut (size|ver))"),
new MarkerCheck(@"(Sped Up Ver.)", @"(?i)(?<!& )(sped|speed) ?up ver"),
new MarkerCheck(@"(Nightcore Mix)", @"(?i)(?<!& )(nightcore|night core) (ver|mix)"),
new MarkerCheck(@"(Sped Up & Cut Ver.)", @"(?i)(sped|speed) ?up (ver)? ?& cut (size|ver)"),
new MarkerCheck(@"(Nightcore & Cut Ver.)", @"(?i)(nightcore|night core) (ver|mix)? ?& cut (size|ver)"),
];
public IEnumerable<Issue> Run(BeatmapVerifierContext context)
{
string romanisedTitle = context.Beatmap.Metadata.Title;
string unicodeTitle = context.Beatmap.Metadata.TitleUnicode;
foreach (var check in markerChecks)
{
bool hasRomanisedTitle = unicodeTitle != romanisedTitle;
if (check.AnyRegex.IsMatch(unicodeTitle) && !unicodeTitle.Contains(check.CorrectMarkerFormat, StringComparison.Ordinal))
yield return new IssueTemplateIncorrectMarker(this).Create("Title", check.CorrectMarkerFormat);
if (hasRomanisedTitle && check.AnyRegex.IsMatch(romanisedTitle) && !romanisedTitle.Contains(check.CorrectMarkerFormat, StringComparison.Ordinal))
yield return new IssueTemplateIncorrectMarker(this).Create("Romanised title", check.CorrectMarkerFormat);
}
}
private class MarkerCheck
{
public readonly string CorrectMarkerFormat;
public readonly Regex AnyRegex;
public MarkerCheck(string exact, string anyRegex)
{
CorrectMarkerFormat = exact;
AnyRegex = new Regex(anyRegex, RegexOptions.Compiled);
}
}
public class IssueTemplateIncorrectMarker : IssueTemplate
{
public IssueTemplateIncorrectMarker(ICheck check)
: base(check, IssueType.Problem, "{0} field has an incorrect format of marker {1}")
{
}
public Issue Create(string titleField, string correctMarkerFormat) => new Issue(this, titleField, correctMarkerFormat);
}
}
}

View File

@ -215,14 +215,14 @@ namespace osu.Game.Rulesets.Edit
toolboxCollection.Items = CompositionTools toolboxCollection.Items = CompositionTools
.Prepend(new SelectTool()) .Prepend(new SelectTool())
.Select(t => new RadioButton(t.Name, () => toolSelected(t), t.CreateIcon)) .Select(t => new HitObjectCompositionToolButton(t, () => toolSelected(t)))
.ToList(); .ToList();
foreach (var item in toolboxCollection.Items) foreach (var item in toolboxCollection.Items)
{ {
item.Selected.DisabledChanged += isDisabled => 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;
}; };
} }

View 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;
}
}
}

View File

@ -1,9 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Localisation;
namespace osu.Game.Rulesets.Edit.Tools namespace osu.Game.Rulesets.Edit.Tools
{ {
@ -11,14 +10,16 @@ namespace osu.Game.Rulesets.Edit.Tools
{ {
public readonly string Name; public readonly string Name;
public LocalisableString TooltipText { get; init; }
protected HitObjectCompositionTool(string name) protected HitObjectCompositionTool(string name)
{ {
Name = 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; public override string ToString() => Name;
} }

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System; using System;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
@ -12,6 +10,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Input; using osu.Framework.Input;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Input.StateChanges;
using osu.Framework.Input.StateChanges.Events; using osu.Framework.Input.StateChanges.Events;
using osu.Framework.Input.States; using osu.Framework.Input.States;
using osu.Game.Configuration; using osu.Game.Configuration;
@ -21,6 +20,7 @@ using osu.Game.Input.Handlers;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Play.HUD.ClicksPerSecond; using osu.Game.Screens.Play.HUD.ClicksPerSecond;
using osuTK;
using static osu.Game.Input.Handlers.ReplayInputHandler; using static osu.Game.Input.Handlers.ReplayInputHandler;
namespace osu.Game.Rulesets.UI namespace osu.Game.Rulesets.UI
@ -32,12 +32,12 @@ namespace osu.Game.Rulesets.UI
public readonly KeyBindingContainer<T> KeyBindingContainer; public readonly KeyBindingContainer<T> KeyBindingContainer;
[Resolved(CanBeNull = true)] [Resolved]
private ScoreProcessor scoreProcessor { get; set; } private ScoreProcessor? scoreProcessor { get; set; }
private ReplayRecorder recorder; private ReplayRecorder? recorder;
public ReplayRecorder Recorder public ReplayRecorder? Recorder
{ {
set set
{ {
@ -103,14 +103,23 @@ namespace osu.Game.Rulesets.UI
#region IHasReplayHandler #region IHasReplayHandler
private ReplayInputHandler replayInputHandler; private ReplayInputHandler? replayInputHandler;
public ReplayInputHandler ReplayInputHandler public ReplayInputHandler? ReplayInputHandler
{ {
get => replayInputHandler; get => replayInputHandler;
set set
{ {
if (replayInputHandler != null) RemoveHandler(replayInputHandler); if (replayInputHandler == value)
return;
if (replayInputHandler != null)
RemoveHandler(replayInputHandler);
// ensures that all replay keys are released, that the last replay state is correctly cleared,
// and that all user-pressed keys are released, so that the replay handler may trigger them itself
// setting `UseParentInput` will only sync releases (https://github.com/ppy/osu-framework/blob/17d65f476d51cc5f2aaea818534f8fbac47e5fe6/osu.Framework/Input/PassThroughInputManager.cs#L179-L182)
new ReplayStateReset().Apply(CurrentState, this);
replayInputHandler = value; replayInputHandler = value;
UseParentInput = replayInputHandler == null; UseParentInput = replayInputHandler == null;
@ -124,8 +133,8 @@ namespace osu.Game.Rulesets.UI
#region Setting application (disables etc.) #region Setting application (disables etc.)
private Bindable<bool> mouseDisabled; private Bindable<bool> mouseDisabled = null!;
private Bindable<bool> tapsDisabled; private Bindable<bool> tapsDisabled = null!;
protected override bool Handle(UIEvent e) protected override bool Handle(UIEvent e)
{ {
@ -222,14 +231,34 @@ namespace osu.Game.Rulesets.UI
RealmKeyBindingStore.ClearDuplicateBindings(KeyBindings); RealmKeyBindingStore.ClearDuplicateBindings(KeyBindings);
} }
} }
private class ReplayStateReset : IInput
{
public void Apply(InputState state, IInputStateChangeHandler handler)
{
if (!(state is RulesetInputManagerInputState<T> inputState))
throw new InvalidOperationException($"{nameof(ReplayState<T>)} should only be applied to a {nameof(RulesetInputManagerInputState<T>)}");
new MouseButtonInput([], state.Mouse.Buttons).Apply(state, handler);
new KeyboardKeyInput([], state.Keyboard.Keys).Apply(state, handler);
new TouchInput(Enum.GetValues<TouchSource>().Select(s => new Touch(s, Vector2.Zero)), false).Apply(state, handler);
new JoystickButtonInput([], state.Joystick.Buttons).Apply(state, handler);
new MidiKeyInput(new MidiState(), state.Midi).Apply(state, handler);
new TabletPenButtonInput([], state.Tablet.PenButtons).Apply(state, handler);
new TabletAuxiliaryButtonInput([], state.Tablet.AuxiliaryButtons).Apply(state, handler);
handler.HandleInputStateChange(new ReplayStateChangeEvent<T>(state, this, inputState.LastReplayState?.PressedActions.ToArray() ?? [], []));
inputState.LastReplayState = null;
}
}
} }
public class RulesetInputManagerInputState<T> : InputState public class RulesetInputManagerInputState<T> : InputState
where T : struct where T : struct
{ {
public ReplayState<T> LastReplayState; public ReplayState<T>? LastReplayState;
public RulesetInputManagerInputState(InputState state = null) public RulesetInputManagerInputState(InputState state)
: base(state) : base(state)
{ {
} }

View File

@ -10,9 +10,11 @@ using osu.Framework.Bindables;
using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
@ -25,14 +27,16 @@ namespace osu.Game.Screens.Edit.Components
public partial class PlaybackControl : BottomBarContainer public partial class PlaybackControl : BottomBarContainer
{ {
private IconButton playButton = null!; private IconButton playButton = null!;
private PlaybackSpeedControl playbackSpeedControl = null!;
[Resolved] [Resolved]
private EditorClock editorClock { get; set; } = null!; 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] [BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider) private void load(OverlayColourProvider colourProvider, Editor? editor)
{ {
Background.Colour = colourProvider.Background4; Background.Colour = colourProvider.Background4;
@ -47,31 +51,61 @@ namespace osu.Game.Screens.Edit.Components
Icon = FontAwesome.Regular.PlayCircle, Icon = FontAwesome.Regular.PlayCircle,
Action = togglePause, Action = togglePause,
}, },
new OsuSpriteText playbackSpeedControl = new PlaybackSpeedControl
{ {
Origin = Anchor.BottomLeft, AutoSizeAxes = Axes.Y,
Text = EditorStrings.PlaybackSpeed, RelativeSizeAxes = Axes.X,
RelativePositionAxes = Axes.Y, Padding = new MarginPadding { Left = 45, },
Y = 0.5f, Anchor = Anchor.CentreRight,
Padding = new MarginPadding { Left = 45 } Origin = Anchor.CentreRight,
}, Direction = FillDirection.Vertical,
new Container Children = new Drawable[]
{ {
Anchor = Anchor.BottomLeft, new OsuSpriteText
Origin = Anchor.BottomLeft, {
RelativeSizeAxes = Axes.Both, Text = EditorStrings.PlaybackSpeed,
Height = 0.5f, },
Padding = new MarginPadding { Left = 45 }, new PlaybackTabControl
Child = new PlaybackTabControl { Current = freqAdjust }, {
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) protected override void Dispose(bool isDisposing)
{ {
Track.Value?.RemoveAdjustment(AdjustableProperty.Frequency, freqAdjust); Track.Value?.RemoveAdjustment(AdjustableProperty.Frequency, tempoAdjustment);
base.Dispose(isDisposing); base.Dispose(isDisposing);
} }
@ -109,6 +143,11 @@ namespace osu.Game.Screens.Edit.Components
playButton.Icon = editorClock.IsRunning ? pause_icon : play_icon; 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 partial class PlaybackTabControl : OsuTabControl<double>
{ {
private static readonly double[] tempo_values = { 0.25, 0.5, 0.75, 1 }; 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) protected override bool OnHover(HoverEvent e)
{ {
updateState(); updateState();
return true; return false;
} }
protected override void OnHoverLost(HoverLostEvent e) => updateState(); protected override void OnHoverLost(HoverLostEvent e) => updateState();

View File

@ -24,11 +24,11 @@ namespace osu.Game.Screens.Edit.Components.RadioButtons
/// <summary> /// <summary>
/// A function which creates a drawable icon to represent this item. If null, a sane default should be used. /// A function which creates a drawable icon to represent this item. If null, a sane default should be used.
/// </summary> /// </summary>
public readonly Func<Drawable>? CreateIcon; public readonly Func<Drawable?>? CreateIcon;
private readonly Action? action; 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; Label = label;
CreateIcon = createIcon; CreateIcon = createIcon;

View File

@ -121,7 +121,11 @@ namespace osu.Game.Screens.Edit
scheduledDifficultySwitch = Schedule(() => scheduledDifficultySwitch = Schedule(() =>
{ {
Beatmap.Value = nextBeatmap.Invoke(); var workingBeatmap = nextBeatmap.Invoke();
Ruleset.Value = workingBeatmap.BeatmapInfo.Ruleset;
Beatmap.Value = workingBeatmap;
state = editorState; state = editorState;
// This screen is a weird exception to the rule that nothing after song select changes the global beatmap. // This screen is a weird exception to the rule that nothing after song select changes the global beatmap.

View File

@ -4,17 +4,21 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Input.Bindings;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Users; using osu.Game.Users;
namespace osu.Game.Screens.Edit.GameplayTest namespace osu.Game.Screens.Edit.GameplayTest
{ {
public partial class EditorPlayer : Player public partial class EditorPlayer : Player, IKeyBindingHandler<GlobalAction>
{ {
private readonly Editor editor; private readonly Editor editor;
private readonly EditorState editorState; private readonly EditorState editorState;
@ -133,6 +137,76 @@ namespace osu.Game.Screens.Edit.GameplayTest
protected override bool CheckModsAllowFailure() => false; // never fail. protected override bool CheckModsAllowFailure() => false; // never fail.
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{
if (e.Repeat)
return false;
switch (e.Action)
{
case GlobalAction.EditorTestPlayToggleAutoplay:
toggleAutoplay();
return true;
case GlobalAction.EditorTestPlayToggleQuickPause:
toggleQuickPause();
return true;
case GlobalAction.EditorTestPlayQuickExitToInitialTime:
quickExit(false);
return true;
case GlobalAction.EditorTestPlayQuickExitToCurrentTime:
quickExit(true);
return true;
default:
return false;
}
}
public void OnReleased(KeyBindingReleaseEvent<GlobalAction> e)
{
}
private void toggleAutoplay()
{
if (DrawableRuleset.ReplayScore == null)
{
var autoplay = Ruleset.Value.CreateInstance().GetAutoplayMod();
if (autoplay == null)
return;
var score = autoplay.CreateScoreFromReplayData(GameplayState.Beatmap, [autoplay]);
// remove past frames to prevent replay frame handler from seeking back to start in an attempt to play back the entirety of the replay.
score.Replay.Frames.RemoveAll(f => f.Time <= GameplayClockContainer.CurrentTime);
DrawableRuleset.SetReplayScore(score);
// Without this schedule, the `GlobalCursorDisplay.Update()` machinery will fade the gameplay cursor out, but we still want it to show.
Schedule(() => DrawableRuleset.Cursor?.Show());
}
else
DrawableRuleset.SetReplayScore(null);
}
private void toggleQuickPause()
{
if (GameplayClockContainer.IsPaused.Value)
GameplayClockContainer.Start();
else
GameplayClockContainer.Stop();
}
private void quickExit(bool useCurrentTime)
{
if (useCurrentTime)
editorState.Time = GameplayClockContainer.CurrentTime;
editor.RestoreState(editorState);
this.Exit();
}
public override void OnEntering(ScreenTransitionEvent e) public override void OnEntering(ScreenTransitionEvent e)
{ {
base.OnEntering(e); base.OnEntering(e);

View File

@ -8,21 +8,29 @@ using osu.Framework.Allocation;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Audio.Sample; using osu.Framework.Audio.Sample;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Logging;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Localisation; 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.Online.Rooms;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Screens.OnlinePlay.Components; 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;
using osu.Game.Screens.OnlinePlay.Match.Components; using osu.Game.Screens.OnlinePlay.Match.Components;
using osu.Game.Screens.OnlinePlay.Playlists; using osu.Game.Screens.OnlinePlay.Playlists;
@ -47,6 +55,9 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
private Sample? sampleStart; private Sample? sampleStart;
private IDisposable? userModsSelectOverlayRegistration; private IDisposable? userModsSelectOverlayRegistration;
private DailyChallengeScoreBreakdown breakdown = null!;
private DailyChallengeEventFeed feed = null!;
[Cached] [Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Plum); private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Plum);
@ -68,6 +79,12 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
[Resolved] [Resolved]
private IOverlayManager? overlayManager { get; set; } 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 override bool DisallowExternalBeatmapRulesetChanges => true;
public DailyChallenge(Room room) public DailyChallenge(Room room)
@ -162,9 +179,39 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
{ {
new Drawable?[] new Drawable?[]
{ {
new DailyChallengeTimeRemainingRing new GridContainer
{ {
RelativeSizeAxes = Axes.Both, 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, null,
// Middle column (leaderboard) // Middle column (leaderboard)
@ -275,6 +322,33 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
var allowedMods = playlistItem.AllowedMods.Select(m => m.ToMod(rulesetInstance)); var allowedMods = playlistItem.AllowedMods.Select(m => m.ToMod(rulesetInstance));
userModsSelectOverlay.IsValidMod = m => allowedMods.Any(a => a.GetType() == m.GetType()); 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() protected override void LoadComplete()
@ -294,6 +368,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
var beatmap = beatmapManager.QueryBeatmap(b => b.OnlineID == playlistItem.Beatmap.OnlineID); 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. Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmap); // this will gracefully fall back to dummy beatmap if missing locally.
Ruleset.Value = rulesets.GetRuleset(playlistItem.RulesetID); Ruleset.Value = rulesets.GetRuleset(playlistItem.RulesetID);
applyLoopingToTrack();
} }
public override void OnEntering(ScreenTransitionEvent e) public override void OnEntering(ScreenTransitionEvent e)
@ -303,6 +378,25 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
waves.Show(); waves.Show();
roomManager.JoinRoom(room); roomManager.JoinRoom(room);
applyLoopingToTrack(); 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) public override void OnResuming(ScreenTransitionEvent e)
@ -327,6 +421,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
this.Delay(WaveContainer.DISAPPEAR_DURATION).FadeOut(); this.Delay(WaveContainer.DISAPPEAR_DURATION).FadeOut();
roomManager.PartRoom(); roomManager.PartRoom();
metadataClient.EndWatchingMultiplayerRoom(room.RoomID.Value!.Value).FireAndForget();
return base.OnExiting(e); return base.OnExiting(e);
} }
@ -375,6 +470,9 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
base.Dispose(isDisposing); base.Dispose(isDisposing);
userModsSelectOverlayRegistration?.Dispose(); userModsSelectOverlayRegistration?.Dispose();
if (metadataClient.IsNotNull())
metadataClient.MultiplayerRoomScoreSet -= onRoomScoreSet;
} }
} }
} }

View File

@ -9,8 +9,7 @@ using osu.Framework.Graphics.Containers;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Screens.OnlinePlay.DailyChallenge.Events;
using osu.Game.Scoring;
using osu.Game.Users.Drawables; using osu.Game.Users.Drawables;
using osuTK; using osuTK;
@ -70,8 +69,6 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
} }
} }
public record NewScoreEvent(IScoreInfo Score, int? NewRank);
private partial class DailyChallengeEventFeedFlow : FillFlowContainer private partial class DailyChallengeEventFeedFlow : FillFlowContainer
{ {
public override IEnumerable<Drawable> FlowingChildren => base.FlowingChildren.Reverse(); public override IEnumerable<Drawable> FlowingChildren => base.FlowingChildren.Reverse();
@ -98,8 +95,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
// TODO: cast is temporary, will be removed later new ClickableAvatar(newScore.User)
new ClickableAvatar((APIUser)newScore.Score.User)
{ {
Size = new Vector2(16), Size = new Vector2(16),
Masking = true, 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.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) if (newScore.NewRank != null)
text.AddText($" and achieved rank #{newScore.NewRank.Value:N0}"); text.AddText($" and achieved rank #{newScore.NewRank.Value:N0}");

View File

@ -13,7 +13,7 @@ using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Metadata; using osu.Game.Online.Metadata;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Scoring; using osu.Game.Screens.OnlinePlay.DailyChallenge.Events;
using osuTK; using osuTK;
namespace osu.Game.Screens.OnlinePlay.DailyChallenge 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; bins[targetBin] += 1;
updateCounts(); updateCounts();
var text = new OsuSpriteText var text = new OsuSpriteText
{ {
Text = scoreInfo.TotalScore.ToString(@"N0"), Text = newScoreEvent.TotalScore.ToString(@"N0"),
Anchor = Anchor.TopCentre, Anchor = Anchor.TopCentre,
Origin = Anchor.BottomCentre, Origin = Anchor.BottomCentre,
Font = OsuFont.Default.With(size: 30), Font = OsuFont.Default.With(size: 30),
@ -108,7 +108,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
private void updateCounts() private void updateCounts()
{ {
long max = bins.Max(); long max = Math.Max(bins.Max(), 1);
for (int i = 0; i < bin_count; ++i) for (int i = 0; i < bin_count; ++i)
barsContainer[i].UpdateCounts(bins[i], max); barsContainer[i].UpdateCounts(bins[i], max);
} }

View File

@ -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; }
}
}

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System; using System;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
@ -17,9 +15,9 @@ namespace osu.Game.Skinning
/// </summary> /// </summary>
public partial class BeatmapSkinProvidingContainer : SkinProvidingContainer public partial class BeatmapSkinProvidingContainer : SkinProvidingContainer
{ {
private Bindable<bool> beatmapSkins; private Bindable<bool> beatmapSkins = null!;
private Bindable<bool> beatmapColours; private Bindable<bool> beatmapColours = null!;
private Bindable<bool> beatmapHitsounds; private Bindable<bool> beatmapHitsounds = null!;
protected override bool AllowConfigurationLookup protected override bool AllowConfigurationLookup
{ {
@ -68,11 +66,15 @@ namespace osu.Game.Skinning
} }
private readonly ISkin skin; 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) : base(skin)
{ {
this.skin = skin; this.skin = skin;
this.classicFallback = classicFallback;
} }
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
@ -93,15 +95,27 @@ namespace osu.Game.Skinning
beatmapColours.BindValueChanged(_ => TriggerSourceChanged()); beatmapColours.BindValueChanged(_ => TriggerSourceChanged());
beatmapHitsounds.BindValueChanged(_ => TriggerSourceChanged()); beatmapHitsounds.BindValueChanged(_ => TriggerSourceChanged());
// If the beatmap skin looks to have skinnable resources, add the default classic skin as a fallback opportunity. currentSkin = skins.CurrentSkin.GetBoundCopy();
if (skin is LegacySkinTransformer legacySkin && legacySkin.IsProvidingLegacyResources) currentSkin.BindValueChanged(_ =>
{ {
SetSources(new[] bool userSkinIsLegacy = skins.CurrentSkin.Value is LegacySkin;
{ bool beatmapProvidingResources = skin is LegacySkinTransformer legacySkin && legacySkin.IsProvidingLegacyResources;
skin,
skins.DefaultClassicSkin // 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);
} }
} }
} }

View File

@ -28,25 +28,33 @@ namespace osu.Game.Skinning
protected readonly Ruleset Ruleset; protected readonly Ruleset Ruleset;
protected readonly IBeatmap Beatmap; protected readonly IBeatmap Beatmap;
[CanBeNull]
private readonly ISkin beatmapSkin;
/// <remarks> /// <remarks>
/// This container already re-exposes all parent <see cref="ISkinSource"/> sources in a ruleset-usable form. /// 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. /// Therefore disallow falling back to any parent <see cref="ISkinSource"/> any further.
/// </remarks> /// </remarks>
protected override bool AllowFallingBackToParent => false; 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) public RulesetSkinProvidingContainer(Ruleset ruleset, IBeatmap beatmap, [CanBeNull] ISkin beatmapSkin)
{ {
Ruleset = ruleset; Ruleset = ruleset;
Beatmap = beatmap; 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 Child = Content,
{
RelativeSizeAxes = Axes.Both,
}
}; };
} }

View File

@ -7,6 +7,7 @@ using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio.Sample; using osu.Framework.Audio.Sample;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
@ -201,7 +202,10 @@ namespace osu.Game.Skinning
source.SourceChanged -= TriggerSourceChanged; 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) foreach (var skin in skinSources)
{ {