1
0
mirror of https://github.com/ppy/osu.git synced 2025-03-03 13:33:07 +08:00

Merge branch 'master' into infrastructure

This commit is contained in:
Dan Balasescu 2019-11-08 19:10:49 +09:00 committed by GitHub
commit 1c1a49011b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
111 changed files with 3246 additions and 753 deletions

5
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: osu!stable issues
url: https://github.com/ppy/osu-stable-issues
about: For issues regarding osu!stable (not osu!lazer), open them here.

View File

@ -1,7 +0,0 @@
---
name: Missing for Live
about: Features which are available in osu!stable but not yet in osu!lazer.
---
**Describe the missing feature:**
**Proposal designs of the feature:**

View File

@ -53,6 +53,6 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2019.1010.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2019.1010.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2019.1029.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2019.1106.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -2,7 +2,7 @@
<Import Project="..\osu.TestProject.props" /> <Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.3.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" />
<PackageReference Include="NUnit" Version="3.12.0" /> <PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.15.1" /> <PackageReference Include="NUnit3TestAdapter" Version="3.15.1" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" /> <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />

View File

@ -2,7 +2,7 @@
<Import Project="..\osu.TestProject.props" /> <Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.3.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" />
<PackageReference Include="NUnit" Version="3.12.0" /> <PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.15.1" /> <PackageReference Include="NUnit3TestAdapter" Version="3.15.1" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" /> <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />

View File

@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Mania.Edit
editorClock = clock; editorClock = clock;
} }
public override void HandleMovement(MoveSelectionEvent moveEvent) public override bool HandleMovement(MoveSelectionEvent moveEvent)
{ {
var maniaBlueprint = (ManiaSelectionBlueprint)moveEvent.Blueprint; var maniaBlueprint = (ManiaSelectionBlueprint)moveEvent.Blueprint;
int lastColumn = maniaBlueprint.DrawableObject.HitObject.Column; int lastColumn = maniaBlueprint.DrawableObject.HitObject.Column;
@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Mania.Edit
performDragMovement(moveEvent); performDragMovement(moveEvent);
performColumnMovement(lastColumn, moveEvent); performColumnMovement(lastColumn, moveEvent);
base.HandleMovement(moveEvent); return true;
} }
/// <summary> /// <summary>

View File

@ -0,0 +1,230 @@
// 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 NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects.Drawables.Connections;
using osu.Game.Tests.Visual;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests
{
public class TestSceneFollowPoints : OsuTestScene
{
private Container<DrawableOsuHitObject> hitObjectContainer;
private FollowPointRenderer followPointRenderer;
[SetUp]
public void Setup() => Schedule(() =>
{
Children = new Drawable[]
{
hitObjectContainer = new TestHitObjectContainer { RelativeSizeAxes = Axes.Both },
followPointRenderer = new FollowPointRenderer { RelativeSizeAxes = Axes.Both }
};
});
[Test]
public void TestAddObject()
{
addObjectsStep(() => new OsuHitObject[] { new HitCircle { Position = new Vector2(100, 100) } });
assertGroups();
}
[Test]
public void TestRemoveObject()
{
addObjectsStep(() => new OsuHitObject[] { new HitCircle { Position = new Vector2(100, 100) } });
removeObjectStep(() => getObject(0));
assertGroups();
}
[Test]
public void TestAddMultipleObjects()
{
addMultipleObjectsStep();
assertGroups();
}
[Test]
public void TestRemoveEndObject()
{
addMultipleObjectsStep();
removeObjectStep(() => getObject(4));
assertGroups();
}
[Test]
public void TestRemoveStartObject()
{
addMultipleObjectsStep();
removeObjectStep(() => getObject(0));
assertGroups();
}
[Test]
public void TestRemoveMiddleObject()
{
addMultipleObjectsStep();
removeObjectStep(() => getObject(2));
assertGroups();
}
[Test]
public void TestMoveObject()
{
addMultipleObjectsStep();
AddStep("move hitobject", () => getObject(2).HitObject.Position = new Vector2(300, 100));
assertGroups();
}
[TestCase(0, 0)] // Start -> Start
[TestCase(0, 2)] // Start -> Middle
[TestCase(0, 5)] // Start -> End
[TestCase(2, 0)] // Middle -> Start
[TestCase(1, 3)] // Middle -> Middle (forwards)
[TestCase(3, 1)] // Middle -> Middle (backwards)
[TestCase(4, 0)] // End -> Start
[TestCase(4, 2)] // End -> Middle
[TestCase(4, 4)] // End -> End
public void TestReorderObjects(int startIndex, int endIndex)
{
addMultipleObjectsStep();
reorderObjectStep(startIndex, endIndex);
assertGroups();
}
private void addMultipleObjectsStep() => addObjectsStep(() => new OsuHitObject[]
{
new HitCircle { Position = new Vector2(100, 100) },
new HitCircle { Position = new Vector2(200, 200) },
new HitCircle { Position = new Vector2(300, 300) },
new HitCircle { Position = new Vector2(400, 400) },
new HitCircle { Position = new Vector2(500, 500) },
});
private void addObjectsStep(Func<OsuHitObject[]> ctorFunc)
{
AddStep("add hitobjects", () =>
{
var objects = ctorFunc();
for (int i = 0; i < objects.Length; i++)
{
objects[i].StartTime = Time.Current + 1000 + 500 * (i + 1);
objects[i].ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
DrawableOsuHitObject drawableObject = null;
switch (objects[i])
{
case HitCircle circle:
drawableObject = new DrawableHitCircle(circle);
break;
case Slider slider:
drawableObject = new DrawableSlider(slider);
break;
case Spinner spinner:
drawableObject = new DrawableSpinner(spinner);
break;
}
hitObjectContainer.Add(drawableObject);
followPointRenderer.AddFollowPoints(drawableObject);
}
});
}
private void removeObjectStep(Func<DrawableOsuHitObject> getFunc)
{
AddStep("remove hitobject", () =>
{
var drawableObject = getFunc?.Invoke();
hitObjectContainer.Remove(drawableObject);
followPointRenderer.RemoveFollowPoints(drawableObject);
});
}
private void reorderObjectStep(int startIndex, int endIndex)
{
AddStep($"move object {startIndex} to {endIndex}", () =>
{
DrawableOsuHitObject toReorder = getObject(startIndex);
double targetTime;
if (endIndex < hitObjectContainer.Count)
targetTime = getObject(endIndex).HitObject.StartTime - 1;
else
targetTime = getObject(hitObjectContainer.Count - 1).HitObject.StartTime + 1;
hitObjectContainer.Remove(toReorder);
toReorder.HitObject.StartTime = targetTime;
hitObjectContainer.Add(toReorder);
});
}
private void assertGroups()
{
AddAssert("has correct group count", () => followPointRenderer.Connections.Count == hitObjectContainer.Count);
AddAssert("group endpoints are correct", () =>
{
for (int i = 0; i < hitObjectContainer.Count; i++)
{
DrawableOsuHitObject expectedStart = getObject(i);
DrawableOsuHitObject expectedEnd = i < hitObjectContainer.Count - 1 ? getObject(i + 1) : null;
if (getGroup(i).Start != expectedStart)
throw new AssertionException($"Object {i} expected to be the start of group {i}.");
if (getGroup(i).End != expectedEnd)
throw new AssertionException($"Object {(expectedEnd == null ? "null" : i.ToString())} expected to be the end of group {i}.");
}
return true;
});
}
private DrawableOsuHitObject getObject(int index) => hitObjectContainer[index];
private FollowPointConnection getGroup(int index) => followPointRenderer.Connections[index];
private class TestHitObjectContainer : Container<DrawableOsuHitObject>
{
protected override int Compare(Drawable x, Drawable y)
{
var osuX = (DrawableOsuHitObject)x;
var osuY = (DrawableOsuHitObject)y;
int compare = osuX.HitObject.StartTime.CompareTo(osuY.HitObject.StartTime);
if (compare == 0)
return base.Compare(x, y);
return compare;
}
}
}
}

View File

@ -42,11 +42,19 @@ namespace osu.Game.Rulesets.Osu.Tests
[Cached(typeof(IDistanceSnapProvider))] [Cached(typeof(IDistanceSnapProvider))]
private readonly SnapProvider snapProvider = new SnapProvider(); private readonly SnapProvider snapProvider = new SnapProvider();
private readonly TestOsuDistanceSnapGrid grid; private TestOsuDistanceSnapGrid grid;
public TestSceneOsuDistanceSnapGrid() public TestSceneOsuDistanceSnapGrid()
{ {
editorBeatmap = new EditorBeatmap<OsuHitObject>(new OsuBeatmap()); editorBeatmap = new EditorBeatmap<OsuHitObject>(new OsuBeatmap());
}
[SetUp]
public void Setup() => Schedule(() =>
{
editorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = 1;
editorBeatmap.ControlPointInfo.Clear();
editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = beat_length });
Children = new Drawable[] Children = new Drawable[]
{ {
@ -58,14 +66,6 @@ namespace osu.Game.Rulesets.Osu.Tests
grid = new TestOsuDistanceSnapGrid(new HitCircle { Position = grid_position }), grid = new TestOsuDistanceSnapGrid(new HitCircle { Position = grid_position }),
new SnappingCursorContainer { GetSnapPosition = v => grid.GetSnappedPosition(grid.ToLocalSpace(v)).position } new SnappingCursorContainer { GetSnapPosition = v => grid.GetSnappedPosition(grid.ToLocalSpace(v)).position }
}; };
}
[SetUp]
public void Setup() => Schedule(() =>
{
editorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = 1;
editorBeatmap.ControlPointInfo.Clear();
editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = beat_length });
}); });
[TestCase(1)] [TestCase(1)]
@ -102,6 +102,27 @@ namespace osu.Game.Rulesets.Osu.Tests
assertSnappedDistance((float)beat_length * 2); assertSnappedDistance((float)beat_length * 2);
} }
[Test]
public void TestLimitedDistance()
{
AddStep("create limited grid", () =>
{
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.SlateGray
},
grid = new TestOsuDistanceSnapGrid(new HitCircle { Position = grid_position }, new HitCircle { StartTime = 200 }),
new SnappingCursorContainer { GetSnapPosition = v => grid.GetSnappedPosition(grid.ToLocalSpace(v)).position }
};
});
AddStep("move mouse outside grid", () => InputManager.MoveMouseTo(grid.ToScreenSpace(grid_position + new Vector2((float)beat_length, 0) * 3f)));
assertSnappedDistance((float)beat_length * 2);
}
private void assertSnappedDistance(float expectedDistance) => AddAssert($"snap distance = {expectedDistance}", () => private void assertSnappedDistance(float expectedDistance) => AddAssert($"snap distance = {expectedDistance}", () =>
{ {
Vector2 snappedPosition = grid.GetSnappedPosition(grid.ToLocalSpace(InputManager.CurrentState.Mouse.Position)).position; Vector2 snappedPosition = grid.GetSnappedPosition(grid.ToLocalSpace(InputManager.CurrentState.Mouse.Position)).position;
@ -152,8 +173,8 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
public new float DistanceSpacing => base.DistanceSpacing; public new float DistanceSpacing => base.DistanceSpacing;
public TestOsuDistanceSnapGrid(OsuHitObject hitObject) public TestOsuDistanceSnapGrid(OsuHitObject hitObject, OsuHitObject nextHitObject = null)
: base(hitObject) : base(hitObject, nextHitObject)
{ {
} }
} }
@ -164,9 +185,9 @@ namespace osu.Game.Rulesets.Osu.Tests
public float GetBeatSnapDistanceAt(double referenceTime) => (float)beat_length; public float GetBeatSnapDistanceAt(double referenceTime) => (float)beat_length;
public float DurationToDistance(double referenceTime, double duration) => 0; public float DurationToDistance(double referenceTime, double duration) => (float)duration;
public double DistanceToDuration(double referenceTime, float distance) => 0; public double DistanceToDuration(double referenceTime, float distance) => distance;
public double GetSnappedDurationFromDistance(double referenceTime, float distance) => 0; public double GetSnappedDurationFromDistance(double referenceTime, float distance) => 0;

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

@ -2,7 +2,7 @@
<Import Project="..\osu.TestProject.props" /> <Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.3.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" />
<PackageReference Include="NUnit" Version="3.12.0" /> <PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.15.1" /> <PackageReference Include="NUnit3TestAdapter" Version="3.15.1" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" /> <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />

View File

@ -3,26 +3,35 @@
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;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Rulesets.Edit;
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(CanBeNull = true)]
private IDistanceSnapProvider snapProvider { get; set; }
[Resolved] [Resolved]
private OsuColour colours { get; set; } private OsuColour colours { get; set; }
@ -30,7 +39,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 +51,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, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Size = new Vector2(10), AutoSizeAxes = Axes.Both,
Masking = true, Children = new[]
Child = new Box { RelativeSizeAxes = Axes.Both } {
new Circle
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(10),
},
markerRing = new CircularContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(14),
Masking = true,
BorderThickness = 2,
BorderColour = Color4.White,
Alpha = 0,
Child = new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0,
AlwaysPresent = true
}
}
}
} }
}; };
} }
@ -57,46 +89,86 @@ 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 control point - the position of the slider changes which means the snapped position and time have to be taken into account
slider.Position += e.Delta; (Vector2 snappedPosition, double snappedTime) = snapProvider?.GetSnappedPosition(e.MousePosition, slider.StartTime) ?? (e.MousePosition, slider.StartTime);
Vector2 movementDelta = snappedPosition - slider.Position;
slider.Position += movementDelta;
slider.StartTime = snappedTime;
// Since control points are relative to the position of the slider, they all need to be offset backwards by the delta // Since control points are relative to the position of the slider, they all need to be offset backwards by the delta
for (int i = 1; i < newControlPoints.Length; i++) for (int i = 1; i < newControlPoints.Length; i++)
newControlPoints[i] -= e.Delta; newControlPoints[i] -= movementDelta;
} }
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 +179,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

@ -2,36 +2,132 @@
// 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.Collections.Generic;
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.Input;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit.Compose;
using osuTK; using osuTK;
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{ {
public class PathControlPointVisualiser : CompositeDrawable public class PathControlPointVisualiser : CompositeDrawable, IKeyBindingHandler<PlatformAction>
{ {
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) [Resolved(CanBeNull = true)]
private IPlacementHandler placementHandler { get; set; }
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;
}
}
public bool OnPressed(PlatformAction action)
{
switch (action.ActionMethod)
{
case PlatformActionMethod.Delete:
var newControlPoints = new List<Vector2>();
foreach (var piece in Pieces)
{
if (!piece.IsSelected.Value)
newControlPoints.Add(slider.Path.ControlPoints[piece.Index]);
}
// Ensure that there are any points to be deleted
if (newControlPoints.Count == slider.Path.ControlPoints.Length)
return false;
// If there are 0 remaining control points, treat the slider as being deleted
if (newControlPoints.Count == 0)
{
placementHandler?.Delete(slider);
return true;
}
// Make control points relative
Vector2 first = newControlPoints[0];
for (int i = 0; i < newControlPoints.Count; i++)
newControlPoints[i] = newControlPoints[i] - first;
// The slider's position defines the position of the first control point, and all further control points are relative to that point
slider.Position = slider.Position + first;
// Since pieces are re-used, they will not point to the deleted control points while remaining selected
foreach (var piece in Pieces)
piece.IsSelected.Value = false;
ControlPointsChanged?.Invoke(newControlPoints.ToArray());
return true;
}
return false;
}
public bool OnReleased(PlatformAction action) => action.ActionMethod == PlatformActionMethod.Delete;
} }
} }

View File

@ -43,5 +43,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
Size = body.Size; Size = body.Size;
OriginPosition = body.PathOffset; OriginPosition = body.PathOffset;
} }
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => body.ReceivePositionalInputAt(screenSpacePos);
} }
} }

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

@ -8,8 +8,8 @@ namespace osu.Game.Rulesets.Osu.Edit
{ {
public class OsuDistanceSnapGrid : CircularDistanceSnapGrid public class OsuDistanceSnapGrid : CircularDistanceSnapGrid
{ {
public OsuDistanceSnapGrid(OsuHitObject hitObject) public OsuDistanceSnapGrid(OsuHitObject hitObject, OsuHitObject nextHitObject)
: base(hitObject, hitObject.StackedEndPosition) : base(hitObject, nextHitObject, hitObject.StackedEndPosition)
{ {
Masking = true; Masking = true;
} }

View File

@ -1,6 +1,7 @@
// 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.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
@ -60,25 +61,40 @@ namespace osu.Game.Rulesets.Osu.Edit
var objects = selectedHitObjects.ToList(); var objects = selectedHitObjects.ToList();
if (objects.Count == 0) if (objects.Count == 0)
return createGrid(h => h.StartTime <= EditorClock.CurrentTime);
double minTime = objects.Min(h => h.StartTime);
return createGrid(h => h.StartTime < minTime, objects.Count + 1);
}
/// <summary>
/// Creates a grid from the last <see cref="HitObject"/> matching a predicate to a target <see cref="HitObject"/>.
/// </summary>
/// <param name="sourceSelector">A predicate that matches <see cref="HitObject"/>s where the grid can start from.
/// Only the last <see cref="HitObject"/> matching the predicate is used.</param>
/// <param name="targetOffset">An offset from the <see cref="HitObject"/> selected via <paramref name="sourceSelector"/> at which the grid should stop.</param>
/// <returns>The <see cref="OsuDistanceSnapGrid"/> from a selected <see cref="HitObject"/> to a target <see cref="HitObject"/>.</returns>
private OsuDistanceSnapGrid createGrid(Func<HitObject, bool> sourceSelector, int targetOffset = 1)
{
if (targetOffset < 1) throw new ArgumentOutOfRangeException(nameof(targetOffset));
int sourceIndex = -1;
for (int i = 0; i < EditorBeatmap.HitObjects.Count; i++)
{ {
var lastObject = EditorBeatmap.HitObjects.LastOrDefault(h => h.StartTime <= EditorClock.CurrentTime); if (!sourceSelector(EditorBeatmap.HitObjects[i]))
break;
if (lastObject == null) sourceIndex = i;
return null;
return new OsuDistanceSnapGrid(lastObject);
} }
else
{
double minTime = objects.Min(h => h.StartTime);
var lastObject = EditorBeatmap.HitObjects.LastOrDefault(h => h.StartTime < minTime); if (sourceIndex == -1)
return null;
if (lastObject == null) OsuHitObject sourceObject = EditorBeatmap.HitObjects[sourceIndex];
return null; OsuHitObject targetObject = sourceIndex + targetOffset < EditorBeatmap.HitObjects.Count ? EditorBeatmap.HitObjects[sourceIndex + targetOffset] : null;
return new OsuDistanceSnapGrid(lastObject); return new OsuDistanceSnapGrid(sourceObject, targetObject);
}
} }
} }
} }

View File

