mirror of
https://github.com/ppy/osu.git
synced 2024-12-13 04:32:57 +08:00
Merge branch 'master' into infrastructure
This commit is contained in:
commit
1c1a49011b
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal 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.
|
@ -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:**
|
@ -53,6 +53,6 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<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>
|
||||
</Project>
|
||||
|
@ -2,7 +2,7 @@
|
||||
<Import Project="..\osu.TestProject.props" />
|
||||
<ItemGroup Label="Package References">
|
||||
<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="NUnit3TestAdapter" Version="3.15.1" />
|
||||
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
|
||||
|
@ -2,7 +2,7 @@
|
||||
<Import Project="..\osu.TestProject.props" />
|
||||
<ItemGroup Label="Package References">
|
||||
<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="NUnit3TestAdapter" Version="3.15.1" />
|
||||
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
|
||||
|
@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Mania.Edit
|
||||
editorClock = clock;
|
||||
}
|
||||
|
||||
public override void HandleMovement(MoveSelectionEvent moveEvent)
|
||||
public override bool HandleMovement(MoveSelectionEvent moveEvent)
|
||||
{
|
||||
var maniaBlueprint = (ManiaSelectionBlueprint)moveEvent.Blueprint;
|
||||
int lastColumn = maniaBlueprint.DrawableObject.HitObject.Column;
|
||||
@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Mania.Edit
|
||||
performDragMovement(moveEvent);
|
||||
performColumnMovement(lastColumn, moveEvent);
|
||||
|
||||
base.HandleMovement(moveEvent);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
230
osu.Game.Rulesets.Osu.Tests/TestSceneFollowPoints.cs
Normal file
230
osu.Game.Rulesets.Osu.Tests/TestSceneFollowPoints.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -42,11 +42,19 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
[Cached(typeof(IDistanceSnapProvider))]
|
||||
private readonly SnapProvider snapProvider = new SnapProvider();
|
||||
|
||||
private readonly TestOsuDistanceSnapGrid grid;
|
||||
private TestOsuDistanceSnapGrid grid;
|
||||
|
||||
public TestSceneOsuDistanceSnapGrid()
|
||||
{
|
||||
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[]
|
||||
{
|
||||
@ -58,14 +66,6 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
grid = new TestOsuDistanceSnapGrid(new HitCircle { Position = grid_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)]
|
||||
@ -102,6 +102,27 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
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}", () =>
|
||||
{
|
||||
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 TestOsuDistanceSnapGrid(OsuHitObject hitObject)
|
||||
: base(hitObject)
|
||||
public TestOsuDistanceSnapGrid(OsuHitObject hitObject, OsuHitObject nextHitObject = null)
|
||||
: base(hitObject, nextHitObject)
|
||||
{
|
||||
}
|
||||
}
|
||||
@ -164,9 +185,9 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
|
||||
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;
|
||||
|
||||
|
@ -16,6 +16,7 @@ using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
@ -85,6 +86,93 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
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()
|
||||
{
|
||||
AddStep("move hitobject", () =>
|
||||
@ -104,11 +192,24 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
() => 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
|
||||
{
|
||||
public new SliderBodyPiece BodyPiece => base.BodyPiece;
|
||||
public new TestSliderCircleBlueprint HeadBlueprint => (TestSliderCircleBlueprint)base.HeadBlueprint;
|
||||
public new TestSliderCircleBlueprint TailBlueprint => (TestSliderCircleBlueprint)base.TailBlueprint;
|
||||
public new PathControlPointVisualiser ControlPointVisualiser => base.ControlPointVisualiser;
|
||||
|
||||
public TestSliderBlueprint(DrawableSlider slider)
|
||||
: base(slider)
|
||||
|
@ -2,7 +2,7 @@
|
||||
<Import Project="..\osu.TestProject.props" />
|
||||
<ItemGroup Label="Package References">
|
||||
<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="NUnit3TestAdapter" Version="3.15.1" />
|
||||
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
|
||||
|
@ -3,26 +3,35 @@
|
||||
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Lines;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
{
|
||||
public class PathControlPointPiece : BlueprintPiece<Slider>
|
||||
{
|
||||
public Action<int> RequestSelection;
|
||||
public Action<Vector2[]> ControlPointsChanged;
|
||||
|
||||
private readonly Slider slider;
|
||||
private readonly int index;
|
||||
public readonly BindableBool IsSelected = new BindableBool();
|
||||
public readonly int Index;
|
||||
|
||||
private readonly Slider slider;
|
||||
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]
|
||||
private OsuColour colours { get; set; }
|
||||
@ -30,7 +39,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
public PathControlPointPiece(Slider slider, int index)
|
||||
{
|
||||
this.slider = slider;
|
||||
this.index = index;
|
||||
Index = index;
|
||||
|
||||
Origin = Anchor.Centre;
|
||||
AutoSizeAxes = Axes.Both;
|
||||
@ -42,13 +51,36 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
Anchor = Anchor.Centre,
|
||||
PathRadius = 1
|
||||
},
|
||||
marker = new CircularContainer
|
||||
marker = new Container
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Size = new Vector2(10),
|
||||
Masking = true,
|
||||
Child = new Box { RelativeSizeAxes = Axes.Both }
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Children = new[]
|
||||
{
|
||||
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();
|
||||
|
||||
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();
|
||||
|
||||
if (index != slider.Path.ControlPoints.Length - 1)
|
||||
if (Index != slider.Path.ControlPoints.Length - 1)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
// The connecting path is excluded from positional input
|
||||
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 OnDrag(DragEvent e)
|
||||
{
|
||||
var newControlPoints = slider.Path.ControlPoints.ToArray();
|
||||
|
||||
if (index == 0)
|
||||
if (Index == 0)
|
||||
{
|
||||
// Special handling for the head - only the position of the slider changes
|
||||
slider.Position += e.Delta;
|
||||
// Special handling for the head control point - the position of the slider changes which means the snapped position and time have to be taken into account
|
||||
(Vector2 snappedPosition, double snappedTime) = snapProvider?.GetSnappedPosition(e.MousePosition, slider.StartTime) ?? (e.MousePosition, slider.StartTime);
|
||||
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
|
||||
for (int i = 1; i < newControlPoints.Length; i++)
|
||||
newControlPoints[i] -= e.Delta;
|
||||
newControlPoints[i] -= movementDelta;
|
||||
}
|
||||
else
|
||||
newControlPoints[index] += e.Delta;
|
||||
newControlPoints[Index] += e.Delta;
|
||||
|
||||
if (isSegmentSeparatorWithNext)
|
||||
newControlPoints[index + 1] = newControlPoints[index];
|
||||
newControlPoints[Index + 1] = newControlPoints[Index];
|
||||
|
||||
if (isSegmentSeparatorWithPrevious)
|
||||
newControlPoints[index - 1] = newControlPoints[index];
|
||||
newControlPoints[Index - 1] = newControlPoints[Index];
|
||||
|
||||
ControlPointsChanged?.Invoke(newControlPoints);
|
||||
|
||||
@ -107,8 +179,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
|
||||
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];
|
||||
}
|
||||
}
|
||||
|
@ -2,36 +2,132 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
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.Screens.Edit.Compose;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
{
|
||||
public class PathControlPointVisualiser : CompositeDrawable
|
||||
public class PathControlPointVisualiser : CompositeDrawable, IKeyBindingHandler<PlatformAction>
|
||||
{
|
||||
public Action<Vector2[]> ControlPointsChanged;
|
||||
|
||||
internal readonly Container<PathControlPointPiece> Pieces;
|
||||
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.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()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
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)
|
||||
pieces.Remove(pieces[pieces.Count - 1]);
|
||||
while (slider.Path.ControlPoints.Length > Pieces.Count)
|
||||
{
|
||||
var piece = new PathControlPointPiece(slider, Pieces.Count)
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -43,5 +43,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
Size = body.Size;
|
||||
OriginPosition = body.PathOffset;
|
||||
}
|
||||
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => body.ReceivePositionalInputAt(screenSpacePos);
|
||||
}
|
||||
}
|
||||
|
@ -51,7 +51,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
bodyPiece = new SliderBodyPiece(),
|
||||
headCirclePiece = new HitCirclePiece(),
|
||||
tailCirclePiece = new HitCirclePiece(),
|
||||
new PathControlPointVisualiser(HitObject) { ControlPointsChanged = _ => updateSlider() },
|
||||
new PathControlPointVisualiser(HitObject, false) { ControlPointsChanged = _ => updateSlider() },
|
||||
};
|
||||
|
||||
setState(PlacementState.Initial);
|
||||
|
@ -18,6 +18,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
protected readonly SliderBodyPiece BodyPiece;
|
||||
protected readonly SliderCircleSelectionBlueprint HeadBlueprint;
|
||||
protected readonly SliderCircleSelectionBlueprint TailBlueprint;
|
||||
protected readonly PathControlPointVisualiser ControlPointVisualiser;
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
private HitObjectComposer composer { get; set; }
|
||||
@ -32,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
BodyPiece = new SliderBodyPiece(),
|
||||
HeadBlueprint = CreateCircleSelectionBlueprint(slider, SliderPosition.Start),
|
||||
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;
|
||||
|
||||
HitObject.Path = new SliderPath(unsnappedPath.Type, controlPoints, snappedDistance);
|
||||
|
||||
UpdateHitObject();
|
||||
}
|
||||
|
||||
public override Vector2 SelectionPoint => HeadBlueprint.SelectionPoint;
|
||||
|
@ -8,8 +8,8 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
public class OsuDistanceSnapGrid : CircularDistanceSnapGrid
|
||||
{
|
||||
public OsuDistanceSnapGrid(OsuHitObject hitObject)
|
||||
: base(hitObject, hitObject.StackedEndPosition)
|
||||
public OsuDistanceSnapGrid(OsuHitObject hitObject, OsuHitObject nextHitObject)
|
||||
: base(hitObject, nextHitObject, hitObject.StackedEndPosition)
|
||||
{
|
||||
Masking = true;
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Beatmaps;
|
||||
@ -60,25 +61,40 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
var objects = selectedHitObjects.ToList();
|
||||
|
||||
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)
|
||||
return null;
|
||||
|
||||
return new OsuDistanceSnapGrid(lastObject);
|
||||
sourceIndex = i;
|
||||
}
|
||||
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)
|
||||
return null;
|
||||
OsuHitObject sourceObject = EditorBeatmap.HitObjects[sourceIndex];
|
||||
OsuHitObject targetObject = sourceIndex + targetOffset < EditorBeatmap.HitObjects.Count ? EditorBeatmap.HitObjects[sourceIndex + targetOffset] : null;
|
||||
|
||||
return new OsuDistanceSnapGrid(lastObject);
|
||||
}
|
||||
return new OsuDistanceSnapGrid(sourceObject, targetObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,13 +4,34 @@
|
||||
using System.Linq;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
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>())
|
||||
{
|
||||
if (h is Spinner)
|
||||
@ -22,7 +43,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
h.Position += moveEvent.InstantDelta;
|
||||
}
|
||||
|
||||
base.HandleMovement(moveEvent);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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; }
|
||||
}
|
||||
}
|
@ -12,6 +12,9 @@ using osu.Game.Skinning;
|
||||
|
||||
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
|
||||
{
|
||||
private const float width = 8;
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,121 +1,110 @@
|
||||
// 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 osuTK;
|
||||
using System.Linq;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
|
||||
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>
|
||||
/// Determines how much space there is between points.
|
||||
/// All the <see cref="FollowPointConnection"/>s contained by this <see cref="FollowPointRenderer"/>.
|
||||
/// </summary>
|
||||
public int PointDistance
|
||||
{
|
||||
get => pointDistance;
|
||||
set
|
||||
{
|
||||
if (pointDistance == value) return;
|
||||
internal IReadOnlyList<FollowPointConnection> Connections => connections;
|
||||
|
||||
pointDistance = value;
|
||||
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();
|
||||
}
|
||||
}
|
||||
private readonly List<FollowPointConnection> connections = new List<FollowPointConnection>();
|
||||
|
||||
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)
|
||||
return;
|
||||
// Groups are sorted by their start time when added such that the index can be used to post-process other surrounding connections
|
||||
int index = connections.AddInPlace(connection, Comparer<FollowPointConnection>.Create((g1, g2) => g1.StartTime.Value.CompareTo(g2.StartTime.Value)));
|
||||
|
||||
OsuHitObject prevHitObject = null;
|
||||
|
||||
foreach (var currHitObject in hitObjects)
|
||||
if (index < connections.Count - 1)
|
||||
{
|
||||
if (prevHitObject != null && !currHitObject.NewCombo && !(prevHitObject is Spinner) && !(currHitObject is Spinner))
|
||||
{
|
||||
Vector2 startPosition = prevHitObject.EndPosition;
|
||||
Vector2 endPosition = currHitObject.Position;
|
||||
double startTime = (prevHitObject as IHasEndTime)?.EndTime ?? prevHitObject.StartTime;
|
||||
double endTime = currHitObject.StartTime;
|
||||
// Update the connection's end point to the next connection's start point
|
||||
// h1 -> -> -> h2
|
||||
// connection nextGroup
|
||||
|
||||
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)(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;
|
||||
FollowPointConnection nextConnection = connections[index + 1];
|
||||
connection.End = nextConnection.Start;
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -37,6 +37,8 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
{
|
||||
PathBindable.Value = value;
|
||||
endPositionCache.Invalidate();
|
||||
|
||||
updateNestedPositions();
|
||||
}
|
||||
}
|
||||
|
||||
@ -48,14 +50,9 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
set
|
||||
{
|
||||
base.Position = value;
|
||||
|
||||
endPositionCache.Invalidate();
|
||||
|
||||
if (HeadCircle != null)
|
||||
HeadCircle.Position = value;
|
||||
|
||||
if (TailCircle != null)
|
||||
TailCircle.Position = EndPosition;
|
||||
updateNestedPositions();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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) =>
|
||||
nodeIndex < NodeSamples.Count ? NodeSamples[nodeIndex] : Samples;
|
||||
|
||||
|
@ -9,7 +9,6 @@ using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables.Connections;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using System.Linq;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Osu.UI.Cursor;
|
||||
using osu.Game.Skinning;
|
||||
@ -20,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
{
|
||||
private readonly ApproachCircleProxyContainer approachCircles;
|
||||
private readonly JudgementContainer<DrawableOsuJudgement> judgementLayer;
|
||||
private readonly ConnectionRenderer<OsuHitObject> connectionLayer;
|
||||
private readonly FollowPointRenderer followPoints;
|
||||
|
||||
public static readonly Vector2 BASE_SIZE = new Vector2(512, 384);
|
||||
|
||||
@ -30,7 +29,7 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
{
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
connectionLayer = new FollowPointRenderer
|
||||
followPoints = new FollowPointRenderer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Depth = 2,
|
||||
@ -64,11 +63,18 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
};
|
||||
|
||||
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)
|
||||
|
@ -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();
|
||||
cursorScaleContainer.MoveTo(GameplayCursor.ActiveCursor.Position);
|
||||
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 = null;
|
||||
GameplayCursor.ActiveCursor.Show();
|
||||
|
||||
base.Hide();
|
||||
GameplayCursor?.ActiveCursor?.Show();
|
||||
}
|
||||
|
||||
protected override bool OnHover(HoverEvent e) => true;
|
||||
|
@ -2,7 +2,7 @@
|
||||
<Import Project="..\osu.TestProject.props" />
|
||||
<ItemGroup Label="Package References">
|
||||
<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="NUnit3TestAdapter" Version="3.15.1" />
|
||||
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
|
||||
|
@ -34,6 +34,7 @@ namespace osu.Game.Tests.Visual.Components
|
||||
PreviewTrack track = null;
|
||||
|
||||
AddStep("get track", () => track = getOwnedTrack());
|
||||
AddUntilStep("wait loaded", () => track.IsLoaded);
|
||||
AddStep("start", () => track.Start());
|
||||
AddAssert("started", () => track.IsRunning);
|
||||
AddStep("stop", () => track.Stop());
|
||||
@ -52,6 +53,8 @@ namespace osu.Game.Tests.Visual.Components
|
||||
track2 = getOwnedTrack();
|
||||
});
|
||||
|
||||
AddUntilStep("wait loaded", () => track1.IsLoaded && track2.IsLoaded);
|
||||
|
||||
AddStep("start track 1", () => track1.Start());
|
||||
AddStep("start track 2", () => track2.Start());
|
||||
AddAssert("track 1 stopped", () => !track1.IsRunning);
|
||||
@ -64,6 +67,7 @@ namespace osu.Game.Tests.Visual.Components
|
||||
PreviewTrack track = null;
|
||||
|
||||
AddStep("get track", () => track = getOwnedTrack());
|
||||
AddUntilStep("wait loaded", () => track.IsLoaded);
|
||||
AddStep("start", () => track.Start());
|
||||
AddStep("stop by owner", () => trackManager.StopAnyPlaying(this));
|
||||
AddAssert("stopped", () => !track.IsRunning);
|
||||
@ -76,6 +80,7 @@ namespace osu.Game.Tests.Visual.Components
|
||||
PreviewTrack track = null;
|
||||
|
||||
AddStep("get track", () => Add(owner = new TestTrackOwner(track = getTrack())));
|
||||
AddUntilStep("wait loaded", () => track.IsLoaded);
|
||||
AddStep("start", () => track.Start());
|
||||
AddStep("attempt stop", () => trackManager.StopAnyPlaying(this));
|
||||
AddAssert("not stopped", () => track.IsRunning);
|
||||
@ -89,16 +94,24 @@ namespace osu.Game.Tests.Visual.Components
|
||||
{
|
||||
var track = getTrack();
|
||||
|
||||
Add(track);
|
||||
LoadComponentAsync(track, Add);
|
||||
|
||||
return track;
|
||||
}
|
||||
|
||||
private class TestTrackOwner : CompositeDrawable, IPreviewTrackOwner
|
||||
{
|
||||
private readonly PreviewTrack track;
|
||||
|
||||
public TestTrackOwner(PreviewTrack track)
|
||||
{
|
||||
AddInternal(track);
|
||||
this.track = track;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
LoadComponentAsync(track, AddInternal);
|
||||
}
|
||||
|
||||
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
|
||||
|
@ -32,7 +32,11 @@ namespace osu.Game.Tests.Visual.Editor
|
||||
{
|
||||
editorBeatmap = new EditorBeatmap<OsuHitObject>(new OsuBeatmap());
|
||||
editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = beat_length });
|
||||
}
|
||||
|
||||
[SetUp]
|
||||
public void Setup() => Schedule(() =>
|
||||
{
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
@ -42,7 +46,7 @@ namespace osu.Game.Tests.Visual.Editor
|
||||
},
|
||||
new TestDistanceSnapGrid(new HitObject(), grid_position)
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
[TestCase(1)]
|
||||
[TestCase(2)]
|
||||
@ -57,12 +61,29 @@ namespace osu.Game.Tests.Visual.Editor
|
||||
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
|
||||
{
|
||||
public new float DistanceSpacing => base.DistanceSpacing;
|
||||
|
||||
public TestDistanceSnapGrid(HitObject hitObject, Vector2 centrePosition)
|
||||
: base(hitObject, centrePosition)
|
||||
public TestDistanceSnapGrid(HitObject hitObject, Vector2 centrePosition, HitObject nextHitObject = null)
|
||||
: base(hitObject, nextHitObject, centrePosition)
|
||||
{
|
||||
}
|
||||
|
||||
@ -77,7 +98,7 @@ namespace osu.Game.Tests.Visual.Editor
|
||||
|
||||
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
|
||||
{
|
||||
@ -90,7 +111,7 @@ namespace osu.Game.Tests.Visual.Editor
|
||||
|
||||
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
|
||||
{
|
||||
@ -103,7 +124,7 @@ namespace osu.Game.Tests.Visual.Editor
|
||||
|
||||
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
|
||||
{
|
||||
@ -116,7 +137,7 @@ namespace osu.Game.Tests.Visual.Editor
|
||||
|
||||
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
|
||||
{
|
||||
@ -138,9 +159,9 @@ namespace osu.Game.Tests.Visual.Editor
|
||||
|
||||
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;
|
||||
|
||||
|
@ -10,9 +10,9 @@ using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Screens.Edit.Compose.Components.Timeline;
|
||||
using osuTK;
|
||||
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 bool started;
|
||||
|
35
osu.Game.Tests/Visual/Editor/TestSceneTimingScreen.cs
Normal file
35
osu.Game.Tests/Visual/Editor/TestSceneTimingScreen.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
@ -69,6 +69,24 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
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]
|
||||
public void TestResumeWithResumeOverlaySkipped()
|
||||
{
|
||||
@ -219,6 +237,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
AddUntilStep("player not exited", () => Player.IsCurrentScreen());
|
||||
AddStep("exit", () => Player.Exit());
|
||||
confirmExited();
|
||||
confirmNoTrackAdjustments();
|
||||
}
|
||||
|
||||
private void confirmPaused()
|
||||
@ -240,6 +259,11 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
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 pause() => AddStep("pause", () => Player.Pause());
|
||||
private void resume() => AddStep("resume", () => Player.Resume());
|
||||
|
@ -7,11 +7,10 @@ using osu.Game.Online;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Users;
|
||||
using osuTK;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Screens.Ranking.Pages;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
@ -42,7 +41,6 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Size = new Vector2(80, 40),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
@ -3,11 +3,16 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Screens.Ranking;
|
||||
using osu.Game.Screens.Ranking.Pages;
|
||||
@ -22,11 +27,13 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
public override IReadOnlyList<Type> RequiredTypes => new[]
|
||||
{
|
||||
typeof(ScoreInfo),
|
||||
typeof(Results),
|
||||
typeof(ResultsPage),
|
||||
typeof(ScoreResultsPage),
|
||||
typeof(LocalLeaderboardPage)
|
||||
typeof(RetryButton),
|
||||
typeof(ReplayDownloadButton),
|
||||
typeof(LocalLeaderboardPage),
|
||||
typeof(TestPlayer)
|
||||
};
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
@ -42,26 +49,82 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
var beatmapInfo = beatmaps.QueryBeatmap(b => b.RulesetID == 0);
|
||||
if (beatmapInfo != null)
|
||||
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,
|
||||
Accuracy = 0.98,
|
||||
MaxCombo = 123,
|
||||
Rank = ScoreRank.A,
|
||||
Date = DateTimeOffset.Now,
|
||||
Statistics = new Dictionary<HitResult, int>
|
||||
{ HitResult.Great, 50 },
|
||||
{ HitResult.Good, 20 },
|
||||
{ HitResult.Meh, 50 },
|
||||
{ HitResult.Miss, 1 }
|
||||
},
|
||||
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 },
|
||||
{ HitResult.Good, 20 },
|
||||
{ HitResult.Meh, 50 },
|
||||
{ HitResult.Miss, 1 }
|
||||
},
|
||||
User = new User
|
||||
{
|
||||
Username = "peppy",
|
||||
}
|
||||
}));
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private class TestSoloResults : SoloResults
|
||||
{
|
||||
public HotkeyRetryOverlay RetryOverlay;
|
||||
|
||||
public TestSoloResults(ScoreInfo score)
|
||||
: base(score)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
RetryOverlay = InternalChildren.OfType<HotkeyRetryOverlay>().SingleOrDefault();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
102
osu.Game.Tests/Visual/Online/TestSceneBeatmapRulesetSelector.cs
Normal file
102
osu.Game.Tests/Visual/Online/TestSceneBeatmapRulesetSelector.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -40,24 +40,19 @@ namespace osu.Game.Tests.Visual.Online
|
||||
typeof(PreviewButton),
|
||||
typeof(SuccessRate),
|
||||
typeof(BeatmapAvailability),
|
||||
typeof(BeatmapRulesetSelector),
|
||||
typeof(BeatmapRulesetTabItem),
|
||||
};
|
||||
|
||||
protected override bool UseOnlineAPI => true;
|
||||
|
||||
private RulesetInfo taikoRuleset;
|
||||
private RulesetInfo maniaRuleset;
|
||||
|
||||
public TestSceneBeatmapSetOverlay()
|
||||
{
|
||||
Add(overlay = new TestBeatmapSetOverlay());
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(RulesetStore rulesets)
|
||||
{
|
||||
taikoRuleset = rulesets.GetRuleset(1);
|
||||
maniaRuleset = rulesets.GetRuleset(3);
|
||||
}
|
||||
[Resolved]
|
||||
private RulesetStore rulesets { get; set; }
|
||||
|
||||
[Test]
|
||||
public void TestLoading()
|
||||
@ -111,7 +106,7 @@ namespace osu.Game.Tests.Visual.Online
|
||||
StarDifficulty = 9.99,
|
||||
Version = @"TEST",
|
||||
Length = 456000,
|
||||
Ruleset = maniaRuleset,
|
||||
Ruleset = rulesets.GetRuleset(3),
|
||||
BaseDifficulty = new BeatmapDifficulty
|
||||
{
|
||||
CircleSize = 1,
|
||||
@ -189,7 +184,7 @@ namespace osu.Game.Tests.Visual.Online
|
||||
StarDifficulty = 5.67,
|
||||
Version = @"ANOTHER TEST",
|
||||
Length = 123000,
|
||||
Ruleset = taikoRuleset,
|
||||
Ruleset = rulesets.GetRuleset(1),
|
||||
BaseDifficulty = new BeatmapDifficulty
|
||||
{
|
||||
CircleSize = 9,
|
||||
@ -217,6 +212,54 @@ namespace osu.Game.Tests.Visual.Online
|
||||
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]
|
||||
public void TestHide()
|
||||
{
|
||||
@ -281,12 +324,12 @@ namespace osu.Game.Tests.Visual.Online
|
||||
|
||||
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
|
||||
{
|
||||
public bool DownloadButtonsVisible => Header.DownloadButtonsVisible;
|
||||
public new Header Header => base.Header;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,7 @@
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Appveyor.TestLogger" 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="NUnit3TestAdapter" Version="3.15.1" />
|
||||
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
|
||||
|
@ -5,7 +5,7 @@
|
||||
</PropertyGroup>
|
||||
<ItemGroup Label="Package References">
|
||||
<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="NUnit3TestAdapter" Version="3.15.1" />
|
||||
</ItemGroup>
|
||||
|
@ -7,6 +7,7 @@ using osu.Framework.Graphics.Colour;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Video;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Game.Graphics;
|
||||
|
||||
namespace osu.Game.Tournament.Components
|
||||
@ -15,6 +16,8 @@ namespace osu.Game.Tournament.Components
|
||||
{
|
||||
private readonly VideoSprite video;
|
||||
|
||||
private readonly ManualClock manualClock;
|
||||
|
||||
public TourneyVideo(Stream stream)
|
||||
{
|
||||
if (stream == null)
|
||||
@ -30,6 +33,7 @@ namespace osu.Game.Tournament.Components
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
FillMode = FillMode.Fit,
|
||||
Clock = new FramedClock(manualClock = new ManualClock())
|
||||
};
|
||||
}
|
||||
|
||||
@ -41,5 +45,17 @@ namespace osu.Game.Tournament.Components
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -15,7 +15,6 @@ using osu.Framework.Logging;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Tournament.Components;
|
||||
using osu.Game.Tournament.Models;
|
||||
using osu.Game.Tournament.Screens.Drawings.Components;
|
||||
@ -24,7 +23,7 @@ using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Tournament.Screens.Drawings
|
||||
{
|
||||
public class DrawingsScreen : CompositeDrawable
|
||||
public class DrawingsScreen : TournamentScreen
|
||||
{
|
||||
private const string results_filename = "drawings_results.txt";
|
||||
|
||||
@ -128,21 +127,21 @@ namespace osu.Game.Tournament.Screens.Drawings
|
||||
// Control panel container
|
||||
new ControlPanel
|
||||
{
|
||||
new OsuButton
|
||||
new TourneyButton
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
|
||||
Text = "Begin random",
|
||||
Action = teamsContainer.StartScrolling,
|
||||
},
|
||||
new OsuButton
|
||||
new TourneyButton
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
|
||||
Text = "Stop random",
|
||||
Action = teamsContainer.StopScrolling,
|
||||
},
|
||||
new OsuButton
|
||||
new TourneyButton
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
|
||||
@ -150,7 +149,7 @@ namespace osu.Game.Tournament.Screens.Drawings
|
||||
Action = reloadTeams
|
||||
},
|
||||
new ControlPanel.Spacer(),
|
||||
new OsuButton
|
||||
new TourneyButton
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
|
||||
|
@ -11,7 +11,6 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Overlays.Settings;
|
||||
@ -32,7 +31,7 @@ namespace osu.Game.Tournament.Screens.Editors
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
ControlPanel.Add(new OsuButton
|
||||
ControlPanel.Add(new TourneyButton
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Text = "Add all countries",
|
||||
|
@ -9,7 +9,6 @@ using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Overlays.Settings;
|
||||
using osu.Game.Tournament.Components;
|
||||
using osuTK;
|
||||
@ -56,7 +55,7 @@ namespace osu.Game.Tournament.Screens.Editors
|
||||
{
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new OsuButton
|
||||
new TourneyButton
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Text = "Add new",
|
||||
|
@ -103,13 +103,13 @@ namespace osu.Game.Tournament.Screens.Gameplay
|
||||
{
|
||||
Children = new Drawable[]
|
||||
{
|
||||
warmupButton = new OsuButton
|
||||
warmupButton = new TourneyButton
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Text = "Toggle warmup",
|
||||
Action = () => warmup.Toggle()
|
||||
},
|
||||
new OsuButton
|
||||
new TourneyButton
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Text = "Toggle chat",
|
||||
|
@ -60,32 +60,32 @@ namespace osu.Game.Tournament.Screens.MapPool
|
||||
{
|
||||
Text = "Current Mode"
|
||||
},
|
||||
buttonRedBan = new OsuButton
|
||||
buttonRedBan = new TourneyButton
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Text = "Red Ban",
|
||||
Action = () => setMode(TeamColour.Red, ChoiceType.Ban)
|
||||
},
|
||||
buttonBlueBan = new OsuButton
|
||||
buttonBlueBan = new TourneyButton
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Text = "Blue Ban",
|
||||
Action = () => setMode(TeamColour.Blue, ChoiceType.Ban)
|
||||
},
|
||||
buttonRedPick = new OsuButton
|
||||
buttonRedPick = new TourneyButton
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Text = "Red Pick",
|
||||
Action = () => setMode(TeamColour.Red, ChoiceType.Pick)
|
||||
},
|
||||
buttonBluePick = new OsuButton
|
||||
buttonBluePick = new TourneyButton
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Text = "Blue Pick",
|
||||
Action = () => setMode(TeamColour.Blue, ChoiceType.Pick)
|
||||
},
|
||||
new ControlPanel.Spacer(),
|
||||
new OsuButton
|
||||
new TourneyButton
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Text = "Reset",
|
||||
|
@ -10,6 +10,8 @@ namespace osu.Game.Tournament.Screens
|
||||
{
|
||||
public abstract class TournamentScreen : CompositeDrawable
|
||||
{
|
||||
public const double FADE_DELAY = 200;
|
||||
|
||||
[Resolved]
|
||||
protected LadderInfo LadderInfo { get; private set; }
|
||||
|
||||
@ -18,14 +20,8 @@ namespace osu.Game.Tournament.Screens
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
}
|
||||
|
||||
public override void Hide()
|
||||
{
|
||||
this.FadeOut(200);
|
||||
}
|
||||
public override void Hide() => this.FadeOut(FADE_DELAY);
|
||||
|
||||
public override void Show()
|
||||
{
|
||||
this.FadeIn(200);
|
||||
}
|
||||
public override void Show() => this.FadeIn(FADE_DELAY);
|
||||
}
|
||||
}
|
||||
|
@ -18,7 +18,6 @@ using osu.Framework.IO.Stores;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Tournament.IPC;
|
||||
using osu.Game.Tournament.Models;
|
||||
@ -76,7 +75,7 @@ namespace osu.Game.Tournament
|
||||
|
||||
AddRange(new[]
|
||||
{
|
||||
new OsuButton
|
||||
new TourneyButton
|
||||
{
|
||||
Text = "Save Changes",
|
||||
Width = 140,
|
||||
@ -215,7 +214,7 @@ namespace osu.Game.Tournament
|
||||
|
||||
foreach (var r in ladder.Rounds)
|
||||
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 });
|
||||
req.Perform(API);
|
||||
|
@ -8,7 +8,8 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
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.Models;
|
||||
using osu.Game.Tournament.Screens;
|
||||
@ -36,6 +37,7 @@ namespace osu.Game.Tournament
|
||||
private TournamentMatchChatDisplay chat = new TournamentMatchChatDisplay();
|
||||
|
||||
private Container chatContainer;
|
||||
private FillFlowContainer buttons;
|
||||
|
||||
public TournamentSceneManager()
|
||||
{
|
||||
@ -101,68 +103,136 @@ namespace osu.Game.Tournament
|
||||
Colour = Color4.Black,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
new FillFlowContainer
|
||||
buttons = new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(2),
|
||||
Padding = new MarginPadding(2),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new OsuButton { RelativeSizeAxes = Axes.X, Text = "Setup", Action = () => SetScreen(typeof(SetupScreen)) },
|
||||
new Container { RelativeSizeAxes = Axes.X, Height = 50 },
|
||||
new OsuButton { RelativeSizeAxes = Axes.X, Text = "Team Editor", Action = () => SetScreen(typeof(TeamEditorScreen)) },
|
||||
new OsuButton { RelativeSizeAxes = Axes.X, Text = "Rounds Editor", Action = () => SetScreen(typeof(RoundEditorScreen)) },
|
||||
new OsuButton { RelativeSizeAxes = Axes.X, Text = "Bracket Editor", Action = () => SetScreen(typeof(LadderEditorScreen)) },
|
||||
new Container { RelativeSizeAxes = Axes.X, Height = 50 },
|
||||
new OsuButton { RelativeSizeAxes = Axes.X, Text = "Drawings", Action = () => SetScreen(typeof(DrawingsScreen)) },
|
||||
new OsuButton { RelativeSizeAxes = Axes.X, Text = "Showcase", Action = () => SetScreen(typeof(ShowcaseScreen)) },
|
||||
new Container { RelativeSizeAxes = Axes.X, Height = 50 },
|
||||
new OsuButton { RelativeSizeAxes = Axes.X, Text = "Schedule", Action = () => SetScreen(typeof(ScheduleScreen)) },
|
||||
new OsuButton { RelativeSizeAxes = Axes.X, Text = "Bracket", Action = () => SetScreen(typeof(LadderScreen)) },
|
||||
new Container { RelativeSizeAxes = Axes.X, Height = 50 },
|
||||
new OsuButton { RelativeSizeAxes = Axes.X, Text = "TeamIntro", Action = () => SetScreen(typeof(TeamIntroScreen)) },
|
||||
new OsuButton { RelativeSizeAxes = Axes.X, Text = "MapPool", Action = () => SetScreen(typeof(MapPoolScreen)) },
|
||||
new OsuButton { RelativeSizeAxes = Axes.X, Text = "Gameplay", Action = () => SetScreen(typeof(GameplayScreen)) },
|
||||
new Container { RelativeSizeAxes = Axes.X, Height = 50 },
|
||||
new OsuButton { RelativeSizeAxes = Axes.X, Text = "Win", Action = () => SetScreen(typeof(TeamWinScreen)) },
|
||||
new ScreenButton(typeof(SetupScreen)) { Text = "Setup", RequestSelection = SetScreen },
|
||||
new Separator(),
|
||||
new ScreenButton(typeof(TeamEditorScreen)) { Text = "Team Editor", RequestSelection = SetScreen },
|
||||
new ScreenButton(typeof(RoundEditorScreen)) { Text = "Rounds Editor", RequestSelection = SetScreen },
|
||||
new ScreenButton(typeof(LadderEditorScreen)) { Text = "Bracket Editor", RequestSelection = SetScreen },
|
||||
new Separator(),
|
||||
new ScreenButton(typeof(ScheduleScreen)) { Text = "Schedule", RequestSelection = SetScreen },
|
||||
new ScreenButton(typeof(LadderScreen)) { Text = "Bracket", RequestSelection = SetScreen },
|
||||
new Separator(),
|
||||
new ScreenButton(typeof(TeamIntroScreen)) { Text = "TeamIntro", RequestSelection = SetScreen },
|
||||
new Separator(),
|
||||
new ScreenButton(typeof(MapPoolScreen)) { Text = "MapPool", RequestSelection = SetScreen },
|
||||
new ScreenButton(typeof(GameplayScreen)) { Text = "Gameplay", RequestSelection = SetScreen },
|
||||
new Separator(),
|
||||
new ScreenButton(typeof(TeamWinScreen)) { Text = "Win", RequestSelection = SetScreen },
|
||||
new Separator(),
|
||||
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));
|
||||
}
|
||||
|
||||
private float depth;
|
||||
|
||||
private Drawable currentScreen;
|
||||
private ScheduledDelegate scheduledHide;
|
||||
|
||||
public void SetScreen(Type screenType)
|
||||
{
|
||||
var screen = screens.FirstOrDefault(s => s.GetType() == screenType);
|
||||
if (screen == null) return;
|
||||
var target = screens.FirstOrDefault(s => s.GetType() == screenType);
|
||||
|
||||
foreach (var s in screens.Children)
|
||||
if (target == null || currentScreen == target) return;
|
||||
|
||||
if (scheduledHide?.Completed == false)
|
||||
{
|
||||
if (s == screen)
|
||||
{
|
||||
s.Show();
|
||||
if (s is IProvideVideo)
|
||||
video.FadeOut(200);
|
||||
else
|
||||
video.Show();
|
||||
}
|
||||
else
|
||||
s.Hide();
|
||||
scheduledHide.RunTask();
|
||||
scheduledHide.Cancel(); // see https://github.com/ppy/osu-framework/issues/2967
|
||||
scheduledHide = null;
|
||||
}
|
||||
|
||||
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 MapPoolScreen _:
|
||||
chatContainer.FadeIn(100);
|
||||
chatContainer.FadeIn(TournamentScreen.FADE_DELAY);
|
||||
break;
|
||||
|
||||
default:
|
||||
chatContainer.FadeOut(100);
|
||||
chatContainer.FadeOut(TournamentScreen.FADE_DELAY);
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
15
osu.Game.Tournament/TourneyButton.cs
Normal file
15
osu.Game.Tournament/TourneyButton.cs
Normal 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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@ -9,15 +9,18 @@ using osu.Framework.Threading;
|
||||
|
||||
namespace osu.Game.Audio
|
||||
{
|
||||
[LongRunningLoad]
|
||||
public abstract class PreviewTrack : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Invoked when this <see cref="PreviewTrack"/> has stopped playing.
|
||||
/// Not invoked in a thread-safe context.
|
||||
/// </summary>
|
||||
public event Action Stopped;
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when this <see cref="PreviewTrack"/> has started playing.
|
||||
/// Not invoked in a thread-safe context.
|
||||
/// </summary>
|
||||
public event Action Started;
|
||||
|
||||
@ -29,7 +32,7 @@ namespace osu.Game.Audio
|
||||
{
|
||||
track = GetTrack();
|
||||
if (track != null)
|
||||
track.Completed += () => Schedule(Stop);
|
||||
track.Completed += Stop;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -93,6 +96,7 @@ namespace osu.Game.Audio
|
||||
hasStarted = false;
|
||||
|
||||
track.Stop();
|
||||
|
||||
Stopped?.Invoke();
|
||||
}
|
||||
|
||||
|
@ -46,18 +46,18 @@ namespace osu.Game.Audio
|
||||
{
|
||||
var track = CreatePreviewTrack(beatmapSetInfo, trackStore);
|
||||
|
||||
track.Started += () =>
|
||||
track.Started += () => Schedule(() =>
|
||||
{
|
||||
current?.Stop();
|
||||
current = track;
|
||||
audio.Tracks.AddAdjustment(AdjustableProperty.Volume, muteBindable);
|
||||
};
|
||||
});
|
||||
|
||||
track.Stopped += () =>
|
||||
track.Stopped += () => Schedule(() =>
|
||||
{
|
||||
current = null;
|
||||
audio.Tracks.RemoveAdjustment(AdjustableProperty.Volume, muteBindable);
|
||||
};
|
||||
});
|
||||
|
||||
return track;
|
||||
}
|
||||
|
@ -392,8 +392,15 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
req.Failure += e => { LogForModel(set, $"Online retrieval failed for {beatmap} ({e.Message})"); };
|
||||
|
||||
// intentionally blocking to limit web request concurrency
|
||||
req.Perform(api);
|
||||
try
|
||||
{
|
||||
// intentionally blocking to limit web request concurrency
|
||||
req.Perform(api);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
LogForModel(set, $"Online retrieval failed for {beatmap} ({e.Message})");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -43,6 +43,11 @@ namespace osu.Game.Beatmaps.ControlPoints
|
||||
set => BeatLengthBindable.Value = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The BPM at this control point.
|
||||
/// </summary>
|
||||
public double BPM => 60000 / BeatLength;
|
||||
|
||||
public override bool EquivalentTo(ControlPoint other) =>
|
||||
other is TimingControlPoint otherTyped
|
||||
&& TimeSignature == otherTyped.TimeSignature && BeatLength.Equals(otherTyped.BeatLength);
|
||||
|
@ -8,6 +8,7 @@ using osu.Framework.Graphics.Textures;
|
||||
|
||||
namespace osu.Game.Beatmaps.Drawables
|
||||
{
|
||||
[LongRunningLoad]
|
||||
public class BeatmapSetCover : Sprite
|
||||
{
|
||||
private readonly BeatmapSetInfo set;
|
||||
|
@ -86,16 +86,7 @@ namespace osu.Game.Database
|
||||
}, TaskCreationOptions.LongRunning);
|
||||
};
|
||||
|
||||
request.Failure += error =>
|
||||
{
|
||||
DownloadFailed?.Invoke(request);
|
||||
|
||||
if (error is OperationCanceledException) return;
|
||||
|
||||
notification.State = ProgressNotificationState.Cancelled;
|
||||
Logger.Error(error, $"{HumanisedModelName.Titleize()} download failed!");
|
||||
currentDownloads.Remove(request);
|
||||
};
|
||||
request.Failure += triggerFailure;
|
||||
|
||||
notification.CancelRequested += () =>
|
||||
{
|
||||
@ -108,11 +99,31 @@ namespace osu.Game.Database
|
||||
currentDownloads.Add(request);
|
||||
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);
|
||||
|
||||
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));
|
||||
|
@ -8,9 +8,6 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
using osu.Game.Users;
|
||||
|
||||
namespace osu.Game.Graphics.Containers
|
||||
@ -23,21 +20,12 @@ namespace osu.Game.Graphics.Containers
|
||||
}
|
||||
|
||||
private OsuGame game;
|
||||
private ChannelManager channelManager;
|
||||
private Action showNotImplementedError;
|
||||
|
||||
[BackgroundDependencyLoader(true)]
|
||||
private void load(OsuGame game, NotificationOverlay notifications, ChannelManager channelManager)
|
||||
private void load(OsuGame game)
|
||||
{
|
||||
// will be null in tests
|
||||
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)
|
||||
@ -56,85 +44,47 @@ namespace osu.Game.Graphics.Containers
|
||||
foreach (var link in links)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
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)
|
||||
=> createLink(AddText(text, creationParameters), text, url, linkType, linkArgument, tooltipText);
|
||||
public void AddLink(string text, string url, Action<SpriteText> creationParameters = null) =>
|
||||
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)
|
||||
=> createLink(AddText(text, creationParameters), text, tooltipText: tooltipText, action: action);
|
||||
public void AddLink(string text, Action action, string tooltipText = null, Action<SpriteText> creationParameters = null)
|
||||
=> 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)
|
||||
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)
|
||||
=> createLink(AddText(user.Username, creationParameters), user.Username, null, LinkAction.OpenUserProfile, user.Id.ToString(), "View profile");
|
||||
public void AddUserLink(User user, Action<SpriteText> creationParameters = null)
|
||||
=> 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())
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
TooltipText = tooltipText ?? (url != text ? url : string.Empty),
|
||||
Action = action ?? (() =>
|
||||
TooltipText = tooltipText,
|
||||
Action = () =>
|
||||
{
|
||||
switch (linkType)
|
||||
{
|
||||
case LinkAction.OpenBeatmap:
|
||||
// TODO: proper query params handling
|
||||
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.");
|
||||
}
|
||||
}),
|
||||
if (action != null)
|
||||
action();
|
||||
else
|
||||
game.HandleLink(link);
|
||||
},
|
||||
});
|
||||
|
||||
return drawables;
|
||||
}
|
||||
|
||||
// We want the compilers to always be visible no matter where they are, so RelativeSizeAxes is used.
|
||||
|
@ -1,10 +1,12 @@
|
||||
// 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.Diagnostics;
|
||||
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.Sprites;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
@ -19,53 +21,106 @@ namespace osu.Game.Graphics.UserInterface
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
Content.Masking = true;
|
||||
Content.CornerRadius = 5;
|
||||
AddInternal(Content = new Container
|
||||
{
|
||||
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]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
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),
|
||||
});
|
||||
if (backgroundColour == null)
|
||||
BackgroundColour = colours.BlueDark;
|
||||
|
||||
Enabled.ValueChanged += enabledChanged;
|
||||
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)
|
||||
{
|
||||
hover.FadeIn(200);
|
||||
return true;
|
||||
if (Enabled.Value)
|
||||
Hover.FadeIn(200, Easing.OutQuint);
|
||||
|
||||
return base.OnHover(e);
|
||||
}
|
||||
|
||||
protected override void OnHoverLost(HoverLostEvent e)
|
||||
{
|
||||
hover.FadeOut(200);
|
||||
base.OnHoverLost(e);
|
||||
|
||||
Hover.FadeOut(300);
|
||||
}
|
||||
|
||||
protected override bool OnMouseDown(MouseDownEvent e)
|
||||
@ -80,12 +135,17 @@ namespace osu.Game.Graphics.UserInterface
|
||||
return base.OnMouseUp(e);
|
||||
}
|
||||
|
||||
protected override SpriteText CreateText() => new OsuSpriteText
|
||||
protected virtual SpriteText CreateText() => new OsuSpriteText
|
||||
{
|
||||
Depth = -1,
|
||||
Origin = Anchor.Centre,
|
||||
Anchor = Anchor.Centre,
|
||||
Font = OsuFont.GetFont(weight: FontWeight.Bold)
|
||||
};
|
||||
|
||||
private void enabledChanged(ValueChangedEvent<bool> e)
|
||||
{
|
||||
this.FadeColour(e.NewValue ? Color4.White : Color4.Gray, 200, Easing.OutQuint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,30 +1,16 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using osu.Framework.IO.Stores;
|
||||
using SharpCompress.Archives.Zip;
|
||||
using SharpCompress.Common;
|
||||
|
||||
namespace osu.Game.IO.Archives
|
||||
{
|
||||
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 ZipArchive archive;
|
||||
|
||||
@ -58,9 +44,7 @@ namespace osu.Game.IO.Archives
|
||||
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.Where(e => !canBeIgnored(e)).Select(e => e.Key).ToArray();
|
||||
public override IEnumerable<string> Filenames => archive.Entries.Select(e => e.Key).ExcludeSystemFileNames();
|
||||
|
||||
public override Stream GetUnderlyingStream() => archiveStream;
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ namespace osu.Game.Online.API
|
||||
|
||||
public Bindable<UserActivity> Activity { get; } = new Bindable<UserActivity>();
|
||||
|
||||
public bool IsLoggedIn => true;
|
||||
public bool IsLoggedIn => State == APIState.Online;
|
||||
|
||||
public string ProvidedUsername => LocalUser.Value.Username;
|
||||
|
||||
|
@ -81,7 +81,7 @@ namespace osu.Game.Online.Chat
|
||||
//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);
|
||||
|
||||
var details = getLinkDetails(linkText);
|
||||
var details = GetLinkDetails(linkText);
|
||||
result.Links.Add(new Link(linkText, index, displayText.Length, linkActionOverride ?? details.Action, details.Argument));
|
||||
|
||||
//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 indexLength = linkText.Length;
|
||||
|
||||
var details = getLinkDetails(linkText);
|
||||
var details = GetLinkDetails(linkText);
|
||||
var link = new Link(linkText, index, indexLength, details.Action, details.Argument);
|
||||
|
||||
// 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);
|
||||
args[0] = args[0].TrimEnd(':');
|
||||
@ -255,17 +255,17 @@ namespace osu.Game.Online.Chat
|
||||
OriginalText = Text = text;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class LinkDetails
|
||||
public class LinkDetails
|
||||
{
|
||||
public LinkAction Action;
|
||||
public string Argument;
|
||||
|
||||
public LinkDetails(LinkAction action, string argument)
|
||||
{
|
||||
public LinkAction Action;
|
||||
public string Argument;
|
||||
|
||||
public LinkDetails(LinkAction action, string argument)
|
||||
{
|
||||
Action = action;
|
||||
Argument = argument;
|
||||
}
|
||||
Action = action;
|
||||
Argument = argument;
|
||||
}
|
||||
}
|
||||
|
||||
@ -279,6 +279,7 @@ namespace osu.Game.Online.Chat
|
||||
JoinMultiplayerMatch,
|
||||
Spectate,
|
||||
OpenUserProfile,
|
||||
Custom
|
||||
}
|
||||
|
||||
public class Link : IComparable<Link>
|
||||
|
@ -21,6 +21,7 @@ using osu.Game.Users.Drawables;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
using Humanizer;
|
||||
using osu.Game.Online.API;
|
||||
|
||||
namespace osu.Game.Online.Leaderboards
|
||||
{
|
||||
@ -37,6 +38,7 @@ namespace osu.Game.Online.Leaderboards
|
||||
|
||||
private readonly ScoreInfo score;
|
||||
private readonly int rank;
|
||||
private readonly bool allowHighlight;
|
||||
|
||||
private Box background;
|
||||
private Container content;
|
||||
@ -49,17 +51,18 @@ namespace osu.Game.Online.Leaderboards
|
||||
|
||||
private List<ScoreComponentLabel> statisticsLabels;
|
||||
|
||||
public LeaderboardScore(ScoreInfo score, int rank)
|
||||
public LeaderboardScore(ScoreInfo score, int rank, bool allowHighlight = true)
|
||||
{
|
||||
this.score = score;
|
||||
this.rank = rank;
|
||||
this.allowHighlight = allowHighlight;
|
||||
|
||||
RelativeSizeAxes = Axes.X;
|
||||
Height = HEIGHT;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
private void load(IAPIProvider api, OsuColour colour)
|
||||
{
|
||||
var user = score.User;
|
||||
|
||||
@ -100,7 +103,7 @@ namespace osu.Game.Online.Leaderboards
|
||||
background = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Color4.Black,
|
||||
Colour = user.Id == api.LocalUser.Value.Id && allowHighlight ? colour.Green : Color4.Black,
|
||||
Alpha = background_alpha,
|
||||
},
|
||||
},
|
||||
|
@ -215,31 +215,102 @@ namespace osu.Game
|
||||
|
||||
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("/"))
|
||||
url = $"{API.Endpoint}{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>
|
||||
/// Show a beatmap set as an overlay.
|
||||
/// </summary>
|
||||
/// <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>
|
||||
/// Show a user's profile as an overlay.
|
||||
/// </summary>
|
||||
/// <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>
|
||||
/// Show a beatmap's set as an overlay, displaying the given beatmap.
|
||||
/// </summary>
|
||||
/// <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>
|
||||
/// Present a beatmap at song select immediately.
|
||||
@ -397,6 +468,23 @@ namespace osu.Game
|
||||
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)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
@ -17,6 +17,7 @@ using osu.Game.Beatmaps.Drawables;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Rulesets;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
@ -27,10 +28,11 @@ namespace osu.Game.Overlays.BeatmapSet
|
||||
private const float tile_icon_padding = 7;
|
||||
private const float tile_spacing = 2;
|
||||
|
||||
private readonly DifficultiesContainer difficulties;
|
||||
private readonly OsuSpriteText version, starRating;
|
||||
private readonly Statistic plays, favourites;
|
||||
|
||||
public readonly DifficultiesContainer Difficulties;
|
||||
|
||||
public readonly Bindable<BeatmapInfo> Beatmap = new Bindable<BeatmapInfo>();
|
||||
|
||||
private BeatmapSetInfo beatmapSet;
|
||||
@ -43,38 +45,10 @@ namespace osu.Game.Overlays.BeatmapSet
|
||||
if (value == beatmapSet) return;
|
||||
|
||||
beatmapSet = value;
|
||||
|
||||
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()
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
@ -89,7 +63,7 @@ namespace osu.Game.Overlays.BeatmapSet
|
||||
Direction = FillDirection.Vertical,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
difficulties = new DifficultiesContainer
|
||||
Difficulties = new DifficultiesContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
@ -147,6 +121,9 @@ namespace osu.Game.Overlays.BeatmapSet
|
||||
};
|
||||
}
|
||||
|
||||
[Resolved]
|
||||
private IBindable<RulesetInfo> ruleset { get; set; }
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
@ -158,10 +135,39 @@ namespace osu.Game.Overlays.BeatmapSet
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
ruleset.ValueChanged += r => updateDisplay();
|
||||
|
||||
// done here so everything can bind in intialization and get the first trigger
|
||||
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)
|
||||
{
|
||||
version.Text = beatmap?.Version;
|
||||
@ -169,10 +175,10 @@ namespace osu.Game.Overlays.BeatmapSet
|
||||
|
||||
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;
|
||||
|
||||
@ -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 size = 52;
|
||||
@ -320,7 +326,7 @@ namespace osu.Game.Overlays.BeatmapSet
|
||||
}
|
||||
}
|
||||
|
||||
private enum DifficultySelectorState
|
||||
public enum DifficultySelectorState
|
||||
{
|
||||
Selected,
|
||||
NotSelected,
|
||||
|
48
osu.Game/Overlays/BeatmapSet/BeatmapRulesetSelector.cs
Normal file
48
osu.Game/Overlays/BeatmapSet/BeatmapRulesetSelector.cs
Normal 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),
|
||||
};
|
||||
}
|
||||
}
|
145
osu.Game/Overlays/BeatmapSet/BeatmapRulesetTabItem.cs
Normal file
145
osu.Game/Overlays/BeatmapSet/BeatmapRulesetTabItem.cs
Normal 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
|
||||
}
|
||||
}
|
@ -3,6 +3,7 @@
|
||||
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Colour;
|
||||
@ -16,6 +17,7 @@ using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online;
|
||||
using osu.Game.Overlays.BeatmapSet.Buttons;
|
||||
using osu.Game.Overlays.Direct;
|
||||
using osu.Game.Rulesets;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
@ -39,6 +41,7 @@ namespace osu.Game.Overlays.BeatmapSet
|
||||
|
||||
public bool DownloadButtonsVisible => downloadButtonsContainer.Any();
|
||||
|
||||
public readonly BeatmapRulesetSelector RulesetSelector;
|
||||
public readonly BeatmapPicker Picker;
|
||||
|
||||
private readonly FavouriteButton favouriteButton;
|
||||
@ -47,6 +50,9 @@ namespace osu.Game.Overlays.BeatmapSet
|
||||
|
||||
private readonly LoadingAnimation loading;
|
||||
|
||||
[Cached(typeof(IBindable<RulesetInfo>))]
|
||||
private readonly Bindable<RulesetInfo> ruleset = new Bindable<RulesetInfo>();
|
||||
|
||||
public Header()
|
||||
{
|
||||
ExternalLinkButton externalLink;
|
||||
@ -69,12 +75,18 @@ namespace osu.Game.Overlays.BeatmapSet
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = tabs_height,
|
||||
Children = new[]
|
||||
Children = new Drawable[]
|
||||
{
|
||||
tabsBg = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
RulesetSelector = new BeatmapRulesetSelector
|
||||
{
|
||||
Current = ruleset,
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.BottomCentre,
|
||||
}
|
||||
},
|
||||
},
|
||||
new Container
|
||||
@ -223,7 +235,7 @@ namespace osu.Game.Overlays.BeatmapSet
|
||||
|
||||
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;
|
||||
|
||||
if (setInfo.NewValue == null)
|
||||
|
@ -110,7 +110,7 @@ namespace osu.Game.Overlays.Changelog
|
||||
t.Font = fontLarge;
|
||||
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 =>
|
||||
{
|
||||
t.Font = fontLarge;
|
||||
@ -140,7 +140,7 @@ namespace osu.Game.Overlays.Changelog
|
||||
t.Colour = entryColour;
|
||||
});
|
||||
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.Colour = entryColour;
|
||||
|
@ -44,7 +44,17 @@ namespace osu.Game.Overlays.Changelog
|
||||
req.Failure += _ => complete = true;
|
||||
|
||||
// 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)
|
||||
{
|
||||
|
@ -170,6 +170,7 @@ namespace osu.Game.Overlays
|
||||
var tcs = new TaskCompletionSource<bool>();
|
||||
|
||||
var req = new GetChangelogRequest();
|
||||
|
||||
req.Success += res => Schedule(() =>
|
||||
{
|
||||
// remap streams to builds to ensure model equality
|
||||
@ -183,8 +184,22 @@ namespace osu.Game.Overlays
|
||||
|
||||
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;
|
||||
});
|
||||
|
@ -168,12 +168,13 @@ namespace osu.Game.Overlays
|
||||
},
|
||||
}
|
||||
},
|
||||
progressBar = new ProgressBar
|
||||
progressBar = new HoverableProgressBar
|
||||
{
|
||||
Origin = Anchor.BottomCentre,
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Height = progress_height,
|
||||
Height = progress_height / 2,
|
||||
FillColour = colours.Yellow,
|
||||
BackgroundColour = colours.YellowDarker.Opacity(0.5f),
|
||||
OnSeek = musicController.SeekTo
|
||||
}
|
||||
},
|
||||
@ -401,5 +402,20 @@ namespace osu.Game.Overlays
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
using osu.Framework.Allocation;
|
||||
@ -9,21 +8,18 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
|
||||
namespace osu.Game.Overlays.Settings
|
||||
{
|
||||
public class SidebarButton : Button
|
||||
public class SidebarButton : OsuButton
|
||||
{
|
||||
private readonly SpriteIcon drawableIcon;
|
||||
private readonly SpriteText headerText;
|
||||
private readonly Box selectionIndicator;
|
||||
private readonly Container text;
|
||||
public new Action<SettingsSection> Action;
|
||||
|
||||
private SettingsSection section;
|
||||
|
||||
@ -62,12 +58,11 @@ namespace osu.Game.Overlays.Settings
|
||||
|
||||
public SidebarButton()
|
||||
{
|
||||
BackgroundColour = OsuColour.Gray(60);
|
||||
Background.Alpha = 0;
|
||||
|
||||
Height = Sidebar.DEFAULT_WIDTH;
|
||||
RelativeSizeAxes = Axes.X;
|
||||
|
||||
BackgroundColour = Color4.Black;
|
||||
|
||||
AddRange(new Drawable[]
|
||||
{
|
||||
text = new Container
|
||||
@ -99,7 +94,6 @@ namespace osu.Game.Overlays.Settings
|
||||
Anchor = Anchor.CentreRight,
|
||||
Origin = Anchor.CentreRight,
|
||||
},
|
||||
new HoverClickSounds(HoverSampleSet.Loud),
|
||||
});
|
||||
}
|
||||
|
||||
@ -108,23 +102,5 @@ namespace osu.Game.Overlays.Settings
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -123,9 +123,9 @@ namespace osu.Game.Overlays
|
||||
var button = new SidebarButton
|
||||
{
|
||||
Section = section,
|
||||
Action = s =>
|
||||
Action = () =>
|
||||
{
|
||||
SectionsContainer.ScrollTo(s);
|
||||
SectionsContainer.ScrollTo(section);
|
||||
Sidebar.State = ExpandedState.Contracted;
|
||||
},
|
||||
};
|
||||
|
@ -40,6 +40,9 @@ namespace osu.Game.Rulesets.Edit
|
||||
[Resolved]
|
||||
protected IFrameBasedClock EditorClock { get; private set; }
|
||||
|
||||
[Resolved]
|
||||
private IAdjustableClock adjustableClock { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private BindableBeatDivisor beatDivisor { get; set; }
|
||||
|
||||
@ -148,7 +151,7 @@ namespace osu.Game.Rulesets.Edit
|
||||
EditorBeatmap = new EditorBeatmap<TObject>(playableBeatmap);
|
||||
EditorBeatmap.HitObjectAdded += addHitObject;
|
||||
EditorBeatmap.HitObjectRemoved += removeHitObject;
|
||||
EditorBeatmap.StartTimeChanged += updateHitObject;
|
||||
EditorBeatmap.StartTimeChanged += UpdateHitObject;
|
||||
|
||||
var dependencies = new DependencyContainer(parent);
|
||||
dependencies.CacheAs<IEditorBeatmap>(EditorBeatmap);
|
||||
@ -225,11 +228,7 @@ namespace osu.Game.Rulesets.Edit
|
||||
|
||||
private ScheduledDelegate scheduledUpdate;
|
||||
|
||||
private void addHitObject(HitObject hitObject) => updateHitObject(hitObject);
|
||||
|
||||
private void removeHitObject(HitObject hitObject) => updateHitObject(null);
|
||||
|
||||
private void updateHitObject([CanBeNull] HitObject hitObject)
|
||||
public override void UpdateHitObject(HitObject hitObject)
|
||||
{
|
||||
scheduledUpdate?.Cancel();
|
||||
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 bool CursorInPlacementArea => drawableRulesetWrapper.Playfield.ReceivePositionalInputAt(inputManager.CurrentState.Mouse.Position);
|
||||
|
||||
@ -256,6 +259,9 @@ namespace osu.Game.Rulesets.Edit
|
||||
public void EndPlacement(HitObject hitObject)
|
||||
{
|
||||
EditorBeatmap.Add(hitObject);
|
||||
|
||||
adjustableClock.Seek(hitObject.StartTime);
|
||||
|
||||
showGridFor(Enumerable.Empty<HitObject>());
|
||||
}
|
||||
|
||||
@ -351,11 +357,22 @@ namespace osu.Game.Rulesets.Edit
|
||||
[CanBeNull]
|
||||
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 float GetBeatSnapDistanceAt(double referenceTime);
|
||||
|
||||
public abstract float DurationToDistance(double referenceTime, double duration);
|
||||
|
||||
public abstract double DistanceToDuration(double referenceTime, float distance);
|
||||
|
||||
public abstract double GetSnappedDurationFromDistance(double referenceTime, float distance);
|
||||
|
||||
public abstract float GetSnappedDistanceFromDistance(double referenceTime, float distance);
|
||||
}
|
||||
}
|
||||
|
@ -3,10 +3,12 @@
|
||||
|
||||
using System;
|
||||
using osu.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osuTK;
|
||||
|
||||
@ -36,6 +38,9 @@ namespace osu.Game.Rulesets.Edit
|
||||
public override bool HandlePositionalInput => ShouldBeAlive;
|
||||
public override bool RemoveWhenNotAlive => false;
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
private HitObjectComposer composer { get; set; }
|
||||
|
||||
protected SelectionBlueprint(DrawableHitObject 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>
|
||||
/// Selects this <see cref="SelectionBlueprint"/>, causing it to become visible.
|
||||
/// </summary>
|
||||
@ -89,6 +97,11 @@ namespace osu.Game.Rulesets.Edit
|
||||
|
||||
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);
|
||||
|
||||
/// <summary>
|
||||
|
@ -239,6 +239,12 @@ namespace osu.Game.Rulesets.UI
|
||||
continueResume();
|
||||
}
|
||||
|
||||
public override void CancelResume()
|
||||
{
|
||||
// called if the user pauses while the resume overlay is open
|
||||
ResumeOverlay?.Hide();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates and adds the visual representation of a <see cref="TObject"/> to this <see cref="DrawableRuleset{TObject}"/>.
|
||||
/// </summary>
|
||||
@ -453,6 +459,11 @@ namespace osu.Game.Rulesets.UI
|
||||
/// <param name="continueResume">The action to run when resuming is to be completed.</param>
|
||||
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>
|
||||
/// Create a <see cref="ScoreProcessor"/> for the associated ruleset and link with this
|
||||
/// <see cref="DrawableRuleset"/>.
|
||||
|
@ -13,12 +13,6 @@ namespace osu.Game.Rulesets.UI.Scrolling
|
||||
{
|
||||
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<ScrollingDirection> direction = new Bindable<ScrollingDirection>();
|
||||
|
||||
@ -123,28 +117,22 @@ namespace osu.Game.Rulesets.UI.Scrolling
|
||||
if (cached.IsValid)
|
||||
return;
|
||||
|
||||
double endTime = hitObject.HitObject.StartTime;
|
||||
|
||||
if (hitObject.HitObject is IHasEndTime e)
|
||||
{
|
||||
endTime = e.EndTime;
|
||||
|
||||
switch (direction.Value)
|
||||
{
|
||||
case ScrollingDirection.Up:
|
||||
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;
|
||||
|
||||
case ScrollingDirection.Left:
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
hitObject.LifetimeEnd = scrollingInfo.Algorithm.TimeAt(scrollLength * safe_lifetime_end_multiplier, endTime, timeRange.Value, scrollLength);
|
||||
|
||||
foreach (var obj in hitObject.NestedHitObjects)
|
||||
{
|
||||
computeInitialStateRecursive(obj);
|
||||
|
@ -29,7 +29,10 @@ namespace osu.Game.Screens.Edit
|
||||
set
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
@ -218,12 +218,17 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
}
|
||||
|
||||
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.Flash();
|
||||
};
|
||||
}, true);
|
||||
}
|
||||
|
||||
protected override void UpdateValue(float value)
|
||||
|
@ -11,6 +11,7 @@ using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Edit.Tools;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
@ -30,6 +31,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
private SelectionHandler selectionHandler;
|
||||
private InputManager inputManager;
|
||||
|
||||
[Resolved]
|
||||
private IAdjustableClock adjustableClock { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private HitObjectComposer composer { get; set; }
|
||||
|
||||
@ -106,6 +110,17 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
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)
|
||||
{
|
||||
// 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);
|
||||
|
||||
// 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)
|
||||
{
|
||||
if (blueprint.IsHovered)
|
||||
@ -361,7 +382,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
(Vector2 snappedPosition, double snappedTime) = composer.GetSnappedPosition(ToLocalSpace(movePosition), draggedObject.StartTime);
|
||||
|
||||
// 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
|
||||
double offset = snappedTime - draggedObject.StartTime;
|
||||
|
@ -12,8 +12,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
{
|
||||
public abstract class CircularDistanceSnapGrid : DistanceSnapGrid
|
||||
{
|
||||
protected CircularDistanceSnapGrid(HitObject hitObject, Vector2 centrePosition)
|
||||
: base(hitObject, centrePosition)
|
||||
protected CircularDistanceSnapGrid(HitObject hitObject, HitObject nextHitObject, Vector2 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 dy = Math.Max(centrePosition.Y, DrawHeight - centrePosition.Y);
|
||||
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++)
|
||||
{
|
||||
@ -65,15 +65,17 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
|
||||
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)
|
||||
direction = new Vector2(0.001f, 0.001f);
|
||||
|
||||
float distance = direction.Length;
|
||||
|
||||
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 snappedPosition = CentrePosition + normalisedDirection * radialCount * radius;
|
||||
|
@ -1,6 +1,7 @@
|
||||
// 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 JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Caching;
|
||||
using osu.Framework.Graphics;
|
||||
@ -29,6 +30,11 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
/// </summary>
|
||||
protected double StartTime { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The maximum number of distance snapping intervals allowed.
|
||||
/// </summary>
|
||||
protected int MaxIntervals { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
@ -49,12 +55,15 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
|
||||
private readonly Cached gridCache = new Cached();
|
||||
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.nextHitObject = nextHitObject;
|
||||
|
||||
CentrePosition = centrePosition;
|
||||
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
}
|
||||
|
||||
@ -74,6 +83,16 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
private void updateSpacing()
|
||||
{
|
||||
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();
|
||||
}
|
||||
|
||||
|
@ -8,21 +8,21 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
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.Game.Graphics;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Compose.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// A component which outlines <see cref="DrawableHitObject"/>s and handles movement of selections.
|
||||
/// </summary>
|
||||
public class SelectionHandler : CompositeDrawable
|
||||
public class SelectionHandler : CompositeDrawable, IKeyBindingHandler<PlatformAction>
|
||||
{
|
||||
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.
|
||||
/// </summary>
|
||||
/// <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)
|
||||
return base.OnKeyDown(e);
|
||||
|
||||
switch (e.Key)
|
||||
switch (action.ActionMethod)
|
||||
{
|
||||
case Key.Delete:
|
||||
case PlatformActionMethod.Delete:
|
||||
foreach (var h in selectedBlueprints.ToList())
|
||||
placementHandler.Delete(h.DrawableObject.HitObject);
|
||||
return true;
|
||||
}
|
||||
|
||||
return base.OnKeyDown(e);
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool OnReleased(PlatformAction action) => action.ActionMethod == PlatformActionMethod.Delete;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Selection Handling
|
||||
|
@ -64,7 +64,10 @@ namespace osu.Game.Screens.Edit
|
||||
{
|
||||
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();
|
||||
clock = new EditorClock(Beatmap.Value, beatDivisor) { IsCoupled = false };
|
||||
clock.ChangeSource(sourceClock);
|
||||
|
50
osu.Game/Screens/Edit/Timing/ControlPointSettings.cs
Normal file
50
osu.Game/Screens/Edit/Timing/ControlPointSettings.cs
Normal 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(),
|
||||
};
|
||||
}
|
||||
}
|
247
osu.Game/Screens/Edit/Timing/ControlPointTable.cs
Normal file
247
osu.Game/Screens/Edit/Timing/ControlPointTable.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
48
osu.Game/Screens/Edit/Timing/DifficultySection.cs
Normal file
48
osu.Game/Screens/Edit/Timing/DifficultySection.cs
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
46
osu.Game/Screens/Edit/Timing/EffectSection.cs
Normal file
46
osu.Game/Screens/Edit/Timing/EffectSection.cs
Normal 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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
60
osu.Game/Screens/Edit/Timing/RowAttribute.cs
Normal file
60
osu.Game/Screens/Edit/Timing/RowAttribute.cs
Normal 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();
|
||||
}
|
||||
}
|
55
osu.Game/Screens/Edit/Timing/SampleSection.cs
Normal file
55
osu.Game/Screens/Edit/Timing/SampleSection.cs
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
130
osu.Game/Screens/Edit/Timing/Section.cs
Normal file
130
osu.Game/Screens/Edit/Timing/Section.cs
Normal 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();
|
||||
}
|
||||
}
|
@ -1,13 +1,151 @@
|
||||
// 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.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
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
85
osu.Game/Screens/Edit/Timing/TimingSection.cs
Normal file
85
osu.Game/Screens/Edit/Timing/TimingSection.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -81,7 +81,7 @@ namespace osu.Game.Screens.Multi.Components
|
||||
Text = new LocalisedString((beatmap.Metadata.TitleUnicode, beatmap.Metadata.Title)),
|
||||
Font = OsuFont.GetFont(size: TextSize),
|
||||
}
|
||||
}, null, LinkAction.OpenBeatmap, beatmap.OnlineBeatmapID.ToString(), "Open beatmap");
|
||||
}, LinkAction.OpenBeatmap, beatmap.OnlineBeatmapID.ToString(), "Open beatmap");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -167,14 +167,17 @@ namespace osu.Game.Screens.Multi
|
||||
public void APIStateChanged(IAPIProvider api, APIState state)
|
||||
{
|
||||
if (state != APIState.Online)
|
||||
forcefullyExit();
|
||||
Schedule(forcefullyExit);
|
||||
}
|
||||
|
||||
private void forcefullyExit()
|
||||
{
|
||||
// This is temporary since we don't currently have a way to force screens to be exited
|
||||
if (this.IsCurrentScreen())
|
||||
this.Exit();
|
||||
{
|
||||
while (this.IsCurrentScreen())
|
||||
this.Exit();
|
||||
}
|
||||
else
|
||||
{
|
||||
this.MakeCurrent();
|
||||
@ -212,6 +215,8 @@ namespace osu.Game.Screens.Multi
|
||||
|
||||
public override bool OnExiting(IScreen next)
|
||||
{
|
||||
roomManager.PartRoom();
|
||||
|
||||
if (screenStack.CurrentScreen != null && !(screenStack.CurrentScreen is LoungeSubScreen))
|
||||
{
|
||||
screenStack.Exit();
|
||||
|
@ -87,9 +87,8 @@ namespace osu.Game.Screens.Multi
|
||||
public void JoinRoom(Room room, Action<Room> onSuccess = null, Action<string> onError = null)
|
||||
{
|
||||
currentJoinRoomRequest?.Cancel();
|
||||
currentJoinRoomRequest = null;
|
||||
|
||||
currentJoinRoomRequest = new JoinRoomRequest(room, api.LocalUser.Value);
|
||||
|
||||
currentJoinRoomRequest.Success += () =>
|
||||
{
|
||||
joinedRoom = room;
|
||||
@ -98,7 +97,8 @@ namespace osu.Game.Screens.Multi
|
||||
|
||||
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());
|
||||
};
|
||||
|
||||
@ -107,6 +107,8 @@ namespace osu.Game.Screens.Multi
|
||||
|
||||
public void PartRoom()
|
||||
{
|
||||
currentJoinRoomRequest?.Cancel();
|
||||
|
||||
if (joinedRoom == null)
|
||||
return;
|
||||
|
||||
|
@ -8,6 +8,7 @@ using System.Threading.Tasks;
|
||||
using osu.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Audio.Track;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
@ -29,7 +30,7 @@ namespace osu.Game.Screens.Play
|
||||
/// <summary>
|
||||
/// The original source (usually a <see cref="WorkingBeatmap"/>'s track).
|
||||
/// </summary>
|
||||
private readonly IAdjustableClock sourceClock;
|
||||
private IAdjustableClock sourceClock;
|
||||
|
||||
public readonly BindableBool IsPaused = new BindableBool();
|
||||
|
||||
@ -153,10 +154,18 @@ namespace osu.Game.Screens.Play
|
||||
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.
|
||||
UserPlaybackRate.Value = 1;
|
||||
if (sourceClock != beatmap.Track)
|
||||
return;
|
||||
|
||||
removeSourceClockAdjustments();
|
||||
|
||||
sourceClock = new TrackVirtual(beatmap.Track.Length);
|
||||
adjustableClock.ChangeSource(sourceClock);
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
@ -185,6 +194,14 @@ namespace osu.Game.Screens.Play
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
removeSourceClockAdjustments();
|
||||
sourceClock = null;
|
||||
}
|
||||
|
||||
private void removeSourceClockAdjustments()
|
||||
{
|
||||
sourceClock.ResetSpeedAdjustments();
|
||||
(sourceClock as IAdjustableAudioComponent)?.RemoveAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust);
|
||||
}
|
||||
}
|
||||
|
@ -30,6 +30,7 @@ using osu.Game.Users;
|
||||
|
||||
namespace osu.Game.Screens.Play
|
||||
{
|
||||
[Cached]
|
||||
public class Player : ScreenWithBeatmapBackground
|
||||
{
|
||||
public override bool AllowBackButton => false; // handled by HoldForMenuButton
|
||||
@ -311,14 +312,19 @@ namespace osu.Game.Screens.Play
|
||||
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()
|
||||
{
|
||||
if (!this.IsCurrentScreen()) return;
|
||||
|
||||
sampleRestart?.Play();
|
||||
|
||||
RestartRequested?.Invoke();
|
||||
performImmediateExit();
|
||||
|
||||
if (this.IsCurrentScreen())
|
||||
performImmediateExit();
|
||||
else
|
||||
this.MakeCurrent();
|
||||
}
|
||||
|
||||
private ScheduledDelegate completionProgressDelegate;
|
||||
@ -443,7 +449,12 @@ namespace osu.Game.Screens.Play
|
||||
{
|
||||
if (!canPause) return;
|
||||
|
||||
IsResuming = false;
|
||||
if (IsResuming)
|
||||
{
|
||||
DrawableRuleset.CancelResume();
|
||||
IsResuming = false;
|
||||
}
|
||||
|
||||
GameplayClockContainer.Stop();
|
||||
PauseOverlay.Show();
|
||||
lastPauseActionTime = GameplayClockContainer.GameplayClock.CurrentTime;
|
||||
@ -525,7 +536,9 @@ namespace osu.Game.Screens.Play
|
||||
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();
|
||||
return base.OnExiting(next);
|
||||
|
@ -4,12 +4,13 @@
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
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.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>
|
||||
{
|
||||
@ -33,6 +34,7 @@ namespace osu.Game.Screens.Play
|
||||
public ReplayDownloadButton(ScoreInfo score)
|
||||
: base(score)
|
||||
{
|
||||
Size = new Vector2(50, 30);
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader(true)]
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user