1
0
mirror of https://github.com/ppy/osu.git synced 2024-09-22 04:07:25 +08:00

Merge branch 'master' into game-handles-links

This commit is contained in:
Dan Balasescu 2019-11-05 12:33:32 +09:00 committed by GitHub
commit 23ad516348
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 602 additions and 203 deletions

View File

@ -16,6 +16,7 @@ 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;
using osuTK; using osuTK;
using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Tests namespace osu.Game.Rulesets.Osu.Tests
{ {
@ -85,6 +86,93 @@ namespace osu.Game.Rulesets.Osu.Tests
checkPositions(); checkPositions();
} }
[Test]
public void TestSingleControlPointSelection()
{
moveMouseToControlPoint(0);
AddStep("click", () => InputManager.Click(MouseButton.Left));
checkControlPointSelected(0, true);
checkControlPointSelected(1, false);
}
[Test]
public void TestSingleControlPointDeselectionViaOtherControlPoint()
{
moveMouseToControlPoint(0);
AddStep("click", () => InputManager.Click(MouseButton.Left));
moveMouseToControlPoint(1);
AddStep("click", () => InputManager.Click(MouseButton.Left));
checkControlPointSelected(0, false);
checkControlPointSelected(1, true);
}
[Test]
public void TestSingleControlPointDeselectionViaClickOutside()
{
moveMouseToControlPoint(0);
AddStep("click", () => InputManager.Click(MouseButton.Left));
AddStep("move mouse outside control point", () => InputManager.MoveMouseTo(drawableObject));
AddStep("click", () => InputManager.Click(MouseButton.Left));
checkControlPointSelected(0, false);
checkControlPointSelected(1, false);
}
[Test]
public void TestMultipleControlPointSelection()
{
moveMouseToControlPoint(0);
AddStep("click", () => InputManager.Click(MouseButton.Left));
moveMouseToControlPoint(1);
AddStep("ctrl + click", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.Click(MouseButton.Left);
InputManager.ReleaseKey(Key.ControlLeft);
});
checkControlPointSelected(0, true);
checkControlPointSelected(1, true);
}
[Test]
public void TestMultipleControlPointDeselectionViaOtherControlPoint()
{
moveMouseToControlPoint(0);
AddStep("click", () => InputManager.Click(MouseButton.Left));
moveMouseToControlPoint(1);
AddStep("ctrl + click", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.Click(MouseButton.Left);
InputManager.ReleaseKey(Key.ControlLeft);
});
moveMouseToControlPoint(2);
AddStep("click", () => InputManager.Click(MouseButton.Left));
checkControlPointSelected(0, false);
checkControlPointSelected(1, false);
}
[Test]
public void TestMultipleControlPointDeselectionViaClickOutside()
{
moveMouseToControlPoint(0);
AddStep("click", () => InputManager.Click(MouseButton.Left));
moveMouseToControlPoint(1);
AddStep("ctrl + click", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.Click(MouseButton.Left);
InputManager.ReleaseKey(Key.ControlLeft);
});
AddStep("move mouse outside control point", () => InputManager.MoveMouseTo(drawableObject));
AddStep("click", () => InputManager.Click(MouseButton.Left));
checkControlPointSelected(0, false);
checkControlPointSelected(1, false);
}
private void moveHitObject() private void moveHitObject()
{ {
AddStep("move hitobject", () => AddStep("move hitobject", () =>
@ -104,11 +192,24 @@ namespace osu.Game.Rulesets.Osu.Tests
() => Precision.AlmostEquals(blueprint.TailBlueprint.CirclePiece.ScreenSpaceDrawQuad.Centre, drawableObject.TailCircle.ScreenSpaceDrawQuad.Centre)); () => Precision.AlmostEquals(blueprint.TailBlueprint.CirclePiece.ScreenSpaceDrawQuad.Centre, drawableObject.TailCircle.ScreenSpaceDrawQuad.Centre));
} }
private void moveMouseToControlPoint(int index)
{
AddStep($"move mouse to control point {index}", () =>
{
Vector2 position = slider.Position + slider.Path.ControlPoints[index];
InputManager.MoveMouseTo(drawableObject.Parent.ToScreenSpace(position));
});
}
private void checkControlPointSelected(int index, bool selected)
=> AddAssert($"control point {index} {(selected ? "selected" : "not selected")}", () => blueprint.ControlPointVisualiser.Pieces[index].IsSelected.Value == selected);
private class TestSliderBlueprint : SliderSelectionBlueprint private class TestSliderBlueprint : SliderSelectionBlueprint
{ {
public new SliderBodyPiece BodyPiece => base.BodyPiece; public new SliderBodyPiece BodyPiece => base.BodyPiece;
public new TestSliderCircleBlueprint HeadBlueprint => (TestSliderCircleBlueprint)base.HeadBlueprint; public new TestSliderCircleBlueprint HeadBlueprint => (TestSliderCircleBlueprint)base.HeadBlueprint;
public new TestSliderCircleBlueprint TailBlueprint => (TestSliderCircleBlueprint)base.TailBlueprint; public new TestSliderCircleBlueprint TailBlueprint => (TestSliderCircleBlueprint)base.TailBlueprint;
public new PathControlPointVisualiser ControlPointVisualiser => base.ControlPointVisualiser;
public TestSliderBlueprint(DrawableSlider slider) public TestSliderBlueprint(DrawableSlider slider)
: base(slider) : base(slider)

View File

@ -3,6 +3,7 @@
using System; using System;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Lines; using osu.Framework.Graphics.Lines;
@ -11,18 +12,22 @@ using osu.Framework.Input.Events;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osuTK; using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{ {
public class PathControlPointPiece : BlueprintPiece<Slider> public class PathControlPointPiece : BlueprintPiece<Slider>
{ {
public Action<int> RequestSelection;
public Action<Vector2[]> ControlPointsChanged; public Action<Vector2[]> ControlPointsChanged;
private readonly Slider slider; public readonly BindableBool IsSelected = new BindableBool();
private readonly int index; public readonly int Index;
private readonly Slider slider;
private readonly Path path; private readonly Path path;
private readonly CircularContainer marker; private readonly Container marker;
private readonly Drawable markerRing;
[Resolved] [Resolved]
private OsuColour colours { get; set; } private OsuColour colours { get; set; }
@ -30,7 +35,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
public PathControlPointPiece(Slider slider, int index) public PathControlPointPiece(Slider slider, int index)
{ {
this.slider = slider; this.slider = slider;
this.index = index; Index = index;
Origin = Anchor.Centre; Origin = Anchor.Centre;
AutoSizeAxes = Axes.Both; AutoSizeAxes = Axes.Both;
@ -42,13 +47,36 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
PathRadius = 1 PathRadius = 1
}, },
marker = new CircularContainer marker = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both,
Children = new[]
{
new Circle
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Size = new Vector2(10), Size = new Vector2(10),
},
markerRing = new CircularContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(14),
Masking = true, Masking = true,
Child = new Box { RelativeSizeAxes = Axes.Both } BorderThickness = 2,
BorderColour = Color4.White,
Alpha = 0,
Child = new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0,
AlwaysPresent = true
}
}
}
} }
}; };
} }
@ -57,30 +85,66 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{ {
base.Update(); base.Update();
Position = slider.StackedPosition + slider.Path.ControlPoints[index]; Position = slider.StackedPosition + slider.Path.ControlPoints[Index];
marker.Colour = isSegmentSeparator ? colours.Red : colours.Yellow; updateMarkerDisplay();
updateConnectingPath();
}
/// <summary>
/// Updates the state of the circular control point marker.
/// </summary>
private void updateMarkerDisplay()
{
markerRing.Alpha = IsSelected.Value ? 1 : 0;
Color4 colour = isSegmentSeparator ? colours.Red : colours.Yellow;
if (IsHovered || IsSelected.Value)
colour = Color4.White;
marker.Colour = colour;
}
/// <summary>
/// Updates the path connecting this control point to the previous one.
/// </summary>
private void updateConnectingPath()
{
path.ClearVertices(); path.ClearVertices();
if (index != slider.Path.ControlPoints.Length - 1) if (Index != slider.Path.ControlPoints.Length - 1)
{ {
path.AddVertex(Vector2.Zero); path.AddVertex(Vector2.Zero);
path.AddVertex(slider.Path.ControlPoints[index + 1] - slider.Path.ControlPoints[index]); path.AddVertex(slider.Path.ControlPoints[Index + 1] - slider.Path.ControlPoints[Index]);
} }
path.OriginPosition = path.PositionInBoundingBox(Vector2.Zero); path.OriginPosition = path.PositionInBoundingBox(Vector2.Zero);
} }
// The connecting path is excluded from positional input
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => marker.ReceivePositionalInputAt(screenSpacePos); public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => marker.ReceivePositionalInputAt(screenSpacePos);
protected override bool OnMouseDown(MouseDownEvent e)
{
if (RequestSelection != null)
{
RequestSelection.Invoke(Index);
return true;
}
return false;
}
protected override bool OnMouseUp(MouseUpEvent e) => RequestSelection != null;
protected override bool OnClick(ClickEvent e) => RequestSelection != null;
protected override bool OnDragStart(DragStartEvent e) => true; protected override bool OnDragStart(DragStartEvent e) => true;
protected override bool OnDrag(DragEvent e) protected override bool OnDrag(DragEvent e)
{ {
var newControlPoints = slider.Path.ControlPoints.ToArray(); var newControlPoints = slider.Path.ControlPoints.ToArray();
if (index == 0) if (Index == 0)
{ {
// Special handling for the head - only the position of the slider changes // Special handling for the head - only the position of the slider changes
slider.Position += e.Delta; slider.Position += e.Delta;
@ -90,13 +154,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
newControlPoints[i] -= e.Delta; newControlPoints[i] -= e.Delta;
} }
else else
newControlPoints[index] += e.Delta; newControlPoints[Index] += e.Delta;
if (isSegmentSeparatorWithNext) if (isSegmentSeparatorWithNext)
newControlPoints[index + 1] = newControlPoints[index]; newControlPoints[Index + 1] = newControlPoints[Index];
if (isSegmentSeparatorWithPrevious) if (isSegmentSeparatorWithPrevious)
newControlPoints[index - 1] = newControlPoints[index]; newControlPoints[Index - 1] = newControlPoints[Index];
ControlPointsChanged?.Invoke(newControlPoints); ControlPointsChanged?.Invoke(newControlPoints);
@ -107,8 +171,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
private bool isSegmentSeparator => isSegmentSeparatorWithNext || isSegmentSeparatorWithPrevious; private bool isSegmentSeparator => isSegmentSeparatorWithNext || isSegmentSeparatorWithPrevious;
private bool isSegmentSeparatorWithNext => index < slider.Path.ControlPoints.Length - 1 && slider.Path.ControlPoints[index + 1] == slider.Path.ControlPoints[index]; private bool isSegmentSeparatorWithNext => Index < slider.Path.ControlPoints.Length - 1 && slider.Path.ControlPoints[Index + 1] == slider.Path.ControlPoints[Index];
private bool isSegmentSeparatorWithPrevious => index > 0 && slider.Path.ControlPoints[index - 1] == slider.Path.ControlPoints[index]; private bool isSegmentSeparatorWithPrevious => Index > 0 && slider.Path.ControlPoints[Index - 1] == slider.Path.ControlPoints[Index];
} }
} }