@ -4,13 +4,34 @@
using System.Linq; using System.Linq;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit.Compose.Components; using osu.Game.Screens.Edit.Compose.Components;
using osuTK;
namespace osu.Game.Rulesets.Osu.Edit namespace osu.Game.Rulesets.Osu.Edit
{ {
public class OsuSelectionHandler : SelectionHandler public class OsuSelectionHandler : SelectionHandler
{ {
public override void HandleMovement(MoveSelectionEvent moveEvent) public override bool HandleMovement(MoveSelectionEvent moveEvent)
{ {
Vector2 minPosition = new Vector2(float.MaxValue, float.MaxValue);
Vector2 maxPosition = new Vector2(float.MinValue, float.MinValue);
// Go through all hitobjects to make sure they would remain in the bounds of the editor after movement, before any movement is attempted
foreach (var h in SelectedHitObjects.OfType<OsuHitObject>())
{
if (h is Spinner)
{
// Spinners don't support position adjustments
continue;
}
// Stacking is not considered
minPosition = Vector2.ComponentMin(minPosition, Vector2.ComponentMin(h.EndPosition + moveEvent.InstantDelta, h.Position + moveEvent.InstantDelta));
maxPosition = Vector2.ComponentMax(maxPosition, Vector2.ComponentMax(h.EndPosition + moveEvent.InstantDelta, h.Position + moveEvent.InstantDelta));
}
if (minPosition.X < 0 || minPosition.Y < 0 || maxPosition.X > DrawWidth || maxPosition.Y > DrawHeight)
return false;
foreach (var h in SelectedHitObjects.OfType<OsuHitObject>()) foreach (var h in SelectedHitObjects.OfType<OsuHitObject>())
{ {
if (h is Spinner) if (h is Spinner)
@ -22,7 +43,7 @@ namespace osu.Game.Rulesets.Osu.Edit
h.Position += moveEvent.InstantDelta; h.Position += moveEvent.InstantDelta;
} }
base.HandleMovement(moveEvent); return true;
} }
} }
} }

View File

@ -1,21 +0,0 @@
// 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.Graphics.Containers;
using osu.Game.Rulesets.Objects;
using System.Collections.Generic;
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
{
/// <summary>
/// Connects hit objects visually, for example with follow points.
/// </summary>
public abstract class ConnectionRenderer<T> : LifetimeManagementContainer
where T : HitObject
{
/// <summary>
/// Hit objects to create connections for
/// </summary>
public abstract IEnumerable<T> HitObjects { get; set; }
}
}

View File

@ -12,6 +12,9 @@ using osu.Game.Skinning;
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
{ {
/// <summary>
/// A single follow point positioned between two adjacent <see cref="DrawableOsuHitObject"/>s.
/// </summary>
public class FollowPoint : Container public class FollowPoint : Container
{ {
private const float width = 8; private const float width = 8;

View File

@ -0,0 +1,140 @@
// 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 JetBrains.Annotations;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Objects.Types;
using osuTK;
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
{
/// <summary>
/// Visualises the <see cref="FollowPoint"/>s between two <see cref="DrawableOsuHitObject"/>s.
/// </summary>
public class FollowPointConnection : CompositeDrawable
{
// Todo: These shouldn't be constants
private const int spacing = 32;
private const double preempt = 800;
/// <summary>
/// The start time of <see cref="Start"/>.
/// </summary>
public readonly Bindable<double> StartTime = new Bindable<double>();
/// <summary>
/// The <see cref="DrawableOsuHitObject"/> which <see cref="FollowPoint"/>s will exit from.
/// </summary>
[NotNull]
public readonly DrawableOsuHitObject Start;
/// <summary>
/// Creates a new <see cref="FollowPointConnection"/>.
/// </summary>
/// <param name="start">The <see cref="DrawableOsuHitObject"/> which <see cref="FollowPoint"/>s will exit from.</param>
public FollowPointConnection([NotNull] DrawableOsuHitObject start)
{
Start = start;
RelativeSizeAxes = Axes.Both;
StartTime.BindTo(Start.HitObject.StartTimeBindable);
}
protected override void LoadComplete()
{
base.LoadComplete();
bindEvents(Start);
}
private DrawableOsuHitObject end;
/// <summary>
/// The <see cref="DrawableOsuHitObject"/> which <see cref="FollowPoint"/>s will enter.
/// </summary>
[CanBeNull]
public DrawableOsuHitObject End
{
get => end;
set
{
end = value;
if (end != null)
bindEvents(end);
if (IsLoaded)
scheduleRefresh();
else
refresh();
}
}
private void bindEvents(DrawableOsuHitObject drawableObject)
{
drawableObject.HitObject.PositionBindable.BindValueChanged(_ => scheduleRefresh());
drawableObject.HitObject.DefaultsApplied += scheduleRefresh;
}
private void scheduleRefresh() => Scheduler.AddOnce(refresh);
private void refresh()
{
ClearInternal();
if (End == null)
return;
OsuHitObject osuStart = Start.HitObject;
OsuHitObject osuEnd = End.HitObject;
if (osuEnd.NewCombo)
return;
if (osuStart is Spinner || osuEnd is Spinner)
return;
Vector2 startPosition = osuStart.EndPosition;
Vector2 endPosition = osuEnd.Position;
double startTime = (osuStart as IHasEndTime)?.EndTime ?? osuStart.StartTime;
double endTime = osuEnd.StartTime;
Vector2 distanceVector = endPosition - startPosition;
int distance = (int)distanceVector.Length;
float rotation = (float)(Math.Atan2(distanceVector.Y, distanceVector.X) * (180 / Math.PI));
double duration = endTime - startTime;
for (int d = (int)(spacing * 1.5); d < distance - spacing; d += spacing)
{
float fraction = (float)d / distance;
Vector2 pointStartPosition = startPosition + (fraction - 0.1f) * distanceVector;
Vector2 pointEndPosition = startPosition + fraction * distanceVector;
double fadeOutTime = startTime + fraction * duration;
double fadeInTime = fadeOutTime - preempt;
FollowPoint fp;
AddInternal(fp = new FollowPoint
{
Position = pointStartPosition,
Rotation = rotation,
Alpha = 0,
Scale = new Vector2(1.5f * osuEnd.Scale),
});
using (fp.BeginAbsoluteSequence(fadeInTime))
{
fp.FadeIn(osuEnd.TimeFadeIn);
fp.ScaleTo(osuEnd.Scale, osuEnd.TimeFadeIn, Easing.Out);
fp.MoveTo(pointEndPosition, osuEnd.TimeFadeIn, Easing.Out);
fp.Delay(fadeOutTime - fadeInTime).FadeOut(osuEnd.TimeFadeIn);
}
fp.Expire(true);
}
}
}
}

View File

@ -1,121 +1,110 @@
// 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.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using osuTK; using System.Linq;
using osu.Framework.Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Rulesets.Objects.Types; using osu.Framework.Graphics.Containers;
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
{ {
public class FollowPointRenderer : ConnectionRenderer<OsuHitObject> /// <summary>
/// Visualises connections between <see cref="DrawableOsuHitObject"/>s.
/// </summary>
public class FollowPointRenderer : CompositeDrawable
{ {
private int pointDistance = 32;
/// <summary> /// <summary>
/// Determines how much space there is between points. /// All the <see cref="FollowPointConnection"/>s contained by this <see cref="FollowPointRenderer"/>.
/// </summary> /// </summary>
public int PointDistance internal IReadOnlyList<FollowPointConnection> Connections => connections;
{
get => pointDistance;
set
{
if (pointDistance == value) return;
pointDistance = value; private readonly List<FollowPointConnection> connections = new List<FollowPointConnection>();
update();
}
}
private int preEmpt = 800;
/// <summary>
/// Follow points to the next hitobject start appearing for this many milliseconds before an hitobject's end time.
/// </summary>
public int PreEmpt
{
get => preEmpt;
set
{
if (preEmpt == value) return;
preEmpt = value;
update();
}
}
private IEnumerable<OsuHitObject> hitObjects;
public override IEnumerable<OsuHitObject> HitObjects
{
get => hitObjects;
set
{
hitObjects = value;
update();
}
}
public override bool RemoveCompletedTransforms => false; public override bool RemoveCompletedTransforms => false;
private void update() /// <summary>
/// Adds the <see cref="FollowPoint"/>s around a <see cref="DrawableOsuHitObject"/>.
/// This includes <see cref="FollowPoint"/>s leading into <paramref name="hitObject"/>, and <see cref="FollowPoint"/>s exiting <paramref name="hitObject"/>.
/// </summary>
/// <param name="hitObject">The <see cref="DrawableOsuHitObject"/> to add <see cref="FollowPoint"/>s for.</param>
public void AddFollowPoints(DrawableOsuHitObject hitObject)
=> addConnection(new FollowPointConnection(hitObject).With(g => g.StartTime.BindValueChanged(_ => onStartTimeChanged(g))));
/// <summary>
/// Removes the <see cref="FollowPoint"/>s around a <see cref="DrawableOsuHitObject"/>.
/// This includes <see cref="FollowPoint"/>s leading into <paramref name="hitObject"/>, and <see cref="FollowPoint"/>s exiting <paramref name="hitObject"/>.
/// </summary>
/// <param name="hitObject">The <see cref="DrawableOsuHitObject"/> to remove <see cref="FollowPoint"/>s for.</param>
public void RemoveFollowPoints(DrawableOsuHitObject hitObject) => removeGroup(connections.Single(g => g.Start == hitObject));
/// <summary>
/// Adds a <see cref="FollowPointConnection"/> to this <see cref="FollowPointRenderer"/>.
/// </summary>
/// <param name="connection">The <see cref="FollowPointConnection"/> to add.</param>
/// <returns>The index of <paramref name="connection"/> in <see cref="connections"/>.</returns>
private void addConnection(FollowPointConnection connection)
{ {
ClearInternal(); AddInternal(connection);
if (hitObjects == null) // Groups are sorted by their start time when added such that the index can be used to post-process other surrounding connections
return; int index = connections.AddInPlace(connection, Comparer<FollowPointConnection>.Create((g1, g2) => g1.StartTime.Value.CompareTo(g2.StartTime.Value)));
OsuHitObject prevHitObject = null; if (index < connections.Count - 1)
foreach (var currHitObject in hitObjects)
{ {
if (prevHitObject != null && !currHitObject.NewCombo && !(prevHitObject is Spinner) && !(currHitObject is Spinner)) // Update the connection's end point to the next connection's start point
{ // h1 -> -> -> h2
Vector2 startPosition = prevHitObject.EndPosition; // connection nextGroup
Vector2 endPosition = currHitObject.Position;
double startTime = (prevHitObject as IHasEndTime)?.EndTime ?? prevHitObject.StartTime;
double endTime = currHitObject.StartTime;
Vector2 distanceVector = endPosition - startPosition; FollowPointConnection nextConnection = connections[index + 1];
int distance = (int)distanceVector.Length; connection.End = nextConnection.Start;
float rotation = (float)(Math.Atan2(distanceVector.Y, distanceVector.X) * (180 / Math.PI));
double duration = endTime - startTime;
for (int d = (int)(PointDistance * 1.5); d < distance - PointDistance; d += PointDistance)
{
float fraction = (float)d / distance;
Vector2 pointStartPosition = startPosition + (fraction - 0.1f) * distanceVector;
Vector2 pointEndPosition = startPosition + fraction * distanceVector;
double fadeOutTime = startTime + fraction * duration;
double fadeInTime = fadeOutTime - PreEmpt;
FollowPoint fp;
AddInternal(fp = new FollowPoint
{
Position = pointStartPosition,
Rotation = rotation,
Alpha = 0,
Scale = new Vector2(1.5f * currHitObject.Scale),
});
using (fp.BeginAbsoluteSequence(fadeInTime))
{
fp.FadeIn(currHitObject.TimeFadeIn);
fp.ScaleTo(currHitObject.Scale, currHitObject.TimeFadeIn, Easing.Out);
fp.MoveTo(pointEndPosition, currHitObject.TimeFadeIn, Easing.Out);
fp.Delay(fadeOutTime - fadeInTime).FadeOut(currHitObject.TimeFadeIn);
}
fp.Expire(true);
}
}
prevHitObject = currHitObject;
} }
else
{
// The end point may be non-null during re-ordering
connection.End = null;
}
if (index > 0)
{
// Update the previous connection's end point to the current connection's start point
// h1 -> -> -> h2
// prevGroup connection
FollowPointConnection previousConnection = connections[index - 1];
previousConnection.End = connection.Start;
}
}
/// <summary>
/// Removes a <see cref="FollowPointConnection"/> from this <see cref="FollowPointRenderer"/>.
/// </summary>
/// <param name="connection">The <see cref="FollowPointConnection"/> to remove.</param>
/// <returns>Whether <paramref name="connection"/> was removed.</returns>
private void removeGroup(FollowPointConnection connection)
{
RemoveInternal(connection);
int index = connections.IndexOf(connection);
if (index > 0)
{
// Update the previous connection's end point to the next connection's start point
// h1 -> -> -> h2 -> -> -> h3
// prevGroup connection nextGroup
// The current connection's end point is used since there may not be a next connection
FollowPointConnection previousConnection = connections[index - 1];
previousConnection.End = connection.End;
}
connections.Remove(connection);
}
private void onStartTimeChanged(FollowPointConnection connection)
{
// Naive but can be improved if performance becomes an issue
removeGroup(connection);
addConnection(connection);
} }
} }
} }

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

@ -9,7 +9,6 @@ using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects.Drawables.Connections; using osu.Game.Rulesets.Osu.Objects.Drawables.Connections;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using System.Linq;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Osu.UI.Cursor; using osu.Game.Rulesets.Osu.UI.Cursor;
using osu.Game.Skinning; using osu.Game.Skinning;
@ -20,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.UI
{ {
private readonly ApproachCircleProxyContainer approachCircles; private readonly ApproachCircleProxyContainer approachCircles;
private readonly JudgementContainer<DrawableOsuJudgement> judgementLayer; private readonly JudgementContainer<DrawableOsuJudgement> judgementLayer;
private readonly ConnectionRenderer<OsuHitObject> connectionLayer; private readonly FollowPointRenderer followPoints;
public static readonly Vector2 BASE_SIZE = new Vector2(512, 384); public static readonly Vector2 BASE_SIZE = new Vector2(512, 384);
@ -30,7 +29,7 @@ namespace osu.Game.Rulesets.Osu.UI
{ {
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
connectionLayer = new FollowPointRenderer followPoints = new FollowPointRenderer
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Depth = 2, Depth = 2,
@ -64,11 +63,18 @@ namespace osu.Game.Rulesets.Osu.UI
}; };
base.Add(h); base.Add(h);
followPoints.AddFollowPoints((DrawableOsuHitObject)h);
} }
public override void PostProcess() public override bool Remove(DrawableHitObject h)
{ {
connectionLayer.HitObjects = HitObjectContainer.Objects.Select(d => d.HitObject).OfType<OsuHitObject>(); bool result = base.Remove(h);
if (result)
followPoints.RemoveFollowPoints((DrawableOsuHitObject)h);
return result;
} }
private void onNewResult(DrawableHitObject judgedObject, JudgementResult result) private void onNewResult(DrawableHitObject judgedObject, JudgementResult result)

View File

@ -38,9 +38,10 @@ namespace osu.Game.Rulesets.Osu.UI
}); });
} }
public override void Show() protected override void PopIn()
{ {
base.Show(); base.PopIn();
GameplayCursor.ActiveCursor.Hide(); GameplayCursor.ActiveCursor.Hide();
cursorScaleContainer.MoveTo(GameplayCursor.ActiveCursor.Position); cursorScaleContainer.MoveTo(GameplayCursor.ActiveCursor.Position);
clickToResumeCursor.Appear(); clickToResumeCursor.Appear();
@ -55,13 +56,13 @@ namespace osu.Game.Rulesets.Osu.UI
} }
} }
public override void Hide() protected override void PopOut()
{ {
base.PopOut();
localCursorContainer?.Expire(); localCursorContainer?.Expire();
localCursorContainer = null; localCursorContainer = null;
GameplayCursor.ActiveCursor.Show(); GameplayCursor?.ActiveCursor?.Show();
base.Hide();
} }
protected override bool OnHover(HoverEvent e) => true; protected override bool OnHover(HoverEvent e) => true;

View File

@ -2,7 +2,7 @@
<Import Project="..\osu.TestProject.props" /> <Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.3.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" />
<PackageReference Include="NUnit" Version="3.12.0" /> <PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.15.1" /> <PackageReference Include="NUnit3TestAdapter" Version="3.15.1" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" /> <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />

View File

@ -34,6 +34,7 @@ namespace osu.Game.Tests.Visual.Components
PreviewTrack track = null; PreviewTrack track = null;
AddStep("get track", () => track = getOwnedTrack()); AddStep("get track", () => track = getOwnedTrack());
AddUntilStep("wait loaded", () => track.IsLoaded);
AddStep("start", () => track.Start()); AddStep("start", () => track.Start());
AddAssert("started", () => track.IsRunning); AddAssert("started", () => track.IsRunning);
AddStep("stop", () => track.Stop()); AddStep("stop", () => track.Stop());
@ -52,6 +53,8 @@ namespace osu.Game.Tests.Visual.Components
track2 = getOwnedTrack(); track2 = getOwnedTrack();
}); });
AddUntilStep("wait loaded", () => track1.IsLoaded && track2.IsLoaded);
AddStep("start track 1", () => track1.Start()); AddStep("start track 1", () => track1.Start());
AddStep("start track 2", () => track2.Start()); AddStep("start track 2", () => track2.Start());
AddAssert("track 1 stopped", () => !track1.IsRunning); AddAssert("track 1 stopped", () => !track1.IsRunning);
@ -64,6 +67,7 @@ namespace osu.Game.Tests.Visual.Components
PreviewTrack track = null; PreviewTrack track = null;
AddStep("get track", () => track = getOwnedTrack()); AddStep("get track", () => track = getOwnedTrack());
AddUntilStep("wait loaded", () => track.IsLoaded);
AddStep("start", () => track.Start()); AddStep("start", () => track.Start());
AddStep("stop by owner", () => trackManager.StopAnyPlaying(this)); AddStep("stop by owner", () => trackManager.StopAnyPlaying(this));
AddAssert("stopped", () => !track.IsRunning); AddAssert("stopped", () => !track.IsRunning);
@ -76,6 +80,7 @@ namespace osu.Game.Tests.Visual.Components
PreviewTrack track = null; PreviewTrack track = null;
AddStep("get track", () => Add(owner = new TestTrackOwner(track = getTrack()))); AddStep("get track", () => Add(owner = new TestTrackOwner(track = getTrack())));
AddUntilStep("wait loaded", () => track.IsLoaded);
AddStep("start", () => track.Start()); AddStep("start", () => track.Start());
AddStep("attempt stop", () => trackManager.StopAnyPlaying(this)); AddStep("attempt stop", () => trackManager.StopAnyPlaying(this));
AddAssert("not stopped", () => track.IsRunning); AddAssert("not stopped", () => track.IsRunning);
@ -89,16 +94,24 @@ namespace osu.Game.Tests.Visual.Components
{ {
var track = getTrack(); var track = getTrack();
Add(track); LoadComponentAsync(track, Add);
return track; return track;
} }
private class TestTrackOwner : CompositeDrawable, IPreviewTrackOwner private class TestTrackOwner : CompositeDrawable, IPreviewTrackOwner
{ {
private readonly PreviewTrack track;
public TestTrackOwner(PreviewTrack track) public TestTrackOwner(PreviewTrack track)
{ {
AddInternal(track); this.track = track;
}
[BackgroundDependencyLoader]
private void load()
{
LoadComponentAsync(track, AddInternal);
} }
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)

View File

@ -32,7 +32,11 @@ namespace osu.Game.Tests.Visual.Editor
{ {
editorBeatmap = new EditorBeatmap<OsuHitObject>(new OsuBeatmap()); editorBeatmap = new EditorBeatmap<OsuHitObject>(new OsuBeatmap());
editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = beat_length }); editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = beat_length });
}
[SetUp]
public void Setup() => Schedule(() =>
{
Children = new Drawable[] Children = new Drawable[]
{ {
new Box new Box
@ -42,7 +46,7 @@ namespace osu.Game.Tests.Visual.Editor
}, },
new TestDistanceSnapGrid(new HitObject(), grid_position) new TestDistanceSnapGrid(new HitObject(), grid_position)
}; };
} });
[TestCase(1)] [TestCase(1)]
[TestCase(2)] [TestCase(2)]
@ -57,12 +61,29 @@ namespace osu.Game.Tests.Visual.Editor
AddStep($"set beat divisor = {divisor}", () => BeatDivisor.Value = divisor); AddStep($"set beat divisor = {divisor}", () => BeatDivisor.Value = divisor);
} }
[Test]
public void TestLimitedDistance()
{
AddStep("create limited grid", () =>
{
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.SlateGray
},
new TestDistanceSnapGrid(new HitObject(), grid_position, new HitObject { StartTime = 100 })
};
});
}
private class TestDistanceSnapGrid : DistanceSnapGrid private class TestDistanceSnapGrid : DistanceSnapGrid
{ {
public new float DistanceSpacing => base.DistanceSpacing; public new float DistanceSpacing => base.DistanceSpacing;
public TestDistanceSnapGrid(HitObject hitObject, Vector2 centrePosition) public TestDistanceSnapGrid(HitObject hitObject, Vector2 centrePosition, HitObject nextHitObject = null)
: base(hitObject, centrePosition) : base(hitObject, nextHitObject, centrePosition)
{ {
} }
@ -77,7 +98,7 @@ namespace osu.Game.Tests.Visual.Editor
int beatIndex = 0; int beatIndex = 0;
for (float s = centrePosition.X + DistanceSpacing; s <= DrawWidth; s += DistanceSpacing, beatIndex++) for (float s = centrePosition.X + DistanceSpacing; s <= DrawWidth && beatIndex < MaxIntervals; s += DistanceSpacing, beatIndex++)
{ {
AddInternal(new Circle AddInternal(new Circle
{ {
@ -90,7 +111,7 @@ namespace osu.Game.Tests.Visual.Editor
beatIndex = 0; beatIndex = 0;
for (float s = centrePosition.X - DistanceSpacing; s >= 0; s -= DistanceSpacing, beatIndex++) for (float s = centrePosition.X - DistanceSpacing; s >= 0 && beatIndex < MaxIntervals; s -= DistanceSpacing, beatIndex++)
{ {
AddInternal(new Circle AddInternal(new Circle
{ {
@ -103,7 +124,7 @@ namespace osu.Game.Tests.Visual.Editor
beatIndex = 0; beatIndex = 0;
for (float s = centrePosition.Y + DistanceSpacing; s <= DrawHeight; s += DistanceSpacing, beatIndex++) for (float s = centrePosition.Y + DistanceSpacing; s <= DrawHeight && beatIndex < MaxIntervals; s += DistanceSpacing, beatIndex++)
{ {
AddInternal(new Circle AddInternal(new Circle
{ {
@ -116,7 +137,7 @@ namespace osu.Game.Tests.Visual.Editor
beatIndex = 0; beatIndex = 0;
for (float s = centrePosition.Y - DistanceSpacing; s >= 0; s -= DistanceSpacing, beatIndex++) for (float s = centrePosition.Y - DistanceSpacing; s >= 0 && beatIndex < MaxIntervals; s -= DistanceSpacing, beatIndex++)
{ {
AddInternal(new Circle AddInternal(new Circle
{ {
@ -138,9 +159,9 @@ namespace osu.Game.Tests.Visual.Editor
public float GetBeatSnapDistanceAt(double referenceTime) => 10; public float GetBeatSnapDistanceAt(double referenceTime) => 10;
public float DurationToDistance(double referenceTime, double duration) => 0; public float DurationToDistance(double referenceTime, double duration) => (float)duration;
public double DistanceToDuration(double referenceTime, float distance) => 0; public double DistanceToDuration(double referenceTime, float distance) => distance;
public double GetSnappedDurationFromDistance(double referenceTime, float distance) => 0; public double GetSnappedDurationFromDistance(double referenceTime, float distance) => 0;

View File

@ -10,9 +10,9 @@ 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.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Timing; using osu.Framework.Timing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface;
using osu.Game.Screens.Edit.Compose.Components.Timeline; using osu.Game.Screens.Edit.Compose.Components.Timeline;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
@ -101,7 +101,7 @@ namespace osu.Game.Tests.Visual.Editor
} }
} }
private class StartStopButton : Button private class StartStopButton : OsuButton
{ {
private IAdjustableClock adjustableClock; private IAdjustableClock adjustableClock;
private bool started; private bool started;

View File

@ -0,0 +1,35 @@
// 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 NUnit.Framework;
using osu.Framework.Allocation;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Edit.Timing;
namespace osu.Game.Tests.Visual.Editor
{
[TestFixture]
public class TestSceneTimingScreen : EditorClockTestScene
{
public override IReadOnlyList<Type> RequiredTypes => new[]
{
typeof(ControlPointTable),
typeof(ControlPointSettings),
typeof(Section<>),
typeof(TimingSection),
typeof(EffectSection),
typeof(SampleSection),
typeof(DifficultySection),
typeof(RowAttribute)
};
[BackgroundDependencyLoader]
private void load()
{
Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
Child = new TimingScreen();
}
}
}

View File

@ -69,6 +69,24 @@ namespace osu.Game.Tests.Visual.Gameplay
confirmClockRunning(true); confirmClockRunning(true);
} }
[Test]
public void TestPauseWithResumeOverlay()
{
AddStep("move cursor to center", () => InputManager.MoveMouseTo(Player.ScreenSpaceDrawQuad.Centre));
AddUntilStep("wait for hitobjects", () => Player.ScoreProcessor.Health.Value < 1);
pauseAndConfirm();
resume();
confirmClockRunning(false);
confirmPauseOverlayShown(false);
pauseAndConfirm();
AddUntilStep("resume overlay is not active", () => Player.DrawableRuleset.ResumeOverlay.State.Value == Visibility.Hidden);
confirmPaused();
}
[Test] [Test]
public void TestResumeWithResumeOverlaySkipped() public void TestResumeWithResumeOverlaySkipped()
{ {
@ -219,6 +237,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("player not exited", () => Player.IsCurrentScreen()); AddUntilStep("player not exited", () => Player.IsCurrentScreen());
AddStep("exit", () => Player.Exit()); AddStep("exit", () => Player.Exit());
confirmExited(); confirmExited();
confirmNoTrackAdjustments();
} }
private void confirmPaused() private void confirmPaused()
@ -240,6 +259,11 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("player exited", () => !Player.IsCurrentScreen()); AddUntilStep("player exited", () => !Player.IsCurrentScreen());
} }
private void confirmNoTrackAdjustments()
{
AddAssert("track has no adjustments", () => Beatmap.Value.Track.AggregateFrequency.Value == 1);
}
private void restart() => AddStep("restart", () => Player.Restart()); private void restart() => AddStep("restart", () => Player.Restart());
private void pause() => AddStep("pause", () => Player.Pause()); private void pause() => AddStep("pause", () => Player.Pause());
private void resume() => AddStep("resume", () => Player.Resume()); private void resume() => AddStep("resume", () => Player.Resume());

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,26 +49,82 @@ 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,
Accuracy = 0.98,
MaxCombo = 123,
Rank = ScoreRank.A,
Date = DateTimeOffset.Now,
Statistics = new Dictionary<HitResult, int>
{ {
TotalScore = 2845370, { HitResult.Great, 50 },
Accuracy = 0.98, { HitResult.Good, 20 },
MaxCombo = 123, { HitResult.Meh, 50 },
Rank = ScoreRank.A, { HitResult.Miss, 1 }
Date = DateTimeOffset.Now, },
Statistics = new Dictionary<HitResult, int> User = new User
{
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)
{ {
{ HitResult.Great, 50 }, RelativeSizeAxes = Axes.Both,
{ HitResult.Good, 20 }, };
{ HitResult.Meh, 50 }, }
{ HitResult.Miss, 1 } }
},
User = new User private class TestSoloResults : SoloResults
{ {
Username = "peppy", public HotkeyRetryOverlay RetryOverlay;
}
})); public TestSoloResults(ScoreInfo score)
: base(score)
{
}
protected override void LoadComplete()
{
base.LoadComplete();
RetryOverlay = InternalChildren.OfType<HotkeyRetryOverlay>().SingleOrDefault();
}
} }
} }
} }

View File

@ -0,0 +1,102 @@
// 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 NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Beatmaps;
using osu.Game.Overlays.BeatmapSet;
using osu.Game.Rulesets;
using System;
using System.Collections.Generic;
using System.Linq;
namespace osu.Game.Tests.Visual.Online
{
public class TestSceneBeatmapRulesetSelector : OsuTestScene
{
public override IReadOnlyList<Type> RequiredTypes => new[]
{
typeof(BeatmapRulesetSelector),
typeof(BeatmapRulesetTabItem),
};
private readonly TestRulesetSelector selector;
public TestSceneBeatmapRulesetSelector()
{
Add(selector = new TestRulesetSelector());
}
[Resolved]
private RulesetStore rulesets { get; set; }
[Test]
public void TestMultipleRulesetsBeatmapSet()
{
var enabledRulesets = rulesets.AvailableRulesets.Skip(1).Take(2);
AddStep("load multiple rulesets beatmapset", () =>
{
selector.BeatmapSet = new BeatmapSetInfo
{
Beatmaps = enabledRulesets.Select(r => new BeatmapInfo { Ruleset = r }).ToList()
};
});
var tabItems = selector.TabContainer.TabItems;
AddAssert("other rulesets disabled", () => tabItems.Except(tabItems.Where(t => enabledRulesets.Any(r => r.Equals(t.Value)))).All(t => !t.Enabled.Value));
AddAssert("left-most ruleset selected", () => tabItems.First(t => t.Enabled.Value).Active.Value);
}
[Test]
public void TestSingleRulesetBeatmapSet()
{
var enabledRuleset = rulesets.AvailableRulesets.Last();
AddStep("load single ruleset beatmapset", () =>
{
selector.BeatmapSet = new BeatmapSetInfo
{
Beatmaps = new List<BeatmapInfo>
{
new BeatmapInfo
{
Ruleset = enabledRuleset
}
}
};
});
AddAssert("single ruleset selected", () => selector.SelectedTab.Value.Equals(enabledRuleset));
}
[Test]
public void TestEmptyBeatmapSet()
{
AddStep("load empty beatmapset", () => selector.BeatmapSet = new BeatmapSetInfo
{
Beatmaps = new List<BeatmapInfo>()
});
AddAssert("no ruleset selected", () => selector.SelectedTab == null);
AddAssert("all rulesets disabled", () => selector.TabContainer.TabItems.All(t => !t.Enabled.Value));
}
[Test]
public void TestNullBeatmapSet()
{
AddStep("load null beatmapset", () => selector.BeatmapSet = null);
AddAssert("no ruleset selected", () => selector.SelectedTab == null);
AddAssert("all rulesets disabled", () => selector.TabContainer.TabItems.All(t => !t.Enabled.Value));
}
private class TestRulesetSelector : BeatmapRulesetSelector
{
public new TabItem<RulesetInfo> SelectedTab => base.SelectedTab;
public new TabFillFlowContainer TabContainer => base.TabContainer;
}
}
}

View File

@ -40,24 +40,19 @@ namespace osu.Game.Tests.Visual.Online
typeof(PreviewButton), typeof(PreviewButton),
typeof(SuccessRate), typeof(SuccessRate),
typeof(BeatmapAvailability), typeof(BeatmapAvailability),
typeof(BeatmapRulesetSelector),
typeof(BeatmapRulesetTabItem),
}; };
protected override bool UseOnlineAPI => true; protected override bool UseOnlineAPI => true;
private RulesetInfo taikoRuleset;
private RulesetInfo maniaRuleset;
public TestSceneBeatmapSetOverlay() public TestSceneBeatmapSetOverlay()
{ {
Add(overlay = new TestBeatmapSetOverlay()); Add(overlay = new TestBeatmapSetOverlay());
} }
[BackgroundDependencyLoader] [Resolved]
private void load(RulesetStore rulesets) private RulesetStore rulesets { get; set; }
{
taikoRuleset = rulesets.GetRuleset(1);
maniaRuleset = rulesets.GetRuleset(3);
}
[Test] [Test]
public void TestLoading() public void TestLoading()
@ -111,7 +106,7 @@ namespace osu.Game.Tests.Visual.Online
StarDifficulty = 9.99, StarDifficulty = 9.99,
Version = @"TEST", Version = @"TEST",
Length = 456000, Length = 456000,
Ruleset = maniaRuleset, Ruleset = rulesets.GetRuleset(3),
BaseDifficulty = new BeatmapDifficulty BaseDifficulty = new BeatmapDifficulty
{ {
CircleSize = 1, CircleSize = 1,
@ -189,7 +184,7 @@ namespace osu.Game.Tests.Visual.Online
StarDifficulty = 5.67, StarDifficulty = 5.67,
Version = @"ANOTHER TEST", Version = @"ANOTHER TEST",
Length = 123000, Length = 123000,
Ruleset = taikoRuleset, Ruleset = rulesets.GetRuleset(1),
BaseDifficulty = new BeatmapDifficulty BaseDifficulty = new BeatmapDifficulty
{ {
CircleSize = 9, CircleSize = 9,
@ -217,6 +212,54 @@ namespace osu.Game.Tests.Visual.Online
downloadAssert(false); downloadAssert(false);
} }
[Test]
public void TestMultipleRulesets()
{
AddStep("show multiple rulesets beatmap", () =>
{
var beatmaps = new List<BeatmapInfo>();
foreach (var ruleset in rulesets.AvailableRulesets.Skip(1))
{
beatmaps.Add(new BeatmapInfo
{
Version = ruleset.Name,
Ruleset = ruleset,
BaseDifficulty = new BeatmapDifficulty(),
OnlineInfo = new BeatmapOnlineInfo(),
Metrics = new BeatmapMetrics
{
Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6).ToArray(),
Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6).ToArray(),
},
});
}
overlay.ShowBeatmapSet(new BeatmapSetInfo
{
Metadata = new BeatmapMetadata
{
Title = @"multiple rulesets beatmap",
Artist = @"none",
Author = new User
{
Username = "BanchoBot",
Id = 3,
}
},
OnlineInfo = new BeatmapSetOnlineInfo
{
Covers = new BeatmapSetOnlineCovers(),
},
Metrics = new BeatmapSetMetrics { Ratings = Enumerable.Range(0, 11).ToArray() },
Beatmaps = beatmaps
});
});
AddAssert("shown beatmaps of current ruleset", () => overlay.Header.Picker.Difficulties.All(b => b.Beatmap.Ruleset.Equals(overlay.Header.RulesetSelector.Current.Value)));
AddAssert("left-most beatmap selected", () => overlay.Header.Picker.Difficulties.First().State == BeatmapPicker.DifficultySelectorState.Selected);
}
[Test] [Test]
public void TestHide() public void TestHide()
{ {
@ -281,12 +324,12 @@ namespace osu.Game.Tests.Visual.Online
private void downloadAssert(bool shown) private void downloadAssert(bool shown)
{ {
AddAssert($"is download button {(shown ? "shown" : "hidden")}", () => overlay.DownloadButtonsVisible == shown); AddAssert($"is download button {(shown ? "shown" : "hidden")}", () => overlay.Header.DownloadButtonsVisible == shown);
} }
private class TestBeatmapSetOverlay : BeatmapSetOverlay private class TestBeatmapSetOverlay : BeatmapSetOverlay
{ {
public bool DownloadButtonsVisible => Header.DownloadButtonsVisible; public new Header Header => base.Header;
} }
} }
} }

View File

@ -3,7 +3,7 @@
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="DeepEqual" Version="2.0.0" /> <PackageReference Include="DeepEqual" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.3.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" />
<PackageReference Include="NUnit" Version="3.12.0" /> <PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.15.1" /> <PackageReference Include="NUnit3TestAdapter" Version="3.15.1" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" /> <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />

View File

@ -5,7 +5,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.3.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" />
<PackageReference Include="NUnit" Version="3.12.0" /> <PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.15.1" /> <PackageReference Include="NUnit3TestAdapter" Version="3.15.1" />
</ItemGroup> </ItemGroup>

View File

@ -7,6 +7,7 @@ using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Video; using osu.Framework.Graphics.Video;
using osu.Framework.Timing;
using osu.Game.Graphics; using osu.Game.Graphics;
namespace osu.Game.Tournament.Components namespace osu.Game.Tournament.Components
@ -15,6 +16,8 @@ namespace osu.Game.Tournament.Components
{ {
private readonly VideoSprite video; private readonly VideoSprite video;
private readonly ManualClock manualClock;
public TourneyVideo(Stream stream) public TourneyVideo(Stream stream)
{ {
if (stream == null) if (stream == null)
@ -30,6 +33,7 @@ namespace osu.Game.Tournament.Components
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fit, FillMode = FillMode.Fit,
Clock = new FramedClock(manualClock = new ManualClock())
}; };
} }
@ -41,5 +45,17 @@ namespace osu.Game.Tournament.Components
video.Loop = value; video.Loop = value;
} }
} }
protected override void Update()
{
base.Update();
if (manualClock != null && Clock.ElapsedFrameTime < 100)
{
// we want to avoid seeking as much as possible, because we care about performance, not sync.
// to avoid seeking completely, we only increment out local clock when in an updating state.
manualClock.CurrentTime += Clock.ElapsedFrameTime;
}
}
} }
} }

View File

@ -15,7 +15,6 @@ using osu.Framework.Logging;
using osu.Framework.Platform; using osu.Framework.Platform;
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.Tournament.Components; using osu.Game.Tournament.Components;
using osu.Game.Tournament.Models; using osu.Game.Tournament.Models;
using osu.Game.Tournament.Screens.Drawings.Components; using osu.Game.Tournament.Screens.Drawings.Components;
@ -24,7 +23,7 @@ using osuTK.Graphics;
namespace osu.Game.Tournament.Screens.Drawings namespace osu.Game.Tournament.Screens.Drawings
{ {
public class DrawingsScreen : CompositeDrawable public class DrawingsScreen : TournamentScreen
{ {
private const string results_filename = "drawings_results.txt"; private const string results_filename = "drawings_results.txt";
@ -128,21 +127,21 @@ namespace osu.Game.Tournament.Screens.Drawings
// Control panel container // Control panel container
new ControlPanel new ControlPanel
{ {
new OsuButton new TourneyButton
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Text = "Begin random", Text = "Begin random",
Action = teamsContainer.StartScrolling, Action = teamsContainer.StartScrolling,
}, },
new OsuButton new TourneyButton
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Text = "Stop random", Text = "Stop random",
Action = teamsContainer.StopScrolling, Action = teamsContainer.StopScrolling,
}, },
new OsuButton new TourneyButton
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
@ -150,7 +149,7 @@ namespace osu.Game.Tournament.Screens.Drawings
Action = reloadTeams Action = reloadTeams
}, },
new ControlPanel.Spacer(), new ControlPanel.Spacer(),
new OsuButton new TourneyButton
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,