View File

@ -4,6 +4,8 @@
using System; using System;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Input;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osuTK; using osuTK;
@ -13,25 +15,66 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{ {
public Action<Vector2[]> ControlPointsChanged; public Action<Vector2[]> ControlPointsChanged;
internal readonly Container<PathControlPointPiece> Pieces;
private readonly Slider slider; private readonly Slider slider;
private readonly bool allowSelection;
private readonly Container<PathControlPointPiece> pieces; private InputManager inputManager;
public PathControlPointVisualiser(Slider slider) public PathControlPointVisualiser(Slider slider, bool allowSelection)
{ {
this.slider = slider; this.slider = slider;
this.allowSelection = allowSelection;
InternalChild = pieces = new Container<PathControlPointPiece> { RelativeSizeAxes = Axes.Both }; RelativeSizeAxes = Axes.Both;
InternalChild = Pieces = new Container<PathControlPointPiece> { RelativeSizeAxes = Axes.Both };
}
protected override void LoadComplete()
{
base.LoadComplete();
inputManager = GetContainingInputManager();
} }
protected override void Update() protected override void Update()
{ {
base.Update(); base.Update();
while (slider.Path.ControlPoints.Length > pieces.Count) while (slider.Path.ControlPoints.Length > Pieces.Count)
pieces.Add(new PathControlPointPiece(slider, pieces.Count) { ControlPointsChanged = c => ControlPointsChanged?.Invoke(c) }); {
while (slider.Path.ControlPoints.Length < pieces.Count) var piece = new PathControlPointPiece(slider, Pieces.Count)
pieces.Remove(pieces[pieces.Count - 1]); {
ControlPointsChanged = c => ControlPointsChanged?.Invoke(c),
};
if (allowSelection)
piece.RequestSelection = selectPiece;
Pieces.Add(piece);
}
while (slider.Path.ControlPoints.Length < Pieces.Count)
Pieces.Remove(Pieces[Pieces.Count - 1]);
}
protected override bool OnClick(ClickEvent e)
{
foreach (var piece in Pieces)
piece.IsSelected.Value = false;
return false;
}
private void selectPiece(int index)
{
if (inputManager.CurrentState.Keyboard.ControlPressed)
Pieces[index].IsSelected.Toggle();
else
{
foreach (var piece in Pieces)
piece.IsSelected.Value = piece.Index == index;
}
} }
} }
} }