View File

@ -11,7 +11,6 @@ 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.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests;
using osu.Game.Overlays.Settings; using osu.Game.Overlays.Settings;
@ -32,7 +31,7 @@ namespace osu.Game.Tournament.Screens.Editors
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
ControlPanel.Add(new OsuButton ControlPanel.Add(new TourneyButton
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Text = "Add all countries", Text = "Add all countries",

View File

@ -9,7 +9,6 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
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.Overlays.Settings; using osu.Game.Overlays.Settings;
using osu.Game.Tournament.Components; using osu.Game.Tournament.Components;
using osuTK; using osuTK;
@ -56,7 +55,7 @@ namespace osu.Game.Tournament.Screens.Editors
{ {
Children = new Drawable[] Children = new Drawable[]
{ {
new OsuButton new TourneyButton
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Text = "Add new", Text = "Add new",

View File

@ -103,13 +103,13 @@ namespace osu.Game.Tournament.Screens.Gameplay
{ {
Children = new Drawable[] Children = new Drawable[]
{ {
warmupButton = new OsuButton warmupButton = new TourneyButton
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Text = "Toggle warmup", Text = "Toggle warmup",
Action = () => warmup.Toggle() Action = () => warmup.Toggle()
}, },
new OsuButton new TourneyButton
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Text = "Toggle chat", Text = "Toggle chat",

View File

@ -60,32 +60,32 @@ namespace osu.Game.Tournament.Screens.MapPool
{ {
Text = "Current Mode" Text = "Current Mode"
}, },
buttonRedBan = new OsuButton buttonRedBan = new TourneyButton
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Text = "Red Ban", Text = "Red Ban",
Action = () => setMode(TeamColour.Red, ChoiceType.Ban) Action = () => setMode(TeamColour.Red, ChoiceType.Ban)
}, },
buttonBlueBan = new OsuButton buttonBlueBan = new TourneyButton
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Text = "Blue Ban", Text = "Blue Ban",
Action = () => setMode(TeamColour.Blue, ChoiceType.Ban) Action = () => setMode(TeamColour.Blue, ChoiceType.Ban)
}, },
buttonRedPick = new OsuButton buttonRedPick = new TourneyButton
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Text = "Red Pick", Text = "Red Pick",
Action = () => setMode(TeamColour.Red, ChoiceType.Pick) Action = () => setMode(TeamColour.Red, ChoiceType.Pick)
}, },
buttonBluePick = new OsuButton buttonBluePick = new TourneyButton
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Text = "Blue Pick", Text = "Blue Pick",
Action = () => setMode(TeamColour.Blue, ChoiceType.Pick) Action = () => setMode(TeamColour.Blue, ChoiceType.Pick)
}, },
new ControlPanel.Spacer(), new ControlPanel.Spacer(),
new OsuButton new TourneyButton
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Text = "Reset", Text = "Reset",

View File

@ -10,6 +10,8 @@ namespace osu.Game.Tournament.Screens
{ {
public abstract class TournamentScreen : CompositeDrawable public abstract class TournamentScreen : CompositeDrawable
{ {
public const double FADE_DELAY = 200;
[Resolved] [Resolved]
protected LadderInfo LadderInfo { get; private set; } protected LadderInfo LadderInfo { get; private set; }
@ -18,14 +20,8 @@ namespace osu.Game.Tournament.Screens
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
} }
public override void Hide() public override void Hide() => this.FadeOut(FADE_DELAY);
{
this.FadeOut(200);
}
public override void Show() public override void Show() => this.FadeIn(FADE_DELAY);
{
this.FadeIn(200);
}
} }
} }

View File

@ -18,7 +18,6 @@ using osu.Framework.IO.Stores;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests;
using osu.Game.Tournament.IPC; using osu.Game.Tournament.IPC;
using osu.Game.Tournament.Models; using osu.Game.Tournament.Models;
@ -76,7 +75,7 @@ namespace osu.Game.Tournament
AddRange(new[] AddRange(new[]
{ {
new OsuButton new TourneyButton
{ {
Text = "Save Changes", Text = "Save Changes",
Width = 140, Width = 140,
@ -215,7 +214,7 @@ namespace osu.Game.Tournament
foreach (var r in ladder.Rounds) foreach (var r in ladder.Rounds)
foreach (var b in r.Beatmaps) foreach (var b in r.Beatmaps)
if (b.BeatmapInfo == null) if (b.BeatmapInfo == null && b.ID > 0)
{ {
var req = new GetBeatmapRequest(new BeatmapInfo { OnlineBeatmapID = b.ID }); var req = new GetBeatmapRequest(new BeatmapInfo { OnlineBeatmapID = b.ID });
req.Perform(API); req.Perform(API);

View File

@ -8,7 +8,8 @@ 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.Platform; using osu.Framework.Platform;
using osu.Game.Graphics.UserInterface; using osu.Framework.Threading;
using osu.Game.Graphics;
using osu.Game.Tournament.Components; using osu.Game.Tournament.Components;
using osu.Game.Tournament.Models; using osu.Game.Tournament.Models;
using osu.Game.Tournament.Screens; using osu.Game.Tournament.Screens;
@ -36,6 +37,7 @@ namespace osu.Game.Tournament
private TournamentMatchChatDisplay chat = new TournamentMatchChatDisplay(); private TournamentMatchChatDisplay chat = new TournamentMatchChatDisplay();
private Container chatContainer; private Container chatContainer;
private FillFlowContainer buttons;
public TournamentSceneManager() public TournamentSceneManager()
{ {
@ -101,68 +103,136 @@ namespace osu.Game.Tournament
Colour = Color4.Black, Colour = Color4.Black,
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
}, },
new FillFlowContainer buttons = new FillFlowContainer
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Direction = FillDirection.Vertical, Direction = FillDirection.Vertical,
Spacing = new Vector2(2),
Padding = new MarginPadding(2),
Children = new Drawable[] Children = new Drawable[]
{ {
new OsuButton { RelativeSizeAxes = Axes.X, Text = "Setup", Action = () => SetScreen(typeof(SetupScreen)) }, new ScreenButton(typeof(SetupScreen)) { Text = "Setup", RequestSelection = SetScreen },
new Container { RelativeSizeAxes = Axes.X, Height = 50 }, new Separator(),
new OsuButton { RelativeSizeAxes = Axes.X, Text = "Team Editor", Action = () => SetScreen(typeof(TeamEditorScreen)) }, new ScreenButton(typeof(TeamEditorScreen)) { Text = "Team Editor", RequestSelection = SetScreen },
new OsuButton { RelativeSizeAxes = Axes.X, Text = "Rounds Editor", Action = () => SetScreen(typeof(RoundEditorScreen)) }, new ScreenButton(typeof(RoundEditorScreen)) { Text = "Rounds Editor", RequestSelection = SetScreen },
new OsuButton { RelativeSizeAxes = Axes.X, Text = "Bracket Editor", Action = () => SetScreen(typeof(LadderEditorScreen)) }, new ScreenButton(typeof(LadderEditorScreen)) { Text = "Bracket Editor", RequestSelection = SetScreen },
new Container { RelativeSizeAxes = Axes.X, Height = 50 }, new Separator(),
new OsuButton { RelativeSizeAxes = Axes.X, Text = "Drawings", Action = () => SetScreen(typeof(DrawingsScreen)) }, new ScreenButton(typeof(ScheduleScreen)) { Text = "Schedule", RequestSelection = SetScreen },
new OsuButton { RelativeSizeAxes = Axes.X, Text = "Showcase", Action = () => SetScreen(typeof(ShowcaseScreen)) }, new ScreenButton(typeof(LadderScreen)) { Text = "Bracket", RequestSelection = SetScreen },
new Container { RelativeSizeAxes = Axes.X, Height = 50 }, new Separator(),
new OsuButton { RelativeSizeAxes = Axes.X, Text = "Schedule", Action = () => SetScreen(typeof(ScheduleScreen)) }, new ScreenButton(typeof(TeamIntroScreen)) { Text = "TeamIntro", RequestSelection = SetScreen },
new OsuButton { RelativeSizeAxes = Axes.X, Text = "Bracket", Action = () => SetScreen(typeof(LadderScreen)) }, new Separator(),
new Container { RelativeSizeAxes = Axes.X, Height = 50 }, new ScreenButton(typeof(MapPoolScreen)) { Text = "MapPool", RequestSelection = SetScreen },
new OsuButton { RelativeSizeAxes = Axes.X, Text = "TeamIntro", Action = () => SetScreen(typeof(TeamIntroScreen)) }, new ScreenButton(typeof(GameplayScreen)) { Text = "Gameplay", RequestSelection = SetScreen },
new OsuButton { RelativeSizeAxes = Axes.X, Text = "MapPool", Action = () => SetScreen(typeof(MapPoolScreen)) }, new Separator(),
new OsuButton { RelativeSizeAxes = Axes.X, Text = "Gameplay", Action = () => SetScreen(typeof(GameplayScreen)) }, new ScreenButton(typeof(TeamWinScreen)) { Text = "Win", RequestSelection = SetScreen },
new Container { RelativeSizeAxes = Axes.X, Height = 50 }, new Separator(),
new OsuButton { RelativeSizeAxes = Axes.X, Text = "Win", Action = () => SetScreen(typeof(TeamWinScreen)) }, new ScreenButton(typeof(DrawingsScreen)) { Text = "Drawings", RequestSelection = SetScreen },
new ScreenButton(typeof(ShowcaseScreen)) { Text = "Showcase", RequestSelection = SetScreen },
} }
}, },
}, },
}, },
}; };
foreach (var drawable in screens)
drawable.Hide();
SetScreen(typeof(SetupScreen)); SetScreen(typeof(SetupScreen));
} }
private float depth;
private Drawable currentScreen;
private ScheduledDelegate scheduledHide;
public void SetScreen(Type screenType) public void SetScreen(Type screenType)
{ {
var screen = screens.FirstOrDefault(s => s.GetType() == screenType); var target = screens.FirstOrDefault(s => s.GetType() == screenType);
if (screen == null) return;
foreach (var s in screens.Children) if (target == null || currentScreen == target) return;
if (scheduledHide?.Completed == false)
{ {
if (s == screen) scheduledHide.RunTask();
{ scheduledHide.Cancel(); // see https://github.com/ppy/osu-framework/issues/2967
s.Show(); scheduledHide = null;
if (s is IProvideVideo)
video.FadeOut(200);
else
video.Show();
}
else
s.Hide();
} }
switch (screen) var lastScreen = currentScreen;
currentScreen = target;
if (currentScreen is IProvideVideo)
{
video.FadeOut(200);
// delay the hide to avoid a double-fade transition.
scheduledHide = Scheduler.AddDelayed(() => lastScreen?.Hide(), TournamentScreen.FADE_DELAY);
}
else
{
lastScreen?.Hide();
video.Show();
}
screens.ChangeChildDepth(currentScreen, depth--);
currentScreen.Show();
switch (currentScreen)
{ {
case GameplayScreen _: case GameplayScreen _:
case MapPoolScreen _: case MapPoolScreen _:
chatContainer.FadeIn(100); chatContainer.FadeIn(TournamentScreen.FADE_DELAY);
break; break;
default: default:
chatContainer.FadeOut(100); chatContainer.FadeOut(TournamentScreen.FADE_DELAY);
break; break;
} }
foreach (var s in buttons.OfType<ScreenButton>())
s.IsSelected = screenType == s.Type;
}
private class Separator : CompositeDrawable
{
public Separator()
{
RelativeSizeAxes = Axes.X;
Height = 20;
}
}
private class ScreenButton : TourneyButton
{
public readonly Type Type;
public ScreenButton(Type type)
{
Type = type;
BackgroundColour = OsuColour.Gray(0.2f);
Action = () => RequestSelection(type);
RelativeSizeAxes = Axes.X;
}
private bool isSelected;
public Action<Type> RequestSelection;
public bool IsSelected
{
get => isSelected;
set
{
if (value == isSelected)
return;
isSelected = value;
BackgroundColour = isSelected ? Color4.SkyBlue : OsuColour.Gray(0.2f);
SpriteText.Colour = isSelected ? Color4.Black : Color4.White;
}
}
} }
} }
} }

View File

@ -0,0 +1,15 @@
// 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.Graphics.UserInterface;
namespace osu.Game.Tournament
{
public class TourneyButton : OsuButton
{
public TourneyButton()
: base(null)
{
}
}
}

View File

@ -9,15 +9,18 @@ using osu.Framework.Threading;
namespace osu.Game.Audio namespace osu.Game.Audio
{ {
[LongRunningLoad]
public abstract class PreviewTrack : Component public abstract class PreviewTrack : Component
{ {
/// <summary> /// <summary>
/// Invoked when this <see cref="PreviewTrack"/> has stopped playing. /// Invoked when this <see cref="PreviewTrack"/> has stopped playing.
/// Not invoked in a thread-safe context.
/// </summary> /// </summary>
public event Action Stopped; public event Action Stopped;
/// <summary> /// <summary>
/// Invoked when this <see cref="PreviewTrack"/> has started playing. /// Invoked when this <see cref="PreviewTrack"/> has started playing.
/// Not invoked in a thread-safe context.
/// </summary> /// </summary>
public event Action Started; public event Action Started;
@ -29,7 +32,7 @@ namespace osu.Game.Audio
{ {
track = GetTrack(); track = GetTrack();
if (track != null) if (track != null)
track.Completed += () => Schedule(Stop); track.Completed += Stop;
} }
/// <summary> /// <summary>
@ -93,6 +96,7 @@ namespace osu.Game.Audio
hasStarted = false; hasStarted = false;
track.Stop(); track.Stop();
Stopped?.Invoke(); Stopped?.Invoke();
} }

View File

@ -46,18 +46,18 @@ namespace osu.Game.Audio
{ {
var track = CreatePreviewTrack(beatmapSetInfo, trackStore); var track = CreatePreviewTrack(beatmapSetInfo, trackStore);
track.Started += () => track.Started += () => Schedule(() =>
{ {
current?.Stop(); current?.Stop();
current = track; current = track;
audio.Tracks.AddAdjustment(AdjustableProperty.Volume, muteBindable); audio.Tracks.AddAdjustment(AdjustableProperty.Volume, muteBindable);
}; });
track.Stopped += () => track.Stopped += () => Schedule(() =>
{ {
current = null; current = null;
audio.Tracks.RemoveAdjustment(AdjustableProperty.Volume, muteBindable); audio.Tracks.RemoveAdjustment(AdjustableProperty.Volume, muteBindable);
}; });
return track; return track;
} }

View File

@ -392,8 +392,15 @@ namespace osu.Game.Beatmaps
req.Failure += e => { LogForModel(set, $"Online retrieval failed for {beatmap} ({e.Message})"); }; req.Failure += e => { LogForModel(set, $"Online retrieval failed for {beatmap} ({e.Message})"); };
// intentionally blocking to limit web request concurrency try
req.Perform(api); {
// intentionally blocking to limit web request concurrency
req.Perform(api);
}
catch (Exception e)
{
LogForModel(set, $"Online retrieval failed for {beatmap} ({e.Message})");
}
} }
} }
} }

View File

@ -43,6 +43,11 @@ namespace osu.Game.Beatmaps.ControlPoints
set => BeatLengthBindable.Value = value; set => BeatLengthBindable.Value = value;
} }
/// <summary>
/// The BPM at this control point.
/// </summary>
public double BPM => 60000 / BeatLength;
public override bool EquivalentTo(ControlPoint other) => public override bool EquivalentTo(ControlPoint other) =>
other is TimingControlPoint otherTyped other is TimingControlPoint otherTyped
&& TimeSignature == otherTyped.TimeSignature && BeatLength.Equals(otherTyped.BeatLength); && TimeSignature == otherTyped.TimeSignature && BeatLength.Equals(otherTyped.BeatLength);

View File

@ -8,6 +8,7 @@ using osu.Framework.Graphics.Textures;
namespace osu.Game.Beatmaps.Drawables namespace osu.Game.Beatmaps.Drawables
{ {
[LongRunningLoad]
public class BeatmapSetCover : Sprite public class BeatmapSetCover : Sprite
{ {
private readonly BeatmapSetInfo set; private readonly BeatmapSetInfo set;

View File

@ -86,16 +86,7 @@ namespace osu.Game.Database
}, TaskCreationOptions.LongRunning); }, TaskCreationOptions.LongRunning);
}; };
request.Failure += error => request.Failure += triggerFailure;
{
DownloadFailed?.Invoke(request);
if (error is OperationCanceledException) return;
notification.State = ProgressNotificationState.Cancelled;
Logger.Error(error, $"{HumanisedModelName.Titleize()} download failed!");
currentDownloads.Remove(request);
};
notification.CancelRequested += () => notification.CancelRequested += () =>
{ {
@ -108,11 +99,31 @@ namespace osu.Game.Database
currentDownloads.Add(request); currentDownloads.Add(request);
PostNotification?.Invoke(notification); PostNotification?.Invoke(notification);
Task.Factory.StartNew(() => request.Perform(api), TaskCreationOptions.LongRunning); Task.Factory.StartNew(() =>
{
try
{
request.Perform(api);
}
catch (Exception error)
{
triggerFailure(error);
}
}, TaskCreationOptions.LongRunning);
DownloadBegan?.Invoke(request); DownloadBegan?.Invoke(request);
return true; return true;
void triggerFailure(Exception error)
{
DownloadFailed?.Invoke(request);
if (error is OperationCanceledException) return;
notification.State = ProgressNotificationState.Cancelled;
Logger.Error(error, $"{HumanisedModelName.Titleize()} download failed!");
currentDownloads.Remove(request);
}
} }
public bool IsAvailableLocally(TModel model) => CheckLocalAvailability(model, modelStore.ConsumableItems.Where(m => !m.DeletePending)); public bool IsAvailableLocally(TModel model) => CheckLocalAvailability(model, modelStore.ConsumableItems.Where(m => !m.DeletePending));

View File

@ -8,9 +8,6 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using System.Collections.Generic; using System.Collections.Generic;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Logging;
using osu.Game.Overlays;
using osu.Game.Overlays.Notifications;
using osu.Game.Users; using osu.Game.Users;
namespace osu.Game.Graphics.Containers namespace osu.Game.Graphics.Containers
@ -23,21 +20,12 @@ namespace osu.Game.Graphics.Containers
} }
private OsuGame game; private OsuGame game;
private ChannelManager channelManager;
private Action showNotImplementedError;
[BackgroundDependencyLoader(true)] [BackgroundDependencyLoader(true)]
private void load(OsuGame game, NotificationOverlay notifications, ChannelManager channelManager) private void load(OsuGame game)
{ {
// will be null in tests // will be null in tests
this.game = game; this.game = game;
this.channelManager = channelManager;
showNotImplementedError = () => notifications?.Post(new SimpleNotification
{
Text = @"This link type is not yet supported!",
Icon = FontAwesome.Solid.LifeRing,
});
} }
public void AddLinks(string text, List<Link> links) public void AddLinks(string text, List<Link> links)
@ -56,85 +44,47 @@ namespace osu.Game.Graphics.Containers
foreach (var link in links) foreach (var link in links)
{ {
AddText(text.Substring(previousLinkEnd, link.Index - previousLinkEnd)); AddText(text.Substring(previousLinkEnd, link.Index - previousLinkEnd));
AddLink(text.Substring(link.Index, link.Length), link.Url, link.Action, link.Argument); AddLink(text.Substring(link.Index, link.Length), link.Action, link.Argument ?? link.Url);
previousLinkEnd = link.Index + link.Length; previousLinkEnd = link.Index + link.Length;
} }
AddText(text.Substring(previousLinkEnd)); AddText(text.Substring(previousLinkEnd));
} }
public IEnumerable<Drawable> AddLink(string text, string url, LinkAction linkType = LinkAction.External, string linkArgument = null, string tooltipText = null, Action<SpriteText> creationParameters = null) public void AddLink(string text, string url, Action<SpriteText> creationParameters = null) =>
=> createLink(AddText(text, creationParameters), text, url, linkType, linkArgument, tooltipText); createLink(AddText(text, creationParameters), new LinkDetails(LinkAction.External, url), url);
public IEnumerable<Drawable> AddLink(string text, Action action, string tooltipText = null, Action<SpriteText> creationParameters = null) public void AddLink(string text, Action action, string tooltipText = null, Action<SpriteText> creationParameters = null)
=> createLink(AddText(text, creationParameters), text, tooltipText: tooltipText, action: action); => createLink(AddText(text, creationParameters), new LinkDetails(LinkAction.Custom, null), tooltipText, action);
public IEnumerable<Drawable> AddLink(IEnumerable<SpriteText> text, string url, LinkAction linkType = LinkAction.External, string linkArgument = null, string tooltipText = null) public void AddLink(string text, LinkAction action, string argument, string tooltipText = null, Action<SpriteText> creationParameters = null)
=> createLink(AddText(text, creationParameters), new LinkDetails(action, argument), null);
public void AddLink(IEnumerable<SpriteText> text, LinkAction action = LinkAction.External, string linkArgument = null, string tooltipText = null)
{ {
foreach (var t in text) foreach (var t in text)
AddArbitraryDrawable(t); AddArbitraryDrawable(t);
return createLink(text, null, url, linkType, linkArgument, tooltipText); createLink(text, new LinkDetails(action, linkArgument), tooltipText);
} }
public IEnumerable<Drawable> AddUserLink(User user, Action<SpriteText> creationParameters = null) public void AddUserLink(User user, Action<SpriteText> creationParameters = null)
=> createLink(AddText(user.Username, creationParameters), user.Username, null, LinkAction.OpenUserProfile, user.Id.ToString(), "View profile"); => createLink(AddText(user.Username, creationParameters), new LinkDetails(LinkAction.OpenUserProfile, user.Id.ToString()), "View Profile");
private IEnumerable<Drawable> createLink(IEnumerable<Drawable> drawables, string text, string url = null, LinkAction linkType = LinkAction.External, string linkArgument = null, string tooltipText = null, Action action = null) private void createLink(IEnumerable<Drawable> drawables, LinkDetails link, string tooltipText, Action action = null)
{ {
AddInternal(new DrawableLinkCompiler(drawables.OfType<SpriteText>().ToList()) AddInternal(new DrawableLinkCompiler(drawables.OfType<SpriteText>().ToList())
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
TooltipText = tooltipText ?? (url != text ? url : string.Empty), TooltipText = tooltipText,
Action = action ?? (() => Action = () =>
{ {
switch (linkType) if (action != null)
{ action();
case LinkAction.OpenBeatmap: else
// TODO: proper query params handling game.HandleLink(link);
if (linkArgument != null && int.TryParse(linkArgument.Contains('?') ? linkArgument.Split('?')[0] : linkArgument, out int beatmapId)) },
game?.ShowBeatmap(beatmapId);
break;
case LinkAction.OpenBeatmapSet:
if (int.TryParse(linkArgument, out int setId))
game?.ShowBeatmapSet(setId);
break;
case LinkAction.OpenChannel:
try
{
channelManager?.OpenChannel(linkArgument);
}
catch (ChannelNotFoundException)
{
Logger.Log($"The requested channel \"{linkArgument}\" does not exist");
}
break;
case LinkAction.OpenEditorTimestamp:
case LinkAction.JoinMultiplayerMatch:
case LinkAction.Spectate:
showNotImplementedError?.Invoke();
break;
case LinkAction.External:
game?.OpenUrlExternally(url);
break;
case LinkAction.OpenUserProfile:
if (long.TryParse(linkArgument, out long userId))
game?.ShowUser(userId);
break;
default:
throw new NotImplementedException($"This {nameof(LinkAction)} ({linkType.ToString()}) is missing an associated action.");
}
}),
}); });
return drawables;
} }
// We want the compilers to always be visible no matter where they are, so RelativeSizeAxes is used. // We want the compilers to always be visible no matter where they are, so RelativeSizeAxes is used.

View File

@ -1,10 +1,12 @@
// 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.
using System.Diagnostics;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
@ -19,53 +21,106 @@ namespace osu.Game.Graphics.UserInterface
/// </summary> /// </summary>
public class OsuButton : Button public class OsuButton : Button
{ {
private Box hover; public string Text
{
get => SpriteText?.Text;
set
{
if (SpriteText != null)
SpriteText.Text = value;
}
}
public OsuButton() private Color4? backgroundColour;
public Color4 BackgroundColour
{
set
{
backgroundColour = value;
Background.FadeColour(value);
}
}
protected override Container<Drawable> Content { get; }
protected Box Hover;
protected Box Background;
protected SpriteText SpriteText;
public OsuButton(HoverSampleSet? hoverSounds = HoverSampleSet.Loud)
{ {
Height = 40; Height = 40;
Content.Masking = true; AddInternal(Content = new Container
Content.CornerRadius = 5; {
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Masking = true,
CornerRadius = 5,
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
Background = new Box
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
},
Hover = new Box
{
Alpha = 0,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Colour = Color4.White.Opacity(.1f),
Blending = BlendingParameters.Additive,
Depth = float.MinValue
},
SpriteText = CreateText(),
}
});
if (hoverSounds.HasValue)
AddInternal(new HoverClickSounds(hoverSounds.Value));
Enabled.BindValueChanged(enabledChanged, true);
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colours) private void load(OsuColour colours)
{ {
BackgroundColour = colours.BlueDark; if (backgroundColour == null)
BackgroundColour = colours.BlueDark;
AddRange(new Drawable[]
{
hover = new Box
{
RelativeSizeAxes = Axes.Both,
Blending = BlendingParameters.Additive,
Colour = Color4.White.Opacity(0.1f),
Alpha = 0,
Depth = -1
},
new HoverClickSounds(HoverSampleSet.Loud),
});
Enabled.ValueChanged += enabledChanged; Enabled.ValueChanged += enabledChanged;
Enabled.TriggerChange(); Enabled.TriggerChange();
} }
private void enabledChanged(ValueChangedEvent<bool> e) protected override bool OnClick(ClickEvent e)
{ {
this.FadeColour(e.NewValue ? Color4.White : Color4.Gray, 200, Easing.OutQuint); if (Enabled.Value)
{
Debug.Assert(backgroundColour != null);
Background.FlashColour(backgroundColour.Value, 200);
}
return base.OnClick(e);
} }
protected override bool OnHover(HoverEvent e) protected override bool OnHover(HoverEvent e)
{ {
hover.FadeIn(200); if (Enabled.Value)
return true; Hover.FadeIn(200, Easing.OutQuint);
return base.OnHover(e);
} }
protected override void OnHoverLost(HoverLostEvent e) protected override void OnHoverLost(HoverLostEvent e)
{ {
hover.FadeOut(200);
base.OnHoverLost(e); base.OnHoverLost(e);
Hover.FadeOut(300);
} }
protected override bool OnMouseDown(MouseDownEvent e) protected override bool OnMouseDown(MouseDownEvent e)
@ -80,12 +135,17 @@ namespace osu.Game.Graphics.UserInterface
return base.OnMouseUp(e); return base.OnMouseUp(e);
} }
protected override SpriteText CreateText() => new OsuSpriteText protected virtual SpriteText CreateText() => new OsuSpriteText
{ {
Depth = -1, Depth = -1,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Font = OsuFont.GetFont(weight: FontWeight.Bold) Font = OsuFont.GetFont(weight: FontWeight.Bold)
}; };
private void enabledChanged(ValueChangedEvent<bool> e)
{
this.FadeColour(e.NewValue ? Color4.White : Color4.Gray, 200, Easing.OutQuint);
}
} }
} }

View File

@ -1,30 +1,16 @@
// 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.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using osu.Framework.IO.Stores;
using SharpCompress.Archives.Zip; using SharpCompress.Archives.Zip;
using SharpCompress.Common;
namespace osu.Game.IO.Archives namespace osu.Game.IO.Archives
{ {
public sealed class ZipArchiveReader : ArchiveReader public sealed class ZipArchiveReader : ArchiveReader
{ {
/// <summary>
/// List of substrings that indicate a file should be ignored during the import process
/// (usually due to representing no useful data and being autogenerated by the OS).
/// </summary>
private static readonly string[] filename_ignore_list =
{
// Mac-specific
"__MACOSX",
".DS_Store",
// Windows-specific
"Thumbs.db"
};
private readonly Stream archiveStream; private readonly Stream archiveStream;
private readonly ZipArchive archive; private readonly ZipArchive archive;
@ -58,9 +44,7 @@ namespace osu.Game.IO.Archives
archiveStream.Dispose(); archiveStream.Dispose();
} }
private static bool canBeIgnored(IEntry entry) => filename_ignore_list.Any(ignoredName => entry.Key.IndexOf(ignoredName, StringComparison.OrdinalIgnoreCase) >= 0); public override IEnumerable<string> Filenames => archive.Entries.Select(e => e.Key).ExcludeSystemFileNames();
public override IEnumerable<string> Filenames => archive.Entries.Where(e => !canBeIgnored(e)).Select(e => e.Key).ToArray();
public override Stream GetUnderlyingStream() => archiveStream; public override Stream GetUnderlyingStream() => archiveStream;
} }

View File

@ -19,7 +19,7 @@ namespace osu.Game.Online.API
public Bindable<UserActivity> Activity { get; } = new Bindable<UserActivity>(); public Bindable<UserActivity> Activity { get; } = new Bindable<UserActivity>();
public bool IsLoggedIn => true; public bool IsLoggedIn => State == APIState.Online;
public string ProvidedUsername => LocalUser.Value.Username; public string ProvidedUsername => LocalUser.Value.Username;

View File

@ -81,7 +81,7 @@ namespace osu.Game.Online.Chat
//since we just changed the line display text, offset any already processed links. //since we just changed the line display text, offset any already processed links.
result.Links.ForEach(l => l.Index -= l.Index > index ? m.Length - displayText.Length : 0); result.Links.ForEach(l => l.Index -= l.Index > index ? m.Length - displayText.Length : 0);
var details = getLinkDetails(linkText); var details = GetLinkDetails(linkText);
result.Links.Add(new Link(linkText, index, displayText.Length, linkActionOverride ?? details.Action, details.Argument)); result.Links.Add(new Link(linkText, index, displayText.Length, linkActionOverride ?? details.Action, details.Argument));
//adjust the offset for processing the current matches group. //adjust the offset for processing the current matches group.
@ -98,7 +98,7 @@ namespace osu.Game.Online.Chat
var linkText = m.Groups["link"].Value; var linkText = m.Groups["link"].Value;
var indexLength = linkText.Length; var indexLength = linkText.Length;
var details = getLinkDetails(linkText); var details = GetLinkDetails(linkText);
var link = new Link(linkText, index, indexLength, details.Action, details.Argument); var link = new Link(linkText, index, indexLength, details.Action, details.Argument);
// sometimes an already-processed formatted link can reduce to a simple URL, too // sometimes an already-processed formatted link can reduce to a simple URL, too
@ -109,7 +109,7 @@ namespace osu.Game.Online.Chat
} }
} }
private static LinkDetails getLinkDetails(string url) public static LinkDetails GetLinkDetails(string url)
{ {
var args = url.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries); var args = url.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
args[0] = args[0].TrimEnd(':'); args[0] = args[0].TrimEnd(':');
@ -255,17 +255,17 @@ namespace osu.Game.Online.Chat
OriginalText = Text = text; OriginalText = Text = text;
} }
} }
}
public class LinkDetails public class LinkDetails
{
public LinkAction Action;
public string Argument;
public LinkDetails(LinkAction action, string argument)
{ {
public LinkAction Action; Action = action;
public string Argument; Argument = argument;
public LinkDetails(LinkAction action, string argument)
{
Action = action;
Argument = argument;
}
} }
} }
@ -279,6 +279,7 @@ namespace osu.Game.Online.Chat
JoinMultiplayerMatch, JoinMultiplayerMatch,
Spectate, Spectate,
OpenUserProfile, OpenUserProfile,
Custom
} }
public class Link : IComparable<Link> public class Link : IComparable<Link>

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