View File

@ -51,7 +51,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
bodyPiece = new SliderBodyPiece(), bodyPiece = new SliderBodyPiece(),
headCirclePiece = new HitCirclePiece(), headCirclePiece = new HitCirclePiece(),
tailCirclePiece = new HitCirclePiece(), tailCirclePiece = new HitCirclePiece(),
new PathControlPointVisualiser(HitObject) { ControlPointsChanged = _ => updateSlider() }, new PathControlPointVisualiser(HitObject, false) { ControlPointsChanged = _ => updateSlider() },
}; };
setState(PlacementState.Initial); setState(PlacementState.Initial);

View File

@ -18,6 +18,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
protected readonly SliderBodyPiece BodyPiece; protected readonly SliderBodyPiece BodyPiece;
protected readonly SliderCircleSelectionBlueprint HeadBlueprint; protected readonly SliderCircleSelectionBlueprint HeadBlueprint;
protected readonly SliderCircleSelectionBlueprint TailBlueprint; protected readonly SliderCircleSelectionBlueprint TailBlueprint;
protected readonly PathControlPointVisualiser ControlPointVisualiser;
[Resolved(CanBeNull = true)] [Resolved(CanBeNull = true)]
private HitObjectComposer composer { get; set; } private HitObjectComposer composer { get; set; }
@ -32,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
BodyPiece = new SliderBodyPiece(), BodyPiece = new SliderBodyPiece(),
HeadBlueprint = CreateCircleSelectionBlueprint(slider, SliderPosition.Start), HeadBlueprint = CreateCircleSelectionBlueprint(slider, SliderPosition.Start),
TailBlueprint = CreateCircleSelectionBlueprint(slider, SliderPosition.End), TailBlueprint = CreateCircleSelectionBlueprint(slider, SliderPosition.End),
new PathControlPointVisualiser(sliderObject) { ControlPointsChanged = onNewControlPoints }, ControlPointVisualiser = new PathControlPointVisualiser(sliderObject, true) { ControlPointsChanged = onNewControlPoints },
}; };
} }
@ -49,6 +50,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
var snappedDistance = composer?.GetSnappedDistanceFromDistance(HitObject.StartTime, (float)unsnappedPath.Distance) ?? (float)unsnappedPath.Distance; var snappedDistance = composer?.GetSnappedDistanceFromDistance(HitObject.StartTime, (float)unsnappedPath.Distance) ?? (float)unsnappedPath.Distance;
HitObject.Path = new SliderPath(unsnappedPath.Type, controlPoints, snappedDistance); HitObject.Path = new SliderPath(unsnappedPath.Type, controlPoints, snappedDistance);
UpdateHitObject();
} }
public override Vector2 SelectionPoint => HeadBlueprint.SelectionPoint; public override Vector2 SelectionPoint => HeadBlueprint.SelectionPoint;