@ -215,31 +215,102 @@ namespace osu.Game
private ExternalLinkOpener externalLinkOpener; private ExternalLinkOpener externalLinkOpener;
public void OpenUrlExternally(string url) /// <summary>
/// Handle an arbitrary URL. Displays via in-game overlays where possible.
/// This can be called from a non-thread-safe non-game-loaded state.
/// </summary>
/// <param name="url">The URL to load.</param>
public void HandleLink(string url) => HandleLink(MessageFormatter.GetLinkDetails(url));
/// <summary>
/// Handle a specific <see cref="LinkDetails"/>.
/// This can be called from a non-thread-safe non-game-loaded state.
/// </summary>
/// <param name="link">The link to load.</param>
public void HandleLink(LinkDetails link) => Schedule(() =>
{
switch (link.Action)
{
case LinkAction.OpenBeatmap:
// TODO: proper query params handling
if (link.Argument != null && int.TryParse(link.Argument.Contains('?') ? link.Argument.Split('?')[0] : link.Argument, out int beatmapId))
ShowBeatmap(beatmapId);
break;
case LinkAction.OpenBeatmapSet:
if (int.TryParse(link.Argument, out int setId))
ShowBeatmapSet(setId);
break;
case LinkAction.OpenChannel:
ShowChannel(link.Argument);
break;
case LinkAction.OpenEditorTimestamp:
case LinkAction.JoinMultiplayerMatch:
case LinkAction.Spectate:
waitForReady(() => notifications, _ => notifications?.Post(new SimpleNotification
{
Text = @"This link type is not yet supported!",
Icon = FontAwesome.Solid.LifeRing,
}));
break;
case LinkAction.External:
OpenUrlExternally(link.Argument);
break;
case LinkAction.OpenUserProfile:
if (long.TryParse(link.Argument, out long userId))
ShowUser(userId);
break;
default:
throw new NotImplementedException($"This {nameof(LinkAction)} ({link.Action.ToString()}) is missing an associated action.");
}
});
public void OpenUrlExternally(string url) => waitForReady(() => externalLinkOpener, _ =>
{ {
if (url.StartsWith("/")) if (url.StartsWith("/"))
url = $"{API.Endpoint}{url}"; url = $"{API.Endpoint}{url}";
externalLinkOpener.OpenUrlExternally(url); externalLinkOpener.OpenUrlExternally(url);
} });
/// <summary>
/// Open a specific channel in chat.
/// </summary>
/// <param name="channel">The channel to display.</param>
public void ShowChannel(string channel) => waitForReady(() => channelManager, _ =>
{
try
{
channelManager.OpenChannel(channel);
}
catch (ChannelNotFoundException)
{
Logger.Log($"The requested channel \"{channel}\" does not exist");
}
});
/// <summary> /// <summary>
/// Show a beatmap set as an overlay. /// Show a beatmap set as an overlay.
/// </summary> /// </summary>
/// <param name="setId">The set to display.</param> /// <param name="setId">The set to display.</param>
public void ShowBeatmapSet(int setId) => beatmapSetOverlay.FetchAndShowBeatmapSet(setId); public void ShowBeatmapSet(int setId) => waitForReady(() => beatmapSetOverlay, _ => beatmapSetOverlay.FetchAndShowBeatmapSet(setId));
/// <summary> /// <summary>
/// Show a user's profile as an overlay. /// Show a user's profile as an overlay.
/// </summary> /// </summary>
/// <param name="userId">The user to display.</param> /// <param name="userId">The user to display.</param>
public void ShowUser(long userId) => userProfile.ShowUser(userId); public void ShowUser(long userId) => waitForReady(() => userProfile, _ => userProfile.ShowUser(userId));
/// <summary> /// <summary>
/// Show a beatmap's set as an overlay, displaying the given beatmap. /// Show a beatmap's set as an overlay, displaying the given beatmap.
/// </summary> /// </summary>
/// <param name="beatmapId">The beatmap to show.</param> /// <param name="beatmapId">The beatmap to show.</param>
public void ShowBeatmap(int beatmapId) => beatmapSetOverlay.FetchAndShowBeatmap(beatmapId); public void ShowBeatmap(int beatmapId) => waitForReady(() => beatmapSetOverlay, _ => beatmapSetOverlay.FetchAndShowBeatmap(beatmapId));
/// <summary> /// <summary>
/// Present a beatmap at song select immediately. /// Present a beatmap at song select immediately.
@ -397,6 +468,23 @@ namespace osu.Game
performFromMainMenuTask = Schedule(() => performFromMainMenu(action, taskName)); performFromMainMenuTask = Schedule(() => performFromMainMenu(action, taskName));
} }
/// <summary>
/// Wait for the game (and target component) to become loaded and then run an action.
/// </summary>
/// <param name="retrieveInstance">A function to retrieve a (potentially not-yet-constructed) target instance.</param>
/// <param name="action">The action to perform on the instance when load is confirmed.</param>
/// <typeparam name="T">The type of the target instance.</typeparam>
private void waitForReady<T>(Func<T> retrieveInstance, Action<T> action)
where T : Drawable
{
var instance = retrieveInstance();
if (ScreenStack == null || ScreenStack.CurrentScreen is StartupScreen || instance?.IsLoaded != true)
Schedule(() => waitForReady(retrieveInstance, action));
else
action(instance);
}
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)
{ {
base.Dispose(isDisposing); base.Dispose(isDisposing);

View File

@ -17,6 +17,7 @@ using osu.Game.Beatmaps.Drawables;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
@ -27,10 +28,11 @@ namespace osu.Game.Overlays.BeatmapSet
private const float tile_icon_padding = 7; private const float tile_icon_padding = 7;
private const float tile_spacing = 2; private const float tile_spacing = 2;
private readonly DifficultiesContainer difficulties;
private readonly OsuSpriteText version, starRating; private readonly OsuSpriteText version, starRating;
private readonly Statistic plays, favourites; private readonly Statistic plays, favourites;
public readonly DifficultiesContainer Difficulties;
public readonly Bindable<BeatmapInfo> Beatmap = new Bindable<BeatmapInfo>(); public readonly Bindable<BeatmapInfo> Beatmap = new Bindable<BeatmapInfo>();
private BeatmapSetInfo beatmapSet; private BeatmapSetInfo beatmapSet;
@ -43,38 +45,10 @@ namespace osu.Game.Overlays.BeatmapSet
if (value == beatmapSet) return; if (value == beatmapSet) return;
beatmapSet = value; beatmapSet = value;
updateDisplay(); updateDisplay();
} }
} }
private void updateDisplay()
{
difficulties.Clear();
if (BeatmapSet != null)
{
difficulties.ChildrenEnumerable = BeatmapSet.Beatmaps.OrderBy(beatmap => beatmap.StarDifficulty).Select(b => new DifficultySelectorButton(b)
{
State = DifficultySelectorState.NotSelected,
OnHovered = beatmap =>
{
showBeatmap(beatmap);
starRating.Text = beatmap.StarDifficulty.ToString("Star Difficulty 0.##");
starRating.FadeIn(100);
},
OnClicked = beatmap => { Beatmap.Value = beatmap; },
});
}
starRating.FadeOut(100);
Beatmap.Value = BeatmapSet?.Beatmaps.FirstOrDefault();
plays.Value = BeatmapSet?.OnlineInfo.PlayCount ?? 0;
favourites.Value = BeatmapSet?.OnlineInfo.FavouriteCount ?? 0;
updateDifficultyButtons();
}
public BeatmapPicker() public BeatmapPicker()
{ {
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
@ -89,7 +63,7 @@ namespace osu.Game.Overlays.BeatmapSet
Direction = FillDirection.Vertical, Direction = FillDirection.Vertical,
Children = new Drawable[] Children = new Drawable[]
{ {
difficulties = new DifficultiesContainer Difficulties = new DifficultiesContainer
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
@ -147,6 +121,9 @@ namespace osu.Game.Overlays.BeatmapSet
}; };
} }
[Resolved]
private IBindable<RulesetInfo> ruleset { get; set; }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colours) private void load(OsuColour colours)
{ {
@ -158,10 +135,39 @@ namespace osu.Game.Overlays.BeatmapSet
{ {
base.LoadComplete(); base.LoadComplete();
ruleset.ValueChanged += r => updateDisplay();
// done here so everything can bind in intialization and get the first trigger // done here so everything can bind in intialization and get the first trigger
Beatmap.TriggerChange(); Beatmap.TriggerChange();
} }
private void updateDisplay()
{
Difficulties.Clear();
if (BeatmapSet != null)
{
Difficulties.ChildrenEnumerable = BeatmapSet.Beatmaps.Where(b => b.Ruleset.Equals(ruleset.Value)).OrderBy(b => b.StarDifficulty).Select(b => new DifficultySelectorButton(b)
{
State = DifficultySelectorState.NotSelected,
OnHovered = beatmap =>
{
showBeatmap(beatmap);
starRating.Text = beatmap.StarDifficulty.ToString("Star Difficulty 0.##");
starRating.FadeIn(100);
},
OnClicked = beatmap => { Beatmap.Value = beatmap; },
});
}
starRating.FadeOut(100);
Beatmap.Value = Difficulties.FirstOrDefault()?.Beatmap;
plays.Value = BeatmapSet?.OnlineInfo.PlayCount ?? 0;
favourites.Value = BeatmapSet?.OnlineInfo.FavouriteCount ?? 0;
updateDifficultyButtons();
}
private void showBeatmap(BeatmapInfo beatmap) private void showBeatmap(BeatmapInfo beatmap)
{ {
version.Text = beatmap?.Version; version.Text = beatmap?.Version;
@ -169,10 +175,10 @@ namespace osu.Game.Overlays.BeatmapSet
private void updateDifficultyButtons() private void updateDifficultyButtons()
{ {
difficulties.Children.ToList().ForEach(diff => diff.State = diff.Beatmap == Beatmap.Value ? DifficultySelectorState.Selected : DifficultySelectorState.NotSelected); Difficulties.Children.ToList().ForEach(diff => diff.State = diff.Beatmap == Beatmap.Value ? DifficultySelectorState.Selected : DifficultySelectorState.NotSelected);
} }
private class DifficultiesContainer : FillFlowContainer<DifficultySelectorButton> public class DifficultiesContainer : FillFlowContainer<DifficultySelectorButton>
{ {
public Action OnLostHover; public Action OnLostHover;
@ -183,7 +189,7 @@ namespace osu.Game.Overlays.BeatmapSet
} }
} }
private class DifficultySelectorButton : OsuClickableContainer, IStateful<DifficultySelectorState> public class DifficultySelectorButton : OsuClickableContainer, IStateful<DifficultySelectorState>
{ {
private const float transition_duration = 100; private const float transition_duration = 100;
private const float size = 52; private const float size = 52;
@ -320,7 +326,7 @@ namespace osu.Game.Overlays.BeatmapSet
} }
} }
private enum DifficultySelectorState public enum DifficultySelectorState
{ {
Selected, Selected,
NotSelected, NotSelected,

View File

@ -0,0 +1,48 @@
// 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.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osuTK;
using System.Linq;
namespace osu.Game.Overlays.BeatmapSet
{
public class BeatmapRulesetSelector : RulesetSelector
{
private readonly Bindable<BeatmapSetInfo> beatmapSet = new Bindable<BeatmapSetInfo>();
public BeatmapSetInfo BeatmapSet
{
get => beatmapSet.Value;
set
{
// propagate value to tab items first to enable only available rulesets.
beatmapSet.Value = value;
SelectTab(TabContainer.TabItems.FirstOrDefault(t => t.Enabled.Value));
}
}
public BeatmapRulesetSelector()
{
AutoSizeAxes = Axes.Both;
}
protected override TabItem<RulesetInfo> CreateTabItem(RulesetInfo value) => new BeatmapRulesetTabItem(value)
{
BeatmapSet = { BindTarget = beatmapSet }
};
protected override TabFillFlowContainer CreateTabFlow() => new TabFillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(10, 0),
};
}
}

View File

@ -0,0 +1,145 @@
// 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.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets;
using osuTK;
using osuTK.Graphics;
using System.Linq;
namespace osu.Game.Overlays.BeatmapSet
{
public class BeatmapRulesetTabItem : TabItem<RulesetInfo>
{
private readonly OsuSpriteText name, count;
private readonly Box bar;
public readonly Bindable<BeatmapSetInfo> BeatmapSet = new Bindable<BeatmapSetInfo>();
public override bool PropagatePositionalInputSubTree => Enabled.Value && !Active.Value && base.PropagatePositionalInputSubTree;
public BeatmapRulesetTabItem(RulesetInfo value)
: base(value)
{
AutoSizeAxes = Axes.Both;
FillFlowContainer nameContainer;
Children = new Drawable[]
{
nameContainer = new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Margin = new MarginPadding { Bottom = 7.5f },
Spacing = new Vector2(2.5f),
Children = new Drawable[]
{
name = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Text = value.Name,
Font = OsuFont.Default.With(size: 18),
},
new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both,
Masking = true,
CornerRadius = 4f,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.Black.Opacity(0.5f),
},
count = new OsuSpriteText
{
Alpha = 0,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Margin = new MarginPadding { Horizontal = 5f },
Font = OsuFont.Default.With(weight: FontWeight.SemiBold),
}
}
}
}
},
bar = new Box
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
RelativeSizeAxes = Axes.X,
},
new HoverClickSounds(),
};
BeatmapSet.BindValueChanged(setInfo =>
{
var beatmapsCount = setInfo.NewValue?.Beatmaps.Count(b => b.Ruleset.Equals(Value)) ?? 0;
count.Text = beatmapsCount.ToString();
count.Alpha = beatmapsCount > 0 ? 1f : 0f;
Enabled.Value = beatmapsCount > 0;
}, true);
Enabled.BindValueChanged(v => nameContainer.Alpha = v.NewValue ? 1f : 0.5f, true);
}
[Resolved]
private OsuColour colour { get; set; }
protected override void LoadComplete()
{
base.LoadComplete();
count.Colour = colour.Gray9;
bar.Colour = colour.Blue;
updateState();
}
private void updateState()
{
var isHoveredOrActive = IsHovered || Active.Value;
bar.ResizeHeightTo(isHoveredOrActive ? 4 : 0, 200, Easing.OutQuint);
name.Colour = isHoveredOrActive ? colour.GrayE : colour.GrayC;
name.Font = name.Font.With(weight: Active.Value ? FontWeight.Bold : FontWeight.Regular);
}
#region Hovering and activation logic
protected override void OnActivated() => updateState();
protected override void OnDeactivated() => updateState();
protected override bool OnHover(HoverEvent e)
{
updateState();
return false;
}
protected override void OnHoverLost(HoverLostEvent e) => updateState();
#endregion
}
}

View File

@ -3,6 +3,7 @@
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Colour;
@ -16,6 +17,7 @@ using osu.Game.Graphics.UserInterface;
using osu.Game.Online; using osu.Game.Online;
using osu.Game.Overlays.BeatmapSet.Buttons; using osu.Game.Overlays.BeatmapSet.Buttons;
using osu.Game.Overlays.Direct; using osu.Game.Overlays.Direct;
using osu.Game.Rulesets;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
@ -39,6 +41,7 @@ namespace osu.Game.Overlays.BeatmapSet
public bool DownloadButtonsVisible => downloadButtonsContainer.Any(); public bool DownloadButtonsVisible => downloadButtonsContainer.Any();
public readonly BeatmapRulesetSelector RulesetSelector;
public readonly BeatmapPicker Picker; public readonly BeatmapPicker Picker;
private readonly FavouriteButton favouriteButton; private readonly FavouriteButton favouriteButton;
@ -47,6 +50,9 @@ namespace osu.Game.Overlays.BeatmapSet
private readonly LoadingAnimation loading; private readonly LoadingAnimation loading;
[Cached(typeof(IBindable<RulesetInfo>))]
private readonly Bindable<RulesetInfo> ruleset = new Bindable<RulesetInfo>();
public Header() public Header()
{ {
ExternalLinkButton externalLink; ExternalLinkButton externalLink;
@ -69,12 +75,18 @@ namespace osu.Game.Overlays.BeatmapSet
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Height = tabs_height, Height = tabs_height,
Children = new[] Children = new Drawable[]
{ {
tabsBg = new Box tabsBg = new Box
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
}, },
RulesetSelector = new BeatmapRulesetSelector
{
Current = ruleset,
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
}
}, },
}, },
new Container new Container
@ -223,7 +235,7 @@ namespace osu.Game.Overlays.BeatmapSet
BeatmapSet.BindValueChanged(setInfo => BeatmapSet.BindValueChanged(setInfo =>
{ {
Picker.BeatmapSet = author.BeatmapSet = beatmapAvailability.BeatmapSet = Details.BeatmapSet = setInfo.NewValue; Picker.BeatmapSet = RulesetSelector.BeatmapSet = author.BeatmapSet = beatmapAvailability.BeatmapSet = Details.BeatmapSet = setInfo.NewValue;
cover.BeatmapSet = setInfo.NewValue; cover.BeatmapSet = setInfo.NewValue;
if (setInfo.NewValue == null) if (setInfo.NewValue == null)

View File

@ -110,7 +110,7 @@ namespace osu.Game.Overlays.Changelog
t.Font = fontLarge; t.Font = fontLarge;
t.Colour = entryColour; t.Colour = entryColour;
}); });
title.AddLink($"{entry.Repository.Replace("ppy/", "")}#{entry.GithubPullRequestId}", entry.GithubUrl, Online.Chat.LinkAction.External, title.AddLink($"{entry.Repository.Replace("ppy/", "")}#{entry.GithubPullRequestId}", entry.GithubUrl,
creationParameters: t => creationParameters: t =>
{ {
t.Font = fontLarge; t.Font = fontLarge;
@ -140,7 +140,7 @@ namespace osu.Game.Overlays.Changelog
t.Colour = entryColour; t.Colour = entryColour;
}); });
else if (entry.GithubUser.GithubUrl != null) else if (entry.GithubUser.GithubUrl != null)
title.AddLink(entry.GithubUser.DisplayName, entry.GithubUser.GithubUrl, Online.Chat.LinkAction.External, null, null, t => title.AddLink(entry.GithubUser.DisplayName, entry.GithubUser.GithubUrl, t =>
{ {
t.Font = fontMedium; t.Font = fontMedium;
t.Colour = entryColour; t.Colour = entryColour;

View File

@ -44,7 +44,17 @@ namespace osu.Game.Overlays.Changelog
req.Failure += _ => complete = true; req.Failure += _ => complete = true;
// This is done on a separate thread to support cancellation below // This is done on a separate thread to support cancellation below
Task.Run(() => req.Perform(api)); Task.Run(() =>
{
try
{
req.Perform(api);
}
catch
{
complete = true;
}
});
while (!complete) while (!complete)
{ {

View File

@ -170,6 +170,7 @@ namespace osu.Game.Overlays
var tcs = new TaskCompletionSource<bool>(); var tcs = new TaskCompletionSource<bool>();
var req = new GetChangelogRequest(); var req = new GetChangelogRequest();
req.Success += res => Schedule(() => req.Success += res => Schedule(() =>
{ {
// remap streams to builds to ensure model equality // remap streams to builds to ensure model equality
@ -183,8 +184,22 @@ namespace osu.Game.Overlays
tcs.SetResult(true); tcs.SetResult(true);
}); });
req.Failure += _ => initialFetchTask = null;
req.Perform(API); req.Failure += _ =>
{
initialFetchTask = null;
tcs.SetResult(false);
};
try
{
req.Perform(API);
}
catch
{
initialFetchTask = null;
tcs.SetResult(false);
}
await tcs.Task; await tcs.Task;
}); });

View File

@ -168,12 +168,13 @@ namespace osu.Game.Overlays
}, },
} }
}, },
progressBar = new ProgressBar progressBar = new HoverableProgressBar
{ {
Origin = Anchor.BottomCentre, Origin = Anchor.BottomCentre,
Anchor = Anchor.BottomCentre, Anchor = Anchor.BottomCentre,
Height = progress_height, Height = progress_height / 2,
FillColour = colours.Yellow, FillColour = colours.Yellow,
BackgroundColour = colours.YellowDarker.Opacity(0.5f),
OnSeek = musicController.SeekTo OnSeek = musicController.SeekTo
} }
}, },
@ -401,5 +402,20 @@ namespace osu.Game.Overlays
return base.OnDragEnd(e); return base.OnDragEnd(e);
} }
} }
private class HoverableProgressBar : ProgressBar
{
protected override bool OnHover(HoverEvent e)
{
this.ResizeHeightTo(progress_height, 500, Easing.OutQuint);
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
this.ResizeHeightTo(progress_height / 2, 500, Easing.OutQuint);
base.OnHoverLost(e);
}
}
} }
} }

View File

@ -1,7 +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.
using System;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
using osu.Framework.Allocation; using osu.Framework.Allocation;
@ -9,21 +8,18 @@ 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.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
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;
namespace osu.Game.Overlays.Settings namespace osu.Game.Overlays.Settings
{ {
public class SidebarButton : Button public class SidebarButton : OsuButton
{ {
private readonly SpriteIcon drawableIcon; private readonly SpriteIcon drawableIcon;
private readonly SpriteText headerText; private readonly SpriteText headerText;
private readonly Box selectionIndicator; private readonly Box selectionIndicator;
private readonly Container text; private readonly Container text;
public new Action<SettingsSection> Action;
private SettingsSection section; private SettingsSection section;
@ -62,12 +58,11 @@ namespace osu.Game.Overlays.Settings
public SidebarButton() public SidebarButton()
{ {
BackgroundColour = OsuColour.Gray(60);
Background.Alpha = 0;
Height = Sidebar.DEFAULT_WIDTH; Height = Sidebar.DEFAULT_WIDTH;
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
BackgroundColour = Color4.Black;
AddRange(new Drawable[] AddRange(new Drawable[]
{ {
text = new Container text = new Container
@ -99,7 +94,6 @@ namespace osu.Game.Overlays.Settings
Anchor = Anchor.CentreRight, Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight, Origin = Anchor.CentreRight,
}, },
new HoverClickSounds(HoverSampleSet.Loud),
}); });
} }
@ -108,23 +102,5 @@ namespace osu.Game.Overlays.Settings
{ {
selectionIndicator.Colour = colours.Yellow; selectionIndicator.Colour = colours.Yellow;
} }
protected override bool OnClick(ClickEvent e)
{
Action?.Invoke(section);
return base.OnClick(e);
}
protected override bool OnHover(HoverEvent e)
{
Background.FadeTo(0.4f, 200);
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
Background.FadeTo(0, 200);
base.OnHoverLost(e);
}
} }
} }

View File

@ -123,9 +123,9 @@ namespace osu.Game.Overlays
var button = new SidebarButton var button = new SidebarButton
{ {
Section = section, Section = section,
Action = s => Action = () =>
{ {
SectionsContainer.ScrollTo(s); SectionsContainer.ScrollTo(section);
Sidebar.State = ExpandedState.Contracted; Sidebar.State = ExpandedState.Contracted;
}, },
}; };

View File

@ -40,6 +40,9 @@ namespace osu.Game.Rulesets.Edit
[Resolved] [Resolved]
protected IFrameBasedClock EditorClock { get; private set; } protected IFrameBasedClock EditorClock { get; private set; }
[Resolved]
private IAdjustableClock adjustableClock { get; set; }
[Resolved] [Resolved]
private BindableBeatDivisor beatDivisor { get; set; } private BindableBeatDivisor beatDivisor { get; set; }
@ -148,7 +151,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 +228,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 +239,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);
@ -256,6 +259,9 @@ namespace osu.Game.Rulesets.Edit
public void EndPlacement(HitObject hitObject) public void EndPlacement(HitObject hitObject)
{ {
EditorBeatmap.Add(hitObject); EditorBeatmap.Add(hitObject);
adjustableClock.Seek(hitObject.StartTime);
showGridFor(Enumerable.Empty<HitObject>()); showGridFor(Enumerable.Empty<HitObject>());
} }
@ -351,11 +357,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;
@ -77,6 +82,9 @@ namespace osu.Game.Rulesets.Edit
} }
} }
// When not selected, input is only required for the blueprint itself to receive IsHovering
protected override bool ShouldBeConsideredForInput(Drawable child) => State == SelectionState.Selected;
/// <summary> /// <summary>
/// Selects this <see cref="SelectionBlueprint"/>, causing it to become visible. /// Selects this <see cref="SelectionBlueprint"/>, causing it to become visible.
/// </summary> /// </summary>
@ -89,6 +97,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

@ -239,6 +239,12 @@ namespace osu.Game.Rulesets.UI
continueResume(); continueResume();
} }
public override void CancelResume()
{
// called if the user pauses while the resume overlay is open
ResumeOverlay?.Hide();
}
/// <summary> /// <summary>
/// Creates and adds the visual representation of a <see cref="TObject"/> to this <see cref="DrawableRuleset{TObject}"/>. /// Creates and adds the visual representation of a <see cref="TObject"/> to this <see cref="DrawableRuleset{TObject}"/>.
/// </summary> /// </summary>
@ -453,6 +459,11 @@ namespace osu.Game.Rulesets.UI
/// <param name="continueResume">The action to run when resuming is to be completed.</param> /// <param name="continueResume">The action to run when resuming is to be completed.</param>
public abstract void RequestResume(Action continueResume); public abstract void RequestResume(Action continueResume);
/// <summary>
/// Invoked when the user requests to pause while the resume overlay is active.
/// </summary>
public abstract void CancelResume();
/// <summary> /// <summary>
/// Create a <see cref="ScoreProcessor"/> for the associated ruleset and link with this /// Create a <see cref="ScoreProcessor"/> for the associated ruleset and link with this
/// <see cref="DrawableRuleset"/>. /// <see cref="DrawableRuleset"/>.

View File