View File

@ -37,6 +37,8 @@ namespace osu.Game.Rulesets.Osu.Objects
{ {
PathBindable.Value = value; PathBindable.Value = value;
endPositionCache.Invalidate(); endPositionCache.Invalidate();
updateNestedPositions();
} }
} }
@ -48,14 +50,9 @@ namespace osu.Game.Rulesets.Osu.Objects
set set
{ {
base.Position = value; base.Position = value;
endPositionCache.Invalidate(); endPositionCache.Invalidate();
if (HeadCircle != null) updateNestedPositions();
HeadCircle.Position = value;
if (TailCircle != null)
TailCircle.Position = EndPosition;
} }
} }
@ -197,6 +194,15 @@ namespace osu.Game.Rulesets.Osu.Objects
} }
} }
private void updateNestedPositions()
{
if (HeadCircle != null)
HeadCircle.Position = Position;
if (TailCircle != null)
TailCircle.Position = EndPosition;
}
private List<HitSampleInfo> getNodeSamples(int nodeIndex) => private List<HitSampleInfo> getNodeSamples(int nodeIndex) =>
nodeIndex < NodeSamples.Count ? NodeSamples[nodeIndex] : Samples; nodeIndex < NodeSamples.Count ? NodeSamples[nodeIndex] : Samples;

View File

@ -7,11 +7,10 @@ using osu.Game.Online;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Users; using osu.Game.Users;
using osuTK;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using osu.Game.Screens.Ranking.Pages;
namespace osu.Game.Tests.Visual.Gameplay namespace osu.Game.Tests.Visual.Gameplay
{ {
@ -42,7 +41,6 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Size = new Vector2(80, 40),
}; };
}); });
} }

View File

@ -3,11 +3,16 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Screens;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Screens;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking; using osu.Game.Screens.Ranking;
using osu.Game.Screens.Ranking.Pages; using osu.Game.Screens.Ranking.Pages;
@ -22,11 +27,13 @@ namespace osu.Game.Tests.Visual.Gameplay
public override IReadOnlyList<Type> RequiredTypes => new[] public override IReadOnlyList<Type> RequiredTypes => new[]
{ {
typeof(ScoreInfo),
typeof(Results), typeof(Results),
typeof(ResultsPage), typeof(ResultsPage),
typeof(ScoreResultsPage), typeof(ScoreResultsPage),
typeof(LocalLeaderboardPage) typeof(RetryButton),
typeof(ReplayDownloadButton),
typeof(LocalLeaderboardPage),
typeof(TestPlayer)
}; };
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
@ -42,8 +49,9 @@ namespace osu.Game.Tests.Visual.Gameplay
var beatmapInfo = beatmaps.QueryBeatmap(b => b.RulesetID == 0); var beatmapInfo = beatmaps.QueryBeatmap(b => b.RulesetID == 0);
if (beatmapInfo != null) if (beatmapInfo != null)
Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmapInfo); Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmapInfo);
}
LoadScreen(new SoloResults(new ScoreInfo private TestSoloResults createResultsScreen() => new TestSoloResults(new ScoreInfo
{ {
TotalScore = 2845370, TotalScore = 2845370,
Accuracy = 0.98, Accuracy = 0.98,
@ -61,7 +69,62 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
Username = "peppy", Username = "peppy",
} }
})); });
[Test]
public void ResultsWithoutPlayer()
{
TestSoloResults screen = null;
AddStep("load results", () => Child = new OsuScreenStack(screen = createResultsScreen())
{
RelativeSizeAxes = Axes.Both
});
AddUntilStep("wait for loaded", () => screen.IsLoaded);
AddAssert("retry overlay not present", () => screen.RetryOverlay == null);
}
[Test]
public void ResultsWithPlayer()
{
TestSoloResults screen = null;
AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen()));
AddUntilStep("wait for loaded", () => screen.IsLoaded);
AddAssert("retry overlay present", () => screen.RetryOverlay != null);
}
private class TestResultsContainer : Container
{
[Cached(typeof(Player))]
private readonly Player player = new TestPlayer();
public TestResultsContainer(IScreen screen)
{
RelativeSizeAxes = Axes.Both;
InternalChild = new OsuScreenStack(screen)
{
RelativeSizeAxes = Axes.Both,
};
}
}
private class TestSoloResults : SoloResults
{
public HotkeyRetryOverlay RetryOverlay;
public TestSoloResults(ScoreInfo score)
: base(score)
{
}
protected override void LoadComplete()
{
base.LoadComplete();
RetryOverlay = InternalChildren.OfType<HotkeyRetryOverlay>().SingleOrDefault();
}
} }
} }
} }

View File