@ -13,12 +13,6 @@ namespace osu.Game.Rulesets.UI.Scrolling
{ {
public class ScrollingHitObjectContainer : HitObjectContainer public class ScrollingHitObjectContainer : HitObjectContainer
{ {
/// <summary>
/// A multiplier applied to the length of the scrolling area to determine a safe default lifetime end for hitobjects.
/// This is only used to limit the lifetime end within reason, as proper lifetime management should be implemented on hitobjects themselves.
/// </summary>
private const float safe_lifetime_end_multiplier = 2;
private readonly IBindable<double> timeRange = new BindableDouble(); private readonly IBindable<double> timeRange = new BindableDouble();
private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>(); private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>();
@ -123,28 +117,22 @@ namespace osu.Game.Rulesets.UI.Scrolling
if (cached.IsValid) if (cached.IsValid)
return; return;
double endTime = hitObject.HitObject.StartTime;
if (hitObject.HitObject is IHasEndTime e) if (hitObject.HitObject is IHasEndTime e)
{ {
endTime = e.EndTime;
switch (direction.Value) switch (direction.Value)
{ {
case ScrollingDirection.Up: case ScrollingDirection.Up:
case ScrollingDirection.Down: case ScrollingDirection.Down:
hitObject.Height = scrollingInfo.Algorithm.GetLength(hitObject.HitObject.StartTime, endTime, timeRange.Value, scrollLength); hitObject.Height = scrollingInfo.Algorithm.GetLength(hitObject.HitObject.StartTime, e.EndTime, timeRange.Value, scrollLength);
break; break;
case ScrollingDirection.Left: case ScrollingDirection.Left:
case ScrollingDirection.Right: case ScrollingDirection.Right:
hitObject.Width = scrollingInfo.Algorithm.GetLength(hitObject.HitObject.StartTime, endTime, timeRange.Value, scrollLength); hitObject.Width = scrollingInfo.Algorithm.GetLength(hitObject.HitObject.StartTime, e.EndTime, timeRange.Value, scrollLength);
break; break;
} }
} }
hitObject.LifetimeEnd = scrollingInfo.Algorithm.TimeAt(scrollLength * safe_lifetime_end_multiplier, endTime, timeRange.Value, scrollLength);
foreach (var obj in hitObject.NestedHitObjects) foreach (var obj in hitObject.NestedHitObjects)
{ {
computeInitialStateRecursive(obj); computeInitialStateRecursive(obj);

View File

@ -29,7 +29,10 @@ namespace osu.Game.Screens.Edit
set set
{ {
if (!VALID_DIVISORS.Contains(value)) if (!VALID_DIVISORS.Contains(value))
throw new ArgumentOutOfRangeException($"Provided divisor is not in {nameof(VALID_DIVISORS)}"); {
// If it doesn't match, value will be 0, but will be clamped to the valid range via DefaultMinValue
value = Array.FindLast(VALID_DIVISORS, d => d < value);
}
base.Value = value; base.Value = value;
} }

View File

@ -218,12 +218,17 @@ namespace osu.Game.Screens.Edit.Compose.Components
} }
AddInternal(marker = new Marker()); AddInternal(marker = new Marker());
}
CurrentNumber.ValueChanged += div => protected override void LoadComplete()
{
base.LoadComplete();
CurrentNumber.BindValueChanged(div =>
{ {
marker.MoveToX(getMappedPosition(div.NewValue), 100, Easing.OutQuint); marker.MoveToX(getMappedPosition(div.NewValue), 100, Easing.OutQuint);
marker.Flash(); marker.Flash();
}; }, true);
} }
protected override void UpdateValue(float value) protected override void UpdateValue(float value)

View File

@ -11,6 +11,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Primitives;
using osu.Framework.Input; using osu.Framework.Input;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Timing;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
@ -30,6 +31,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
private SelectionHandler selectionHandler; private SelectionHandler selectionHandler;
private InputManager inputManager; private InputManager inputManager;
[Resolved]
private IAdjustableClock adjustableClock { get; set; }
[Resolved] [Resolved]
private HitObjectComposer composer { get; set; } private HitObjectComposer composer { get; set; }
@ -106,6 +110,17 @@ namespace osu.Game.Screens.Edit.Compose.Components
return true; return true;
} }
protected override bool OnDoubleClick(DoubleClickEvent e)
{
SelectionBlueprint clickedBlueprint = selectionHandler.SelectedBlueprints.FirstOrDefault(b => b.IsHovered);
if (clickedBlueprint == null)
return false;
adjustableClock?.Seek(clickedBlueprint.DrawableObject.HitObject.StartTime);
return true;
}
protected override bool OnMouseUp(MouseUpEvent e) protected override bool OnMouseUp(MouseUpEvent e)
{ {
// Special case for when a drag happened instead of a click // Special case for when a drag happened instead of a click
@ -254,6 +269,12 @@ namespace osu.Game.Screens.Edit.Compose.Components
{ {
Debug.Assert(!clickSelectionBegan); Debug.Assert(!clickSelectionBegan);
// If a select blueprint is already hovered, disallow changes in selection.
// Exception is made when holding control, as deselection should still be allowed.
if (!e.CurrentState.Keyboard.ControlPressed &&
selectionHandler.SelectedBlueprints.Any(s => s.IsHovered))
return;
foreach (SelectionBlueprint blueprint in selectionBlueprints.AliveBlueprints) foreach (SelectionBlueprint blueprint in selectionBlueprints.AliveBlueprints)
{ {
if (blueprint.IsHovered) if (blueprint.IsHovered)
@ -361,7 +382,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
(Vector2 snappedPosition, double snappedTime) = composer.GetSnappedPosition(ToLocalSpace(movePosition), draggedObject.StartTime); (Vector2 snappedPosition, double snappedTime) = composer.GetSnappedPosition(ToLocalSpace(movePosition), draggedObject.StartTime);
// Move the hitobjects // Move the hitobjects
selectionHandler.HandleMovement(new MoveSelectionEvent(movementBlueprint, startPosition, ToScreenSpace(snappedPosition))); if (!selectionHandler.HandleMovement(new MoveSelectionEvent(movementBlueprint, startPosition, ToScreenSpace(snappedPosition))))
return true;
// Apply the start time at the newly snapped-to position // Apply the start time at the newly snapped-to position
double offset = snappedTime - draggedObject.StartTime; double offset = snappedTime - draggedObject.StartTime;

View File

@ -12,8 +12,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
{ {
public abstract class CircularDistanceSnapGrid : DistanceSnapGrid public abstract class CircularDistanceSnapGrid : DistanceSnapGrid
{ {
protected CircularDistanceSnapGrid(HitObject hitObject, Vector2 centrePosition) protected CircularDistanceSnapGrid(HitObject hitObject, HitObject nextHitObject, Vector2 centrePosition)
: base(hitObject, centrePosition) : base(hitObject, nextHitObject, centrePosition)
{ {
} }
@ -45,7 +45,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
float dx = Math.Max(centrePosition.X, DrawWidth - centrePosition.X); float dx = Math.Max(centrePosition.X, DrawWidth - centrePosition.X);
float dy = Math.Max(centrePosition.Y, DrawHeight - centrePosition.Y); float dy = Math.Max(centrePosition.Y, DrawHeight - centrePosition.Y);
float maxDistance = new Vector2(dx, dy).Length; float maxDistance = new Vector2(dx, dy).Length;
int requiredCircles = (int)(maxDistance / DistanceSpacing); int requiredCircles = Math.Min(MaxIntervals, (int)(maxDistance / DistanceSpacing));
for (int i = 0; i < requiredCircles; i++) for (int i = 0; i < requiredCircles; i++)
{ {
@ -65,15 +65,17 @@ namespace osu.Game.Screens.Edit.Compose.Components
public override (Vector2 position, double time) GetSnappedPosition(Vector2 position) public override (Vector2 position, double time) GetSnappedPosition(Vector2 position)
{ {
Vector2 direction = position - CentrePosition; if (MaxIntervals == 0)
return (CentrePosition, StartTime);
Vector2 direction = position - CentrePosition;
if (direction == Vector2.Zero) if (direction == Vector2.Zero)
direction = new Vector2(0.001f, 0.001f); direction = new Vector2(0.001f, 0.001f);
float distance = direction.Length; float distance = direction.Length;
float radius = DistanceSpacing; float radius = DistanceSpacing;
int radialCount = Math.Max(1, (int)Math.Round(distance / radius)); int radialCount = MathHelper.Clamp((int)Math.Round(distance / radius), 1, MaxIntervals);
Vector2 normalisedDirection = direction * new Vector2(1f / distance); Vector2 normalisedDirection = direction * new Vector2(1f / distance);
Vector2 snappedPosition = CentrePosition + normalisedDirection * radialCount * radius; Vector2 snappedPosition = CentrePosition + normalisedDirection * radialCount * radius;

View File

@ -1,6 +1,7 @@
// 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.
using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Caching; using osu.Framework.Caching;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -29,6 +30,11 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// </summary> /// </summary>
protected double StartTime { get; private set; } protected double StartTime { get; private set; }
/// <summary>
/// The maximum number of distance snapping intervals allowed.
/// </summary>
protected int MaxIntervals { get; private set; }
/// <summary> /// <summary>
/// The position which the grid is centred on. /// The position which the grid is centred on.
/// The first beat snapping tick is located at <see cref="CentrePosition"/> + <see cref="DistanceSpacing"/> in the desired direction. /// The first beat snapping tick is located at <see cref="CentrePosition"/> + <see cref="DistanceSpacing"/> in the desired direction.
@ -49,12 +55,15 @@ namespace osu.Game.Screens.Edit.Compose.Components
private readonly Cached gridCache = new Cached(); private readonly Cached gridCache = new Cached();
private readonly HitObject hitObject; private readonly HitObject hitObject;
private readonly HitObject nextHitObject;
protected DistanceSnapGrid(HitObject hitObject, Vector2 centrePosition) protected DistanceSnapGrid(HitObject hitObject, [CanBeNull] HitObject nextHitObject, Vector2 centrePosition)
{ {
this.hitObject = hitObject; this.hitObject = hitObject;
this.nextHitObject = nextHitObject;
CentrePosition = centrePosition; CentrePosition = centrePosition;
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
} }
@ -74,6 +83,16 @@ namespace osu.Game.Screens.Edit.Compose.Components
private void updateSpacing() private void updateSpacing()
{ {
DistanceSpacing = SnapProvider.GetBeatSnapDistanceAt(StartTime); DistanceSpacing = SnapProvider.GetBeatSnapDistanceAt(StartTime);
if (nextHitObject == null)
MaxIntervals = int.MaxValue;
else
{
// +1 is added since a snapped hitobject may have its start time slightly less than the snapped time due to floating point errors
double maxDuration = nextHitObject.StartTime - StartTime + 1;
MaxIntervals = (int)(maxDuration / SnapProvider.DistanceToDuration(StartTime, DistanceSpacing));
}
gridCache.Invalidate(); gridCache.Invalidate();
} }

View File

@ -8,21 +8,21 @@ 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.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events; using osu.Framework.Input;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.States; using osu.Framework.Input.States;
using osu.Game.Graphics; using osu.Game.Graphics;
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 osuTK; using osuTK;
using osuTK.Input;
namespace osu.Game.Screens.Edit.Compose.Components namespace osu.Game.Screens.Edit.Compose.Components
{ {
/// <summary> /// <summary>
/// A component which outlines <see cref="DrawableHitObject"/>s and handles movement of selections. /// A component which outlines <see cref="DrawableHitObject"/>s and handles movement of selections.
/// </summary> /// </summary>
public class SelectionHandler : CompositeDrawable public class SelectionHandler : CompositeDrawable, IKeyBindingHandler<PlatformAction>
{ {
public const float BORDER_RADIUS = 2; public const float BORDER_RADIUS = 2;
@ -68,26 +68,24 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// Handles the selected <see cref="DrawableHitObject"/>s being moved. /// Handles the selected <see cref="DrawableHitObject"/>s being moved.
/// </summary> /// </summary>
/// <param name="moveEvent">The move event.</param> /// <param name="moveEvent">The move event.</param>
public virtual void HandleMovement(MoveSelectionEvent moveEvent) /// <returns>Whether any <see cref="DrawableHitObject"/>s were moved.</returns>
{ public virtual bool HandleMovement(MoveSelectionEvent moveEvent) => false;
}
protected override bool OnKeyDown(KeyDownEvent e) public bool OnPressed(PlatformAction action)
{ {
if (e.Repeat) switch (action.ActionMethod)
return base.OnKeyDown(e);
switch (e.Key)
{ {
case Key.Delete: case PlatformActionMethod.Delete:
foreach (var h in selectedBlueprints.ToList()) foreach (var h in selectedBlueprints.ToList())
placementHandler.Delete(h.DrawableObject.HitObject); placementHandler.Delete(h.DrawableObject.HitObject);
return true; return true;
} }
return base.OnKeyDown(e); return false;
} }
public bool OnReleased(PlatformAction action) => action.ActionMethod == PlatformActionMethod.Delete;
#endregion #endregion
#region Selection Handling #region Selection Handling

View File

@ -64,7 +64,10 @@ namespace osu.Game.Screens.Edit
{ {
this.host = host; this.host = host;
// TODO: should probably be done at a DrawableRuleset level to share logic with Player. beatDivisor.Value = Beatmap.Value.BeatmapInfo.BeatDivisor;
beatDivisor.BindValueChanged(divisor => Beatmap.Value.BeatmapInfo.BeatDivisor = divisor.NewValue);
// Todo: should probably be done at a DrawableRuleset level to share logic with Player.
var sourceClock = (IAdjustableClock)Beatmap.Value.Track ?? new StopwatchClock(); var sourceClock = (IAdjustableClock)Beatmap.Value.Track ?? new StopwatchClock();
clock = new EditorClock(Beatmap.Value, beatDivisor) { IsCoupled = false }; clock = new EditorClock(Beatmap.Value, beatDivisor) { IsCoupled = false };
clock.ChangeSource(sourceClock); clock.ChangeSource(sourceClock);

View File

@ -0,0 +1,50 @@
// 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.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
namespace osu.Game.Screens.Edit.Timing
{
public class ControlPointSettings : CompositeDrawable
{
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
RelativeSizeAxes = Axes.Both;
InternalChildren = new Drawable[]
{
new Box
{
Colour = colours.Gray3,
RelativeSizeAxes = Axes.Both,
},
new OsuScrollContainer
{
RelativeSizeAxes = Axes.Both,
Child = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Children = createSections()
},
}
};
}
private IReadOnlyList<Drawable> createSections() => new Drawable[]
{
new TimingSection(),
new DifficultySection(),
new SampleSection(),
new EffectSection(),
};
}
}

View File

@ -0,0 +1,247 @@
// 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.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.Edit.Timing
{
public class ControlPointTable : TableContainer
{
private const float horizontal_inset = 20;
private const float row_height = 25;
private const int text_size = 14;
private readonly FillFlowContainer backgroundFlow;
[Resolved]
private Bindable<ControlPointGroup> selectedGroup { get; set; }
public ControlPointTable()
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
Padding = new MarginPadding { Horizontal = horizontal_inset };
RowSize = new Dimension(GridSizeMode.Absolute, row_height);
AddInternal(backgroundFlow = new FillFlowContainer
{
RelativeSizeAxes = Axes.Both,
Depth = 1f,
Padding = new MarginPadding { Horizontal = -horizontal_inset },
Margin = new MarginPadding { Top = row_height }
});
}
public IEnumerable<ControlPointGroup> ControlGroups
{
set
{
Content = null;
backgroundFlow.Clear();
if (value?.Any() != true)
return;
foreach (var group in value)
{
backgroundFlow.Add(new RowBackground(group));
}
Columns = createHeaders();
Content = value.Select((g, i) => createContent(i, g)).ToArray().ToRectangular();
}
}
private TableColumn[] createHeaders()
{
var columns = new List<TableColumn>
{
new TableColumn(string.Empty, Anchor.Centre, new Dimension(GridSizeMode.AutoSize)),
new TableColumn("Time", Anchor.Centre, new Dimension(GridSizeMode.AutoSize)),
new TableColumn("Attributes", Anchor.Centre),
};
return columns.ToArray();
}
private Drawable[] createContent(int index, ControlPointGroup group) => new Drawable[]
{
new OsuSpriteText
{
Text = $"#{index + 1}",
Font = OsuFont.GetFont(size: text_size, weight: FontWeight.Bold),
Margin = new MarginPadding(10)
},
new OsuSpriteText
{
Text = $"{group.Time:n0}ms",
Font = OsuFont.GetFont(size: text_size, weight: FontWeight.Bold)
},
new ControlGroupAttributes(group),
};
private class ControlGroupAttributes : CompositeDrawable
{
private readonly IBindableList<ControlPoint> controlPoints;
private readonly FillFlowContainer fill;
public ControlGroupAttributes(ControlPointGroup group)
{
InternalChild = fill = new FillFlowContainer
{
RelativeSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Padding = new MarginPadding(10),
Spacing = new Vector2(2)
};
controlPoints = group.ControlPoints.GetBoundCopy();
controlPoints.ItemsAdded += _ => createChildren();
controlPoints.ItemsRemoved += _ => createChildren();
createChildren();
}
private void createChildren()
{
fill.ChildrenEnumerable = controlPoints.Select(createAttribute).Where(c => c != null);
}
private Drawable createAttribute(ControlPoint controlPoint)
{
switch (controlPoint)
{
case TimingControlPoint timing:
return new RowAttribute("timing", () => $"{60000 / timing.BeatLength:n1}bpm {timing.TimeSignature}");
case DifficultyControlPoint difficulty:
return new RowAttribute("difficulty", () => $"{difficulty.SpeedMultiplier:n2}x");
case EffectControlPoint effect:
return new RowAttribute("effect", () => $"{(effect.KiaiMode ? "Kiai " : "")}{(effect.OmitFirstBarLine ? "NoBarLine " : "")}");
case SampleControlPoint sample:
return new RowAttribute("sample", () => $"{sample.SampleBank} {sample.SampleVolume}%");
}
return null;
}
}
protected override Drawable CreateHeader(int index, TableColumn column) => new HeaderText(column?.Header ?? string.Empty);
private class HeaderText : OsuSpriteText
{
public HeaderText(string text)
{
Text = text.ToUpper();
Font = OsuFont.GetFont(size: 12, weight: FontWeight.Black);
}
}
public class RowBackground : OsuClickableContainer
{
private readonly ControlPointGroup controlGroup;
private const int fade_duration = 100;
private readonly Box hoveredBackground;
[Resolved]
private Bindable<ControlPointGroup> selectedGroup { get; set; }
public RowBackground(ControlPointGroup controlGroup)
{
this.controlGroup = controlGroup;
RelativeSizeAxes = Axes.X;
Height = 25;
AlwaysPresent = true;
CornerRadius = 3;
Masking = true;
Children = new Drawable[]
{
hoveredBackground = new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0,
},
};
Action = () => selectedGroup.Value = controlGroup;
}
private Color4 colourHover;
private Color4 colourSelected;
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
hoveredBackground.Colour = colourHover = colours.BlueDarker;
colourSelected = colours.YellowDarker;
}
protected override void LoadComplete()
{
base.LoadComplete();
selectedGroup.BindValueChanged(group => { Selected = controlGroup == group.NewValue; }, true);
}
private bool selected;
protected bool Selected
{
get => selected;
set
{
if (value == selected)
return;
selected = value;
updateState();
}
}
protected override bool OnHover(HoverEvent e)
{
updateState();
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
updateState();
base.OnHoverLost(e);
}
private void updateState()
{
hoveredBackground.FadeColour(selected ? colourSelected : colourHover, 450, Easing.OutQuint);
if (selected || IsHovered)
hoveredBackground.FadeIn(fade_duration, Easing.OutQuint);
else
hoveredBackground.FadeOut(fade_duration, Easing.OutQuint);
}
}
}
}

View File

@ -0,0 +1,48 @@
// 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.Bindables;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Overlays.Settings;
namespace osu.Game.Screens.Edit.Timing
{
internal class DifficultySection : Section<DifficultyControlPoint>
{
private SettingsSlider<double> multiplier;
[BackgroundDependencyLoader]
private void load()
{
Flow.AddRange(new[]
{
multiplier = new SettingsSlider<double>
{
LabelText = "Speed Multiplier",
Bindable = new DifficultyControlPoint().SpeedMultiplierBindable,
RelativeSizeAxes = Axes.X,
}
});
}
protected override void OnControlPointChanged(ValueChangedEvent<DifficultyControlPoint> point)
{
if (point.NewValue != null)
{
multiplier.Bindable = point.NewValue.SpeedMultiplierBindable;
}
}
protected override DifficultyControlPoint CreatePoint()
{
var reference = Beatmap.Value.Beatmap.ControlPointInfo.DifficultyPointAt(SelectedGroup.Value.Time);
return new DifficultyControlPoint
{
SpeedMultiplier = reference.SpeedMultiplier,
};
}
}
}

View File

@ -0,0 +1,46 @@
// 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.Bindables;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.UserInterfaceV2;
namespace osu.Game.Screens.Edit.Timing
{
internal class EffectSection : Section<EffectControlPoint>
{
private LabelledSwitchButton kiai;
private LabelledSwitchButton omitBarLine;
[BackgroundDependencyLoader]
private void load()
{
Flow.AddRange(new[]
{
kiai = new LabelledSwitchButton { Label = "Kiai Time" },
omitBarLine = new LabelledSwitchButton { Label = "Skip Bar Line" },
});
}
protected override void OnControlPointChanged(ValueChangedEvent<EffectControlPoint> point)
{
if (point.NewValue != null)
{
kiai.Current = point.NewValue.KiaiModeBindable;
omitBarLine.Current = point.NewValue.OmitFirstBarLineBindable;
}
}
protected override EffectControlPoint CreatePoint()
{
var reference = Beatmap.Value.Beatmap.ControlPointInfo.EffectPointAt(SelectedGroup.Value.Time);
return new EffectControlPoint
{
KiaiMode = reference.KiaiMode,
OmitFirstBarLine = reference.OmitFirstBarLine
};
}
}
}

View File

@ -0,0 +1,60 @@
// 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.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
namespace osu.Game.Screens.Edit.Timing
{
public class RowAttribute : CompositeDrawable, IHasTooltip
{
private readonly string header;
private readonly Func<string> content;
public RowAttribute(string header, Func<string> content)
{
this.header = header;
this.content = content;
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
AutoSizeAxes = Axes.X;
Height = 20;
Anchor = Anchor.CentreLeft;
Origin = Anchor.CentreLeft;
Masking = true;
CornerRadius = 5;
InternalChildren = new Drawable[]
{
new Box
{
Colour = colours.Yellow,
RelativeSizeAxes = Axes.Both,
},
new OsuSpriteText
{
Padding = new MarginPadding(2),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Font = OsuFont.Default.With(weight: FontWeight.SemiBold, size: 12),
Text = header,
Colour = colours.Gray3
},
};
}
public string TooltipText => content();
}
}

View File

@ -0,0 +1,55 @@
// 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.Bindables;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Overlays.Settings;
namespace osu.Game.Screens.Edit.Timing
{
internal class SampleSection : Section<SampleControlPoint>
{
private LabelledTextBox bank;
private SettingsSlider<int> volume;
[BackgroundDependencyLoader]
private void load()
{
Flow.AddRange(new Drawable[]
{
bank = new LabelledTextBox
{
Label = "Bank Name",
},
volume = new SettingsSlider<int>
{
Bindable = new SampleControlPoint().SampleVolumeBindable,
LabelText = "Volume",
}
});
}
protected override void OnControlPointChanged(ValueChangedEvent<SampleControlPoint> point)
{
if (point.NewValue != null)
{
bank.Current = point.NewValue.SampleBankBindable;
volume.Bindable = point.NewValue.SampleVolumeBindable;
}
}
protected override SampleControlPoint CreatePoint()
{
var reference = Beatmap.Value.Beatmap.ControlPointInfo.SamplePointAt(SelectedGroup.Value.Time);
return new SampleControlPoint
{
SampleBank = reference.SampleBank,
SampleVolume = reference.SampleVolume,
};
}
}
}

View File

@ -0,0 +1,130 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
namespace osu.Game.Screens.Edit.Timing
{
internal abstract class Section<T> : CompositeDrawable
where T : ControlPoint
{
private OsuCheckbox checkbox;
private Container content;
protected FillFlowContainer Flow { get; private set; }
protected Bindable<T> ControlPoint { get; } = new Bindable<T>();
private const float header_height = 20;
[Resolved]
protected IBindable<WorkingBeatmap> Beatmap { get; private set; }
[Resolved]
protected Bindable<ControlPointGroup> SelectedGroup { get; private set; }
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
RelativeSizeAxes = Axes.X;
AutoSizeDuration = 200;
AutoSizeEasing = Easing.OutQuint;
AutoSizeAxes = Axes.Y;
Masking = true;
InternalChildren = new Drawable[]
{
new Box
{
Colour = colours.Gray1,
RelativeSizeAxes = Axes.Both,
},
new Container
{
RelativeSizeAxes = Axes.X,
Height = header_height,
Children = new Drawable[]
{
checkbox = new OsuCheckbox
{
LabelText = typeof(T).Name.Replace(typeof(ControlPoint).Name, string.Empty)
}
}
},
content = new Container
{
Y = header_height,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Children = new Drawable[]
{
new Box
{
Colour = colours.Gray2,
RelativeSizeAxes = Axes.Both,
},
Flow = new FillFlowContainer
{
Padding = new MarginPadding(10),
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
},
}
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
checkbox.Current.BindValueChanged(selected =>
{
if (selected.NewValue)
{
if (SelectedGroup.Value == null)
{
checkbox.Current.Value = false;
return;
}
if (ControlPoint.Value == null)
SelectedGroup.Value.Add(ControlPoint.Value = CreatePoint());
}
else
{
if (ControlPoint.Value != null)
{
SelectedGroup.Value.Remove(ControlPoint.Value);
ControlPoint.Value = null;
}
}
content.BypassAutoSizeAxes = selected.NewValue ? Axes.None : Axes.Y;
}, true);
SelectedGroup.BindValueChanged(points =>
{
ControlPoint.Value = points.NewValue?.ControlPoints.OfType<T>().FirstOrDefault();
checkbox.Current.Value = ControlPoint.Value != null;
}, true);
ControlPoint.BindValueChanged(OnControlPointChanged, true);
}
protected abstract void OnControlPointChanged(ValueChangedEvent<T> point);
protected abstract T CreatePoint();
}
}

View File

@ -1,13 +1,151 @@
// 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.
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osuTK;
namespace osu.Game.Screens.Edit.Timing namespace osu.Game.Screens.Edit.Timing
{ {
public class TimingScreen : EditorScreen public class TimingScreen : EditorScreenWithTimeline
{ {
public TimingScreen() [Cached]
private Bindable<ControlPointGroup> selectedGroup = new Bindable<ControlPointGroup>();
[Resolved]
private IAdjustableClock clock { get; set; }
protected override Drawable CreateMainContent() => new GridContainer
{ {
Child = new ScreenWhiteBox.UnderConstructionMessage("Timing mode"); RelativeSizeAxes = Axes.Both,
ColumnDimensions = new[]
{
new Dimension(),
new Dimension(GridSizeMode.Absolute, 200),
},
Content = new[]
{
new Drawable[]
{
new ControlPointList(),
new ControlPointSettings(),
},
}
};
protected override void LoadComplete()
{
base.LoadComplete();
selectedGroup.BindValueChanged(selected =>
{
if (selected.NewValue != null)
clock.Seek(selected.NewValue.Time);
});
}
public class ControlPointList : CompositeDrawable
{
private OsuButton deleteButton;
private ControlPointTable table;
private IBindableList<ControlPointGroup> controlGroups;
[Resolved]
private IFrameBasedClock clock { get; set; }
[Resolved]
protected IBindable<WorkingBeatmap> Beatmap { get; private set; }
[Resolved]
private Bindable<ControlPointGroup> selectedGroup { get; set; }
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
RelativeSizeAxes = Axes.Both;
InternalChildren = new Drawable[]
{
new Box
{
Colour = colours.Gray0,
RelativeSizeAxes = Axes.Both,
},
new OsuScrollContainer
{
RelativeSizeAxes = Axes.Both,
Child = table = new ControlPointTable(),
},
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
Direction = FillDirection.Horizontal,
Margin = new MarginPadding(10),
Spacing = new Vector2(5),
Children = new Drawable[]
{
deleteButton = new OsuButton
{
Text = "-",
Size = new Vector2(30, 30),
Action = delete,
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
},
new OsuButton
{
Text = "+",
Action = addNew,
Size = new Vector2(30, 30),
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
},
}
},
};
}
protected override void LoadComplete()
{
base.LoadComplete();
selectedGroup.BindValueChanged(selected => { deleteButton.Enabled.Value = selected.NewValue != null; }, true);
controlGroups = Beatmap.Value.Beatmap.ControlPointInfo.Groups.GetBoundCopy();
controlGroups.ItemsAdded += _ => createContent();
controlGroups.ItemsRemoved += _ => createContent();
createContent();
}
private void createContent() => table.ControlGroups = controlGroups;
private void delete()
{
if (selectedGroup.Value == null)
return;
Beatmap.Value.Beatmap.ControlPointInfo.RemoveGroup(selectedGroup.Value);
selectedGroup.Value = Beatmap.Value.Beatmap.ControlPointInfo.Groups.FirstOrDefault(g => g.Time >= clock.CurrentTime);
}
private void addNew()
{
selectedGroup.Value = Beatmap.Value.Beatmap.ControlPointInfo.GroupAt(clock.CurrentTime, true);
}
} }
} }
} }

View File

@ -0,0 +1,85 @@
// 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.Bindables;
using osu.Framework.Graphics;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Timing;
using osu.Game.Overlays.Settings;
namespace osu.Game.Screens.Edit.Timing
{
internal class TimingSection : Section<TimingControlPoint>
{
private SettingsSlider<double> bpm;
private SettingsEnumDropdown<TimeSignatures> timeSignature;
[BackgroundDependencyLoader]
private void load()
{
Flow.AddRange(new Drawable[]
{
bpm = new BPMSlider
{
Bindable = new TimingControlPoint().BeatLengthBindable,
LabelText = "BPM",
},
timeSignature = new SettingsEnumDropdown<TimeSignatures>
{
LabelText = "Time Signature"
},
});
}
protected override void OnControlPointChanged(ValueChangedEvent<TimingControlPoint> point)
{
if (point.NewValue != null)
{
bpm.Bindable = point.NewValue.BeatLengthBindable;
timeSignature.Bindable = point.NewValue.TimeSignatureBindable;
}
}
protected override TimingControlPoint CreatePoint()
{
var reference = Beatmap.Value.Beatmap.ControlPointInfo.TimingPointAt(SelectedGroup.Value.Time);
return new TimingControlPoint
{
BeatLength = reference.BeatLength,
TimeSignature = reference.TimeSignature
};
}
private class BPMSlider : SettingsSlider<double>
{
private readonly BindableDouble beatLengthBindable = new BindableDouble();
private BindableDouble bpmBindable;
public override Bindable<double> Bindable
{
get => base.Bindable;
set
{
// incoming will be beatlength
beatLengthBindable.UnbindBindings();
beatLengthBindable.BindTo(value);
base.Bindable = bpmBindable = new BindableDouble(beatLengthToBpm(beatLengthBindable.Value))
{
MinValue = beatLengthToBpm(beatLengthBindable.MaxValue),
MaxValue = beatLengthToBpm(beatLengthBindable.MinValue),
Default = beatLengthToBpm(beatLengthBindable.Default),
};
bpmBindable.BindValueChanged(bpm => beatLengthBindable.Value = beatLengthToBpm(bpm.NewValue));
}
}
private double beatLengthToBpm(double beatLength) => 60000 / beatLength;
}
}
}

View File

@ -81,7 +81,7 @@ namespace osu.Game.Screens.Multi.Components
Text = new LocalisedString((beatmap.Metadata.TitleUnicode, beatmap.Metadata.Title)), Text = new LocalisedString((beatmap.Metadata.TitleUnicode, beatmap.Metadata.Title)),
Font = OsuFont.GetFont(size: TextSize), Font = OsuFont.GetFont(size: TextSize),
} }
}, null, LinkAction.OpenBeatmap, beatmap.OnlineBeatmapID.ToString(), "Open beatmap"); }, LinkAction.OpenBeatmap, beatmap.OnlineBeatmapID.ToString(), "Open beatmap");
} }
} }
} }

View File

@ -167,14 +167,17 @@ namespace osu.Game.Screens.Multi
public void APIStateChanged(IAPIProvider api, APIState state) public void APIStateChanged(IAPIProvider api, APIState state)
{ {
if (state != APIState.Online) if (state != APIState.Online)
forcefullyExit(); Schedule(forcefullyExit);
} }
private void forcefullyExit() private void forcefullyExit()
{ {
// This is temporary since we don't currently have a way to force screens to be exited // This is temporary since we don't currently have a way to force screens to be exited
if (this.IsCurrentScreen()) if (this.IsCurrentScreen())
this.Exit(); {
while (this.IsCurrentScreen())
this.Exit();
}
else else
{ {
this.MakeCurrent(); this.MakeCurrent();
@ -212,6 +215,8 @@ namespace osu.Game.Screens.Multi
public override bool OnExiting(IScreen next) public override bool OnExiting(IScreen next)
{ {
roomManager.PartRoom();
if (screenStack.CurrentScreen != null && !(screenStack.CurrentScreen is LoungeSubScreen)) if (screenStack.CurrentScreen != null && !(screenStack.CurrentScreen is LoungeSubScreen))
{ {
screenStack.Exit(); screenStack.Exit();

View File

@ -87,9 +87,8 @@ namespace osu.Game.Screens.Multi
public void JoinRoom(Room room, Action<Room> onSuccess = null, Action<string> onError = null) public void JoinRoom(Room room, Action<Room> onSuccess = null, Action<string> onError = null)
{ {
currentJoinRoomRequest?.Cancel(); currentJoinRoomRequest?.Cancel();
currentJoinRoomRequest = null;
currentJoinRoomRequest = new JoinRoomRequest(room, api.LocalUser.Value); currentJoinRoomRequest = new JoinRoomRequest(room, api.LocalUser.Value);
currentJoinRoomRequest.Success += () => currentJoinRoomRequest.Success += () =>
{ {
joinedRoom = room; joinedRoom = room;
@ -98,7 +97,8 @@ namespace osu.Game.Screens.Multi
currentJoinRoomRequest.Failure += exception => currentJoinRoomRequest.Failure += exception =>
{ {
Logger.Log($"Failed to join room: {exception}", level: LogLevel.Important); if (!(exception is OperationCanceledException))
Logger.Log($"Failed to join room: {exception}", level: LogLevel.Important);
onError?.Invoke(exception.ToString()); onError?.Invoke(exception.ToString());
}; };
@ -107,6 +107,8 @@ namespace osu.Game.Screens.Multi
public void PartRoom() public void PartRoom()
{ {
currentJoinRoomRequest?.Cancel();
if (joinedRoom == null) if (joinedRoom == null)
return; return;

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,10 +154,18 @@ namespace osu.Game.Screens.Play
IsPaused.Value = true; IsPaused.Value = true;
} }
public void ResetLocalAdjustments() /// <summary>
/// Changes the backing clock to avoid using the originally provided beatmap's track.
/// </summary>
public void StopUsingBeatmapClock()
{ {
// In the case of replays, we may have changed the playback rate. if (sourceClock != beatmap.Track)
UserPlaybackRate.Value = 1; return;
removeSourceClockAdjustments();
sourceClock = new TrackVirtual(beatmap.Track.Length);
adjustableClock.ChangeSource(sourceClock);
} }
protected override void Update() protected override void Update()
@ -185,6 +194,14 @@ namespace osu.Game.Screens.Play
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)
{ {
base.Dispose(isDisposing); base.Dispose(isDisposing);
removeSourceClockAdjustments();
sourceClock = null;
}
private void removeSourceClockAdjustments()
{
sourceClock.ResetSpeedAdjustments();
(sourceClock as IAdjustableAudioComponent)?.RemoveAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust); (sourceClock as IAdjustableAudioComponent)?.RemoveAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust);
} }
} }

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();
performImmediateExit();
if (this.IsCurrentScreen())
performImmediateExit();
else
this.MakeCurrent();
} }
private ScheduledDelegate completionProgressDelegate; private ScheduledDelegate completionProgressDelegate;
@ -443,7 +449,12 @@ namespace osu.Game.Screens.Play
{ {
if (!canPause) return; if (!canPause) return;
IsResuming = false; if (IsResuming)
{
DrawableRuleset.CancelResume();
IsResuming = false;
}
GameplayClockContainer.Stop(); GameplayClockContainer.Stop();
PauseOverlay.Show(); PauseOverlay.Show();
lastPauseActionTime = GameplayClockContainer.GameplayClock.CurrentTime; lastPauseActionTime = GameplayClockContainer.GameplayClock.CurrentTime;
@ -525,7 +536,9 @@ namespace osu.Game.Screens.Play
return true; return true;
} }
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)]

Some files were not shown because too many files have changed in this diff Show More