@ -21,6 +21,7 @@ using osu.Game.Users.Drawables;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
using Humanizer; using Humanizer;
using osu.Game.Online.API;
namespace osu.Game.Online.Leaderboards namespace osu.Game.Online.Leaderboards
{ {
@ -37,6 +38,7 @@ namespace osu.Game.Online.Leaderboards
private readonly ScoreInfo score; private readonly ScoreInfo score;
private readonly int rank; private readonly int rank;
private readonly bool allowHighlight;
private Box background; private Box background;
private Container content; private Container content;
@ -49,17 +51,18 @@ namespace osu.Game.Online.Leaderboards
private List<ScoreComponentLabel> statisticsLabels; private List<ScoreComponentLabel> statisticsLabels;
public LeaderboardScore(ScoreInfo score, int rank) public LeaderboardScore(ScoreInfo score, int rank, bool allowHighlight = true)
{ {
this.score = score; this.score = score;
this.rank = rank; this.rank = rank;
this.allowHighlight = allowHighlight;
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
Height = HEIGHT; Height = HEIGHT;
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load(IAPIProvider api, OsuColour colour)
{ {
var user = score.User; var user = score.User;
@ -100,7 +103,7 @@ namespace osu.Game.Online.Leaderboards
background = new Box background = new Box
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Colour = Color4.Black, Colour = user.Id == api.LocalUser.Value.Id && allowHighlight ? colour.Green : Color4.Black,
Alpha = background_alpha, Alpha = background_alpha,
}, },
}, },

View File

@ -148,7 +148,7 @@ namespace osu.Game.Rulesets.Edit
EditorBeatmap = new EditorBeatmap<TObject>(playableBeatmap); EditorBeatmap = new EditorBeatmap<TObject>(playableBeatmap);
EditorBeatmap.HitObjectAdded += addHitObject; EditorBeatmap.HitObjectAdded += addHitObject;
EditorBeatmap.HitObjectRemoved += removeHitObject; EditorBeatmap.HitObjectRemoved += removeHitObject;
EditorBeatmap.StartTimeChanged += updateHitObject; EditorBeatmap.StartTimeChanged += UpdateHitObject;
var dependencies = new DependencyContainer(parent); var dependencies = new DependencyContainer(parent);
dependencies.CacheAs<IEditorBeatmap>(EditorBeatmap); dependencies.CacheAs<IEditorBeatmap>(EditorBeatmap);
@ -225,11 +225,7 @@ namespace osu.Game.Rulesets.Edit
private ScheduledDelegate scheduledUpdate; private ScheduledDelegate scheduledUpdate;
private void addHitObject(HitObject hitObject) => updateHitObject(hitObject); public override void UpdateHitObject(HitObject hitObject)
private void removeHitObject(HitObject hitObject) => updateHitObject(null);
private void updateHitObject([CanBeNull] HitObject hitObject)
{ {
scheduledUpdate?.Cancel(); scheduledUpdate?.Cancel();
scheduledUpdate = Schedule(() => scheduledUpdate = Schedule(() =>
@ -240,6 +236,10 @@ namespace osu.Game.Rulesets.Edit
}); });
} }
private void addHitObject(HitObject hitObject) => UpdateHitObject(hitObject);
private void removeHitObject(HitObject hitObject) => UpdateHitObject(null);
public override IEnumerable<DrawableHitObject> HitObjects => drawableRulesetWrapper.Playfield.AllHitObjects; public override IEnumerable<DrawableHitObject> HitObjects => drawableRulesetWrapper.Playfield.AllHitObjects;
public override bool CursorInPlacementArea => drawableRulesetWrapper.Playfield.ReceivePositionalInputAt(inputManager.CurrentState.Mouse.Position); public override bool CursorInPlacementArea => drawableRulesetWrapper.Playfield.ReceivePositionalInputAt(inputManager.CurrentState.Mouse.Position);
@ -351,11 +351,22 @@ namespace osu.Game.Rulesets.Edit
[CanBeNull] [CanBeNull]
protected virtual DistanceSnapGrid CreateDistanceSnapGrid([NotNull] IEnumerable<HitObject> selectedHitObjects) => null; protected virtual DistanceSnapGrid CreateDistanceSnapGrid([NotNull] IEnumerable<HitObject> selectedHitObjects) => null;
/// <summary>
/// Updates a <see cref="HitObject"/>, invoking <see cref="HitObject.ApplyDefaults"/> and re-processing the beatmap.
/// </summary>
/// <param name="hitObject">The <see cref="HitObject"/> to update.</param>
public abstract void UpdateHitObject([CanBeNull] HitObject hitObject);
public abstract (Vector2 position, double time) GetSnappedPosition(Vector2 position, double time); public abstract (Vector2 position, double time) GetSnappedPosition(Vector2 position, double time);
public abstract float GetBeatSnapDistanceAt(double referenceTime); public abstract float GetBeatSnapDistanceAt(double referenceTime);
public abstract float DurationToDistance(double referenceTime, double duration); public abstract float DurationToDistance(double referenceTime, double duration);
public abstract double DistanceToDuration(double referenceTime, float distance); public abstract double DistanceToDuration(double referenceTime, float distance);
public abstract double GetSnappedDurationFromDistance(double referenceTime, float distance); public abstract double GetSnappedDurationFromDistance(double referenceTime, float distance);
public abstract float GetSnappedDistanceFromDistance(double referenceTime, float distance); public abstract float GetSnappedDistanceFromDistance(double referenceTime, float distance);
} }
} }

View File

@ -3,10 +3,12 @@
using System; using System;
using osu.Framework; using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Primitives;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osuTK; using osuTK;
@ -36,6 +38,9 @@ namespace osu.Game.Rulesets.Edit
public override bool HandlePositionalInput => ShouldBeAlive; public override bool HandlePositionalInput => ShouldBeAlive;
public override bool RemoveWhenNotAlive => false; public override bool RemoveWhenNotAlive => false;
[Resolved(CanBeNull = true)]
private HitObjectComposer composer { get; set; }
protected SelectionBlueprint(DrawableHitObject drawableObject) protected SelectionBlueprint(DrawableHitObject drawableObject)
{ {
DrawableObject = drawableObject; DrawableObject = drawableObject;
@ -89,6 +94,11 @@ namespace osu.Game.Rulesets.Edit
public bool IsSelected => State == SelectionState.Selected; public bool IsSelected => State == SelectionState.Selected;
/// <summary>
/// Updates the <see cref="HitObject"/>, invoking <see cref="HitObject.ApplyDefaults"/> and re-processing the beatmap.
/// </summary>
protected void UpdateHitObject() => composer?.UpdateHitObject(DrawableObject.HitObject);
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => DrawableObject.ReceivePositionalInputAt(screenSpacePos); public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => DrawableObject.ReceivePositionalInputAt(screenSpacePos);
/// <summary> /// <summary>

View File

@ -8,6 +8,7 @@ using System.Threading.Tasks;
using osu.Framework; using osu.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
@ -29,7 +30,7 @@ namespace osu.Game.Screens.Play
/// <summary> /// <summary>
/// The original source (usually a <see cref="WorkingBeatmap"/>'s track). /// The original source (usually a <see cref="WorkingBeatmap"/>'s track).
/// </summary> /// </summary>
private readonly IAdjustableClock sourceClock; private IAdjustableClock sourceClock;
public readonly BindableBool IsPaused = new BindableBool(); public readonly BindableBool IsPaused = new BindableBool();
@ -153,6 +154,18 @@ namespace osu.Game.Screens.Play
IsPaused.Value = true; IsPaused.Value = true;
} }
/// <summary>
/// Changes the backing clock to avoid using the originally provided beatmap's track.
/// </summary>
public void StopUsingBeatmapClock()
{
if (sourceClock != beatmap.Track)
return;
sourceClock = new TrackVirtual(beatmap.Track.Length);
adjustableClock.ChangeSource(sourceClock);
}
public void ResetLocalAdjustments() public void ResetLocalAdjustments()
{ {
// In the case of replays, we may have changed the playback rate. // In the case of replays, we may have changed the playback rate.

View File

@ -30,6 +30,7 @@ using osu.Game.Users;
namespace osu.Game.Screens.Play namespace osu.Game.Screens.Play
{ {
[Cached]
public class Player : ScreenWithBeatmapBackground public class Player : ScreenWithBeatmapBackground
{ {
public override bool AllowBackButton => false; // handled by HoldForMenuButton public override bool AllowBackButton => false; // handled by HoldForMenuButton
@ -311,14 +312,19 @@ namespace osu.Game.Screens.Play
this.Exit(); this.Exit();
} }
/// <summary>
/// Restart gameplay via a parent <see cref="PlayerLoader"/>.
/// <remarks>This can be called from a child screen in order to trigger the restart process.</remarks>
/// </summary>
public void Restart() public void Restart()
{ {
if (!this.IsCurrentScreen()) return;
sampleRestart?.Play(); sampleRestart?.Play();
RestartRequested?.Invoke(); RestartRequested?.Invoke();
if (this.IsCurrentScreen())
performImmediateExit(); performImmediateExit();
else
this.MakeCurrent();
} }
private ScheduledDelegate completionProgressDelegate; private ScheduledDelegate completionProgressDelegate;
@ -532,6 +538,10 @@ namespace osu.Game.Screens.Play
GameplayClockContainer.ResetLocalAdjustments(); GameplayClockContainer.ResetLocalAdjustments();
// GameplayClockContainer performs seeks / start / stop operations on the beatmap's track.
// as we are no longer the current screen, we cannot guarantee the track is still usable.
GameplayClockContainer.StopUsingBeatmapClock();
fadeOut(); fadeOut();
return base.OnExiting(next); return base.OnExiting(next);
} }

View File

@ -4,12 +4,13 @@
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Online;
using osu.Game.Scoring;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Online;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Scoring;
using osuTK;
namespace osu.Game.Screens.Play namespace osu.Game.Screens.Ranking.Pages
{ {
public class ReplayDownloadButton : DownloadTrackingComposite<ScoreInfo, ScoreManager> public class ReplayDownloadButton : DownloadTrackingComposite<ScoreInfo, ScoreManager>
{ {
@ -33,6 +34,7 @@ namespace osu.Game.Screens.Play
public ReplayDownloadButton(ScoreInfo score) public ReplayDownloadButton(ScoreInfo score)
: base(score) : base(score)
{ {
Size = new Vector2(50, 30);
} }
[BackgroundDependencyLoader(true)] [BackgroundDependencyLoader(true)]

View File

@ -0,0 +1,54 @@
// 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.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
using osu.Game.Screens.Play;
using osuTK;
namespace osu.Game.Screens.Ranking.Pages
{
public class RetryButton : OsuAnimatedButton
{
private readonly Box background;
[Resolved(canBeNull: true)]
private Player player { get; set; }
public RetryButton()
{
Size = new Vector2(50, 30);
Children = new Drawable[]
{
background = new Box
{
RelativeSizeAxes = Axes.Both,
Depth = float.MaxValue
},
new SpriteIcon
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(13),
Icon = FontAwesome.Solid.ArrowCircleLeft,
},
};
TooltipText = "Retry";
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
background.Colour = colours.Green;
if (player != null)
Action = () => player.Restart();
}
}
}

View File

@ -169,12 +169,19 @@ namespace osu.Game.Screens.Ranking.Pages
}, },
}, },
}, },
new ReplayDownloadButton(score) new FillFlowContainer
{ {
Anchor = Anchor.BottomCentre, Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre, Origin = Anchor.BottomCentre,
Margin = new MarginPadding { Bottom = 10 }, Margin = new MarginPadding { Bottom = 10 },
Size = new Vector2(50, 30), Spacing = new Vector2(5),
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Children = new Drawable[]
{
new ReplayDownloadButton(score),
new RetryButton()
}
}, },
}; };

View File

@ -19,6 +19,7 @@ using osu.Game.Graphics;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Screens.Play;
namespace osu.Game.Screens.Ranking namespace osu.Game.Screens.Ranking
{ {
@ -34,6 +35,9 @@ namespace osu.Game.Screens.Ranking
private ResultModeTabControl modeChangeButtons; private ResultModeTabControl modeChangeButtons;
[Resolved(canBeNull: true)]
private Player player { get; set; }
public override bool DisallowExternalBeatmapRulesetChanges => true; public override bool DisallowExternalBeatmapRulesetChanges => true;
protected readonly ScoreInfo Score; protected readonly ScoreInfo Score;
@ -100,10 +104,7 @@ namespace osu.Game.Screens.Ranking
public override bool OnExiting(IScreen next) public override bool OnExiting(IScreen next)
{ {
allCircles.ForEach(c => allCircles.ForEach(c => c.ScaleTo(0, transition_time, Easing.OutSine));
{
c.ScaleTo(0, transition_time, Easing.OutSine);
});
Background.ScaleTo(1f, transition_time / 4, Easing.OutQuint); Background.ScaleTo(1f, transition_time / 4, Easing.OutQuint);
@ -115,9 +116,7 @@ namespace osu.Game.Screens.Ranking
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colours) private void load(OsuColour colours)
{ {
InternalChildren = new Drawable[] InternalChild = new AspectContainer
{
new AspectContainer
{ {
RelativeSizeAxes = Axes.Y, RelativeSizeAxes = Axes.Y,
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
@ -253,9 +252,21 @@ namespace osu.Game.Screens.Ranking
} }
} }
} }
}
}; };
if (player != null)
{
AddInternal(new HotkeyRetryOverlay
{
Action = () =>
{
if (!this.IsCurrentScreen()) return;
player?.Restart();
},
});
}
var pages = CreateResultPages(); var pages = CreateResultPages();
foreach (var p in pages) foreach (var p in pages)

View File

@ -179,7 +179,7 @@ namespace osu.Game.Screens.Select.Leaderboards
return req; return req;
} }
protected override LeaderboardScore CreateDrawableScore(ScoreInfo model, int index) => new LeaderboardScore(model, index) protected override LeaderboardScore CreateDrawableScore(ScoreInfo model, int index) => new LeaderboardScore(model, index, IsOnlineScope)
{ {
Action = () => ScoreSelected?.Invoke(model) Action = () => ScoreSelected?.Invoke(model)
}; };

View File

@ -77,7 +77,7 @@ namespace osu.Game.Screens.Select.Leaderboards
if (newScore == null) if (newScore == null)
return; return;
LoadComponentAsync(new LeaderboardScore(newScore.Score, newScore.Position) LoadComponentAsync(new LeaderboardScore(newScore.Score, newScore.Position, false)
{ {
Action = () => ScoreSelected?.Invoke(newScore.Score) Action = () => ScoreSelected?.Invoke(newScore.Score)
}, drawableScore => }, drawableScore =>

View File

@ -8,7 +8,7 @@ using osu.Game.Rulesets.Edit;
namespace osu.Game.Tests.Visual namespace osu.Game.Tests.Visual
{ {
public abstract class SelectionBlueprintTestScene : OsuTestScene public abstract class SelectionBlueprintTestScene : ManualInputManagerTestScene
{ {
protected override Container<Drawable> Content => content ?? base.Content; protected override Container<Drawable> Content => content ?? base.Content;
private readonly Container content; private readonly Container content;