mirror of
https://github.com/ppy/osu.git
synced 2024-11-11 15:47:26 +08:00
Merge branch 'master' into prev-button-revamp
This commit is contained in:
commit
70feab8316
8
.github/ISSUE_TEMPLATE/00-mobile-issues.md
vendored
Normal file
8
.github/ISSUE_TEMPLATE/00-mobile-issues.md
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
name: Mobile Report
|
||||||
|
about: ⚠ Due to current development priorities we are not accepting mobile reports at this time (unless you're willing to fix them yourself!)
|
||||||
|
---
|
||||||
|
|
||||||
|
⚠ **PLEASE READ** ⚠: Due to prioritising finishing the client for desktop first we are not accepting reports related to mobile platforms for the time being, unless you're willing to fix them.
|
||||||
|
If you'd like to report a problem or suggest a feature and then work on it, feel free to open an issue and highlight that you'd like to address it yourself in the issue body; mobile pull requests are also welcome.
|
||||||
|
Otherwise, please check back in the future when the focus of development shifts towards mobile!
|
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:**
|
|
@ -62,6 +62,6 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2019.1010.0" />
|
<PackageReference Include="ppy.osu.Game.Resources" Version="2019.1010.0" />
|
||||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2019.1023.0" />
|
<PackageReference Include="ppy.osu.Framework.Android" Version="2019.1106.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
|||||||
private void load()
|
private void load()
|
||||||
{
|
{
|
||||||
var controlPointInfo = new ControlPointInfo();
|
var controlPointInfo = new ControlPointInfo();
|
||||||
controlPointInfo.TimingPoints.Add(new TimingControlPoint());
|
controlPointInfo.Add(0, new TimingControlPoint());
|
||||||
|
|
||||||
WorkingBeatmap beatmap = CreateWorkingBeatmap(new Beatmap
|
WorkingBeatmap beatmap = CreateWorkingBeatmap(new Beatmap
|
||||||
{
|
{
|
||||||
|
@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Mania.Edit
|
|||||||
editorClock = clock;
|
editorClock = clock;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void HandleMovement(MoveSelectionEvent moveEvent)
|
public override bool HandleMovement(MoveSelectionEvent moveEvent)
|
||||||
{
|
{
|
||||||
var maniaBlueprint = (ManiaSelectionBlueprint)moveEvent.Blueprint;
|
var maniaBlueprint = (ManiaSelectionBlueprint)moveEvent.Blueprint;
|
||||||
int lastColumn = maniaBlueprint.DrawableObject.HitObject.Column;
|
int lastColumn = maniaBlueprint.DrawableObject.HitObject.Column;
|
||||||
@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Mania.Edit
|
|||||||
performDragMovement(moveEvent);
|
performDragMovement(moveEvent);
|
||||||
performColumnMovement(lastColumn, moveEvent);
|
performColumnMovement(lastColumn, moveEvent);
|
||||||
|
|
||||||
base.HandleMovement(moveEvent);
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -11,6 +11,7 @@ using osu.Framework.Graphics.Shapes;
|
|||||||
using osu.Framework.Input.Events;
|
using osu.Framework.Input.Events;
|
||||||
using osu.Framework.MathUtils;
|
using osu.Framework.MathUtils;
|
||||||
using osu.Game.Beatmaps.ControlPoints;
|
using osu.Game.Beatmaps.ControlPoints;
|
||||||
|
using osu.Game.Rulesets.Edit;
|
||||||
using osu.Game.Rulesets.Osu.Beatmaps;
|
using osu.Game.Rulesets.Osu.Beatmaps;
|
||||||
using osu.Game.Rulesets.Osu.Edit;
|
using osu.Game.Rulesets.Osu.Edit;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
@ -38,26 +39,33 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
[Cached]
|
[Cached]
|
||||||
private readonly BindableBeatDivisor beatDivisor = new BindableBeatDivisor();
|
private readonly BindableBeatDivisor beatDivisor = new BindableBeatDivisor();
|
||||||
|
|
||||||
|
[Cached(typeof(IDistanceSnapProvider))]
|
||||||
|
private readonly SnapProvider snapProvider = new SnapProvider();
|
||||||
|
|
||||||
private TestOsuDistanceSnapGrid grid;
|
private TestOsuDistanceSnapGrid grid;
|
||||||
|
|
||||||
public TestSceneOsuDistanceSnapGrid()
|
public TestSceneOsuDistanceSnapGrid()
|
||||||
{
|
{
|
||||||
editorBeatmap = new EditorBeatmap<OsuHitObject>(new OsuBeatmap());
|
editorBeatmap = new EditorBeatmap<OsuHitObject>(new OsuBeatmap());
|
||||||
|
|
||||||
createGrid();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[SetUp]
|
[SetUp]
|
||||||
public void Setup() => Schedule(() =>
|
public void Setup() => Schedule(() =>
|
||||||
{
|
{
|
||||||
Clear();
|
|
||||||
|
|
||||||
editorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = 1;
|
editorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = 1;
|
||||||
editorBeatmap.ControlPointInfo.DifficultyPoints.Clear();
|
editorBeatmap.ControlPointInfo.Clear();
|
||||||
editorBeatmap.ControlPointInfo.TimingPoints.Clear();
|
editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = beat_length });
|
||||||
editorBeatmap.ControlPointInfo.TimingPoints.Add(new TimingControlPoint { BeatLength = beat_length });
|
|
||||||
|
|
||||||
beatDivisor.Value = 1;
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
new Box
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Colour = Color4.SlateGray
|
||||||
|
},
|
||||||
|
grid = new TestOsuDistanceSnapGrid(new HitCircle { Position = grid_position }),
|
||||||
|
new SnappingCursorContainer { GetSnapPosition = v => grid.GetSnappedPosition(grid.ToLocalSpace(v)).position }
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
[TestCase(1)]
|
[TestCase(1)]
|
||||||
@ -71,53 +79,11 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
public void TestBeatDivisor(int divisor)
|
public void TestBeatDivisor(int divisor)
|
||||||
{
|
{
|
||||||
AddStep($"set beat divisor = {divisor}", () => beatDivisor.Value = divisor);
|
AddStep($"set beat divisor = {divisor}", () => beatDivisor.Value = divisor);
|
||||||
createGrid();
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestCase(100, 100)]
|
|
||||||
[TestCase(200, 100)]
|
|
||||||
public void TestBeatLength(float beatLength, float expectedSpacing)
|
|
||||||
{
|
|
||||||
AddStep($"set beat length = {beatLength}", () =>
|
|
||||||
{
|
|
||||||
editorBeatmap.ControlPointInfo.TimingPoints.Clear();
|
|
||||||
editorBeatmap.ControlPointInfo.TimingPoints.Add(new TimingControlPoint { BeatLength = beatLength });
|
|
||||||
});
|
|
||||||
|
|
||||||
createGrid();
|
|
||||||
AddAssert($"spacing = {expectedSpacing}", () => Precision.AlmostEquals(expectedSpacing, grid.DistanceSpacing));
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestCase(0.5f, 50)]
|
|
||||||
[TestCase(1, 100)]
|
|
||||||
[TestCase(1.5f, 150)]
|
|
||||||
public void TestSpeedMultiplier(float multiplier, float expectedSpacing)
|
|
||||||
{
|
|
||||||
AddStep($"set speed multiplier = {multiplier}", () =>
|
|
||||||
{
|
|
||||||
editorBeatmap.ControlPointInfo.DifficultyPoints.Clear();
|
|
||||||
editorBeatmap.ControlPointInfo.DifficultyPoints.Add(new DifficultyControlPoint { SpeedMultiplier = multiplier });
|
|
||||||
});
|
|
||||||
|
|
||||||
createGrid();
|
|
||||||
AddAssert($"spacing = {expectedSpacing}", () => Precision.AlmostEquals(expectedSpacing, grid.DistanceSpacing));
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestCase(0.5f, 50)]
|
|
||||||
[TestCase(1, 100)]
|
|
||||||
[TestCase(1.5f, 150)]
|
|
||||||
public void TestSliderMultiplier(float multiplier, float expectedSpacing)
|
|
||||||
{
|
|
||||||
AddStep($"set speed multiplier = {multiplier}", () => editorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = multiplier);
|
|
||||||
createGrid();
|
|
||||||
AddAssert($"spacing = {expectedSpacing}", () => Precision.AlmostEquals(expectedSpacing, grid.DistanceSpacing));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestCursorInCentre()
|
public void TestCursorInCentre()
|
||||||
{
|
{
|
||||||
createGrid();
|
|
||||||
|
|
||||||
AddStep("move mouse to centre", () => InputManager.MoveMouseTo(grid.ToScreenSpace(grid_position)));
|
AddStep("move mouse to centre", () => InputManager.MoveMouseTo(grid.ToScreenSpace(grid_position)));
|
||||||
assertSnappedDistance((float)beat_length);
|
assertSnappedDistance((float)beat_length);
|
||||||
}
|
}
|
||||||
@ -125,8 +91,6 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestCursorBeforeMovementPoint()
|
public void TestCursorBeforeMovementPoint()
|
||||||
{
|
{
|
||||||
createGrid();
|
|
||||||
|
|
||||||
AddStep("move mouse to just before movement point", () => InputManager.MoveMouseTo(grid.ToScreenSpace(grid_position + new Vector2((float)beat_length, 0) * 1.49f)));
|
AddStep("move mouse to just before movement point", () => InputManager.MoveMouseTo(grid.ToScreenSpace(grid_position + new Vector2((float)beat_length, 0) * 1.49f)));
|
||||||
assertSnappedDistance((float)beat_length);
|
assertSnappedDistance((float)beat_length);
|
||||||
}
|
}
|
||||||
@ -134,23 +98,14 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestCursorAfterMovementPoint()
|
public void TestCursorAfterMovementPoint()
|
||||||
{
|
{
|
||||||
createGrid();
|
|
||||||
|
|
||||||
AddStep("move mouse to just after movement point", () => InputManager.MoveMouseTo(grid.ToScreenSpace(grid_position + new Vector2((float)beat_length, 0) * 1.51f)));
|
AddStep("move mouse to just after movement point", () => InputManager.MoveMouseTo(grid.ToScreenSpace(grid_position + new Vector2((float)beat_length, 0) * 1.51f)));
|
||||||
assertSnappedDistance((float)beat_length * 2);
|
assertSnappedDistance((float)beat_length * 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void assertSnappedDistance(float expectedDistance) => AddAssert($"snap distance = {expectedDistance}", () =>
|
[Test]
|
||||||
|
public void TestLimitedDistance()
|
||||||
{
|
{
|
||||||
Vector2 snappedPosition = grid.GetSnapPosition(grid.ToLocalSpace(InputManager.CurrentState.Mouse.Position));
|
AddStep("create limited grid", () =>
|
||||||
float distance = Vector2.Distance(snappedPosition, grid_position);
|
|
||||||
|
|
||||||
return Precision.AlmostEquals(expectedDistance, distance);
|
|
||||||
});
|
|
||||||
|
|
||||||
private void createGrid()
|
|
||||||
{
|
|
||||||
AddStep("create grid", () =>
|
|
||||||
{
|
{
|
||||||
Children = new Drawable[]
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
@ -159,12 +114,22 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Colour = Color4.SlateGray
|
Colour = Color4.SlateGray
|
||||||
},
|
},
|
||||||
grid = new TestOsuDistanceSnapGrid(new HitCircle { Position = grid_position }),
|
grid = new TestOsuDistanceSnapGrid(new HitCircle { Position = grid_position }, new HitCircle { StartTime = 200 }),
|
||||||
new SnappingCursorContainer { GetSnapPosition = v => grid.GetSnapPosition(grid.ToLocalSpace(v)) }
|
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;
|
||||||
|
|
||||||
|
return Precision.AlmostEquals(expectedDistance, Vector2.Distance(snappedPosition, grid_position));
|
||||||
|
});
|
||||||
|
|
||||||
private class SnappingCursorContainer : CompositeDrawable
|
private class SnappingCursorContainer : CompositeDrawable
|
||||||
{
|
{
|
||||||
public Func<Vector2, Vector2> GetSnapPosition;
|
public Func<Vector2, Vector2> GetSnapPosition;
|
||||||
@ -208,10 +173,25 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
{
|
{
|
||||||
public new float DistanceSpacing => base.DistanceSpacing;
|
public new float DistanceSpacing => base.DistanceSpacing;
|
||||||
|
|
||||||
public TestOsuDistanceSnapGrid(OsuHitObject hitObject)
|
public TestOsuDistanceSnapGrid(OsuHitObject hitObject, OsuHitObject nextHitObject = null)
|
||||||
: base(hitObject)
|
: base(hitObject, nextHitObject)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class SnapProvider : IDistanceSnapProvider
|
||||||
|
{
|
||||||
|
public (Vector2 position, double time) GetSnappedPosition(Vector2 position, double time) => (position, time);
|
||||||
|
|
||||||
|
public float GetBeatSnapDistanceAt(double referenceTime) => (float)beat_length;
|
||||||
|
|
||||||
|
public float DurationToDistance(double referenceTime, double duration) => (float)duration;
|
||||||
|
|
||||||
|
public double DistanceToDuration(double referenceTime, float distance) => distance;
|
||||||
|
|
||||||
|
public double GetSnappedDurationFromDistance(double referenceTime, float distance) => 0;
|
||||||
|
|
||||||
|
public float GetSnappedDistanceFromDistance(double referenceTime, float distance) => 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -308,7 +308,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
private Drawable createDrawable(Slider slider, float circleSize, double speedMultiplier)
|
private Drawable createDrawable(Slider slider, float circleSize, double speedMultiplier)
|
||||||
{
|
{
|
||||||
var cpi = new ControlPointInfo();
|
var cpi = new ControlPointInfo();
|
||||||
cpi.DifficultyPoints.Add(new DifficultyControlPoint { SpeedMultiplier = speedMultiplier });
|
cpi.Add(0, new DifficultyControlPoint { SpeedMultiplier = speedMultiplier });
|
||||||
|
|
||||||
slider.ApplyDefaults(cpi, new BeatmapDifficulty { CircleSize = circleSize, SliderTickRate = 3 });
|
slider.ApplyDefaults(cpi, new BeatmapDifficulty { CircleSize = circleSize, SliderTickRate = 3 });
|
||||||
|
|
||||||
|
@ -313,10 +313,6 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
}, 25),
|
}, 25),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
ControlPointInfo =
|
|
||||||
{
|
|
||||||
DifficultyPoints = { new DifficultyControlPoint { SpeedMultiplier = 0.1f } }
|
|
||||||
},
|
|
||||||
BeatmapInfo =
|
BeatmapInfo =
|
||||||
{
|
{
|
||||||
BaseDifficulty = new BeatmapDifficulty { SliderTickRate = 3 },
|
BaseDifficulty = new BeatmapDifficulty { SliderTickRate = 3 },
|
||||||
@ -324,6 +320,8 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Beatmap.Value.Beatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 0.1f });
|
||||||
|
|
||||||
var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } });
|
var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } });
|
||||||
|
|
||||||
p.OnLoadComplete += _ =>
|
p.OnLoadComplete += _ =>
|
||||||
|
@ -16,6 +16,7 @@ using osu.Game.Rulesets.Osu.Objects;
|
|||||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||||
using osu.Game.Tests.Visual;
|
using osu.Game.Tests.Visual;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
using osuTK.Input;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Tests
|
namespace osu.Game.Rulesets.Osu.Tests
|
||||||
{
|
{
|
||||||
@ -85,6 +86,93 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
checkPositions();
|
checkPositions();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestSingleControlPointSelection()
|
||||||
|
{
|
||||||
|
moveMouseToControlPoint(0);
|
||||||
|
AddStep("click", () => InputManager.Click(MouseButton.Left));
|
||||||
|
checkControlPointSelected(0, true);
|
||||||
|
checkControlPointSelected(1, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestSingleControlPointDeselectionViaOtherControlPoint()
|
||||||
|
{
|
||||||
|
moveMouseToControlPoint(0);
|
||||||
|
AddStep("click", () => InputManager.Click(MouseButton.Left));
|
||||||
|
|
||||||
|
moveMouseToControlPoint(1);
|
||||||
|
AddStep("click", () => InputManager.Click(MouseButton.Left));
|
||||||
|
checkControlPointSelected(0, false);
|
||||||
|
checkControlPointSelected(1, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestSingleControlPointDeselectionViaClickOutside()
|
||||||
|
{
|
||||||
|
moveMouseToControlPoint(0);
|
||||||
|
AddStep("click", () => InputManager.Click(MouseButton.Left));
|
||||||
|
|
||||||
|
AddStep("move mouse outside control point", () => InputManager.MoveMouseTo(drawableObject));
|
||||||
|
AddStep("click", () => InputManager.Click(MouseButton.Left));
|
||||||
|
checkControlPointSelected(0, false);
|
||||||
|
checkControlPointSelected(1, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestMultipleControlPointSelection()
|
||||||
|
{
|
||||||
|
moveMouseToControlPoint(0);
|
||||||
|
AddStep("click", () => InputManager.Click(MouseButton.Left));
|
||||||
|
moveMouseToControlPoint(1);
|
||||||
|
AddStep("ctrl + click", () =>
|
||||||
|
{
|
||||||
|
InputManager.PressKey(Key.ControlLeft);
|
||||||
|
InputManager.Click(MouseButton.Left);
|
||||||
|
InputManager.ReleaseKey(Key.ControlLeft);
|
||||||
|
});
|
||||||
|
checkControlPointSelected(0, true);
|
||||||
|
checkControlPointSelected(1, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestMultipleControlPointDeselectionViaOtherControlPoint()
|
||||||
|
{
|
||||||
|
moveMouseToControlPoint(0);
|
||||||
|
AddStep("click", () => InputManager.Click(MouseButton.Left));
|
||||||
|
moveMouseToControlPoint(1);
|
||||||
|
AddStep("ctrl + click", () =>
|
||||||
|
{
|
||||||
|
InputManager.PressKey(Key.ControlLeft);
|
||||||
|
InputManager.Click(MouseButton.Left);
|
||||||
|
InputManager.ReleaseKey(Key.ControlLeft);
|
||||||
|
});
|
||||||
|
|
||||||
|
moveMouseToControlPoint(2);
|
||||||
|
AddStep("click", () => InputManager.Click(MouseButton.Left));
|
||||||
|
checkControlPointSelected(0, false);
|
||||||
|
checkControlPointSelected(1, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestMultipleControlPointDeselectionViaClickOutside()
|
||||||
|
{
|
||||||
|
moveMouseToControlPoint(0);
|
||||||
|
AddStep("click", () => InputManager.Click(MouseButton.Left));
|
||||||
|
moveMouseToControlPoint(1);
|
||||||
|
AddStep("ctrl + click", () =>
|
||||||
|
{
|
||||||
|
InputManager.PressKey(Key.ControlLeft);
|
||||||
|
InputManager.Click(MouseButton.Left);
|
||||||
|
InputManager.ReleaseKey(Key.ControlLeft);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("move mouse outside control point", () => InputManager.MoveMouseTo(drawableObject));
|
||||||
|
AddStep("click", () => InputManager.Click(MouseButton.Left));
|
||||||
|
checkControlPointSelected(0, false);
|
||||||
|
checkControlPointSelected(1, false);
|
||||||
|
}
|
||||||
|
|
||||||
private void moveHitObject()
|
private void moveHitObject()
|
||||||
{
|
{
|
||||||
AddStep("move hitobject", () =>
|
AddStep("move hitobject", () =>
|
||||||
@ -104,11 +192,24 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
() => Precision.AlmostEquals(blueprint.TailBlueprint.CirclePiece.ScreenSpaceDrawQuad.Centre, drawableObject.TailCircle.ScreenSpaceDrawQuad.Centre));
|
() => Precision.AlmostEquals(blueprint.TailBlueprint.CirclePiece.ScreenSpaceDrawQuad.Centre, drawableObject.TailCircle.ScreenSpaceDrawQuad.Centre));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void moveMouseToControlPoint(int index)
|
||||||
|
{
|
||||||
|
AddStep($"move mouse to control point {index}", () =>
|
||||||
|
{
|
||||||
|
Vector2 position = slider.Position + slider.Path.ControlPoints[index];
|
||||||
|
InputManager.MoveMouseTo(drawableObject.Parent.ToScreenSpace(position));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkControlPointSelected(int index, bool selected)
|
||||||
|
=> AddAssert($"control point {index} {(selected ? "selected" : "not selected")}", () => blueprint.ControlPointVisualiser.Pieces[index].IsSelected.Value == selected);
|
||||||
|
|
||||||
private class TestSliderBlueprint : SliderSelectionBlueprint
|
private class TestSliderBlueprint : SliderSelectionBlueprint
|
||||||
{
|
{
|
||||||
public new SliderBodyPiece BodyPiece => base.BodyPiece;
|
public new SliderBodyPiece BodyPiece => base.BodyPiece;
|
||||||
public new TestSliderCircleBlueprint HeadBlueprint => (TestSliderCircleBlueprint)base.HeadBlueprint;
|
public new TestSliderCircleBlueprint HeadBlueprint => (TestSliderCircleBlueprint)base.HeadBlueprint;
|
||||||
public new TestSliderCircleBlueprint TailBlueprint => (TestSliderCircleBlueprint)base.TailBlueprint;
|
public new TestSliderCircleBlueprint TailBlueprint => (TestSliderCircleBlueprint)base.TailBlueprint;
|
||||||
|
public new PathControlPointVisualiser ControlPointVisualiser => base.ControlPointVisualiser;
|
||||||
|
|
||||||
public TestSliderBlueprint(DrawableSlider slider)
|
public TestSliderBlueprint(DrawableSlider slider)
|
||||||
: base(slider)
|
: base(slider)
|
||||||
|
@ -1,26 +1,33 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Lines;
|
using osu.Framework.Graphics.Lines;
|
||||||
using osu.Framework.Graphics.Shapes;
|
using osu.Framework.Graphics.Shapes;
|
||||||
using osu.Framework.Input.Events;
|
using osu.Framework.Input.Events;
|
||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Rulesets.Objects;
|
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
using osuTK.Graphics;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||||
{
|
{
|
||||||
public class PathControlPointPiece : BlueprintPiece<Slider>
|
public class PathControlPointPiece : BlueprintPiece<Slider>
|
||||||
{
|
{
|
||||||
private readonly Slider slider;
|
public Action<int> RequestSelection;
|
||||||
private readonly int index;
|
public Action<Vector2[]> ControlPointsChanged;
|
||||||
|
|
||||||
|
public readonly BindableBool IsSelected = new BindableBool();
|
||||||
|
public readonly int Index;
|
||||||
|
|
||||||
|
private readonly Slider slider;
|
||||||
private readonly Path path;
|
private readonly Path path;
|
||||||
private readonly CircularContainer marker;
|
private readonly Container marker;
|
||||||
|
private readonly Drawable markerRing;
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private OsuColour colours { get; set; }
|
private OsuColour colours { get; set; }
|
||||||
@ -28,7 +35,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
|||||||
public PathControlPointPiece(Slider slider, int index)
|
public PathControlPointPiece(Slider slider, int index)
|
||||||
{
|
{
|
||||||
this.slider = slider;
|
this.slider = slider;
|
||||||
this.index = index;
|
Index = index;
|
||||||
|
|
||||||
Origin = Anchor.Centre;
|
Origin = Anchor.Centre;
|
||||||
AutoSizeAxes = Axes.Both;
|
AutoSizeAxes = Axes.Both;
|
||||||
@ -40,13 +47,36 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
|||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
PathRadius = 1
|
PathRadius = 1
|
||||||
},
|
},
|
||||||
marker = new CircularContainer
|
marker = new Container
|
||||||
{
|
{
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
Size = new Vector2(10),
|
AutoSizeAxes = Axes.Both,
|
||||||
Masking = true,
|
Children = new[]
|
||||||
Child = new Box { RelativeSizeAxes = Axes.Both }
|
{
|
||||||
|
new Circle
|
||||||
|
{
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
Size = new Vector2(10),
|
||||||
|
},
|
||||||
|
markerRing = new CircularContainer
|
||||||
|
{
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
Size = new Vector2(14),
|
||||||
|
Masking = true,
|
||||||
|
BorderThickness = 2,
|
||||||
|
BorderColour = Color4.White,
|
||||||
|
Alpha = 0,
|
||||||
|
Child = new Box
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Alpha = 0,
|
||||||
|
AlwaysPresent = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -55,30 +85,66 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
|||||||
{
|
{
|
||||||
base.Update();
|
base.Update();
|
||||||
|
|
||||||
Position = slider.StackedPosition + slider.Path.ControlPoints[index];
|
Position = slider.StackedPosition + slider.Path.ControlPoints[Index];
|
||||||
|
|
||||||
marker.Colour = isSegmentSeparator ? colours.Red : colours.Yellow;
|
updateMarkerDisplay();
|
||||||
|
updateConnectingPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates the state of the circular control point marker.
|
||||||
|
/// </summary>
|
||||||
|
private void updateMarkerDisplay()
|
||||||
|
{
|
||||||
|
markerRing.Alpha = IsSelected.Value ? 1 : 0;
|
||||||
|
|
||||||
|
Color4 colour = isSegmentSeparator ? colours.Red : colours.Yellow;
|
||||||
|
if (IsHovered || IsSelected.Value)
|
||||||
|
colour = Color4.White;
|
||||||
|
marker.Colour = colour;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates the path connecting this control point to the previous one.
|
||||||
|
/// </summary>
|
||||||
|
private void updateConnectingPath()
|
||||||
|
{
|
||||||
path.ClearVertices();
|
path.ClearVertices();
|
||||||
|
|
||||||
if (index != slider.Path.ControlPoints.Length - 1)
|
if (Index != slider.Path.ControlPoints.Length - 1)
|
||||||
{
|
{
|
||||||
path.AddVertex(Vector2.Zero);
|
path.AddVertex(Vector2.Zero);
|
||||||
path.AddVertex(slider.Path.ControlPoints[index + 1] - slider.Path.ControlPoints[index]);
|
path.AddVertex(slider.Path.ControlPoints[Index + 1] - slider.Path.ControlPoints[Index]);
|
||||||
}
|
}
|
||||||
|
|
||||||
path.OriginPosition = path.PositionInBoundingBox(Vector2.Zero);
|
path.OriginPosition = path.PositionInBoundingBox(Vector2.Zero);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The connecting path is excluded from positional input
|
||||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => marker.ReceivePositionalInputAt(screenSpacePos);
|
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => marker.ReceivePositionalInputAt(screenSpacePos);
|
||||||
|
|
||||||
|
protected override bool OnMouseDown(MouseDownEvent e)
|
||||||
|
{
|
||||||
|
if (RequestSelection != null)
|
||||||
|
{
|
||||||
|
RequestSelection.Invoke(Index);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool OnMouseUp(MouseUpEvent e) => RequestSelection != null;
|
||||||
|
|
||||||
|
protected override bool OnClick(ClickEvent e) => RequestSelection != null;
|
||||||
|
|
||||||
protected override bool OnDragStart(DragStartEvent e) => true;
|
protected override bool OnDragStart(DragStartEvent e) => true;
|
||||||
|
|
||||||
protected override bool OnDrag(DragEvent e)
|
protected override bool OnDrag(DragEvent e)
|
||||||
{
|
{
|
||||||
var newControlPoints = slider.Path.ControlPoints.ToArray();
|
var newControlPoints = slider.Path.ControlPoints.ToArray();
|
||||||
|
|
||||||
if (index == 0)
|
if (Index == 0)
|
||||||
{
|
{
|
||||||
// Special handling for the head - only the position of the slider changes
|
// Special handling for the head - only the position of the slider changes
|
||||||
slider.Position += e.Delta;
|
slider.Position += e.Delta;
|
||||||
@ -88,15 +154,15 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
|||||||
newControlPoints[i] -= e.Delta;
|
newControlPoints[i] -= e.Delta;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
newControlPoints[index] += e.Delta;
|
newControlPoints[Index] += e.Delta;
|
||||||
|
|
||||||
if (isSegmentSeparatorWithNext)
|
if (isSegmentSeparatorWithNext)
|
||||||
newControlPoints[index + 1] = newControlPoints[index];
|
newControlPoints[Index + 1] = newControlPoints[Index];
|
||||||
|
|
||||||
if (isSegmentSeparatorWithPrevious)
|
if (isSegmentSeparatorWithPrevious)
|
||||||
newControlPoints[index - 1] = newControlPoints[index];
|
newControlPoints[Index - 1] = newControlPoints[Index];
|
||||||
|
|
||||||
slider.Path = new SliderPath(slider.Path.Type, newControlPoints);
|
ControlPointsChanged?.Invoke(newControlPoints);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -105,8 +171,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
|||||||
|
|
||||||
private bool isSegmentSeparator => isSegmentSeparatorWithNext || isSegmentSeparatorWithPrevious;
|
private bool isSegmentSeparator => isSegmentSeparatorWithNext || isSegmentSeparatorWithPrevious;
|
||||||
|
|
||||||
private bool isSegmentSeparatorWithNext => index < slider.Path.ControlPoints.Length - 1 && slider.Path.ControlPoints[index + 1] == slider.Path.ControlPoints[index];
|
private bool isSegmentSeparatorWithNext => Index < slider.Path.ControlPoints.Length - 1 && slider.Path.ControlPoints[Index + 1] == slider.Path.ControlPoints[Index];
|
||||||
|
|
||||||
private bool isSegmentSeparatorWithPrevious => index > 0 && slider.Path.ControlPoints[index - 1] == slider.Path.ControlPoints[index];
|
private bool isSegmentSeparatorWithPrevious => Index > 0 && slider.Path.ControlPoints[Index - 1] == slider.Path.ControlPoints[Index];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,33 +1,133 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Input;
|
||||||
|
using osu.Framework.Input.Bindings;
|
||||||
|
using osu.Framework.Input.Events;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
|
using osu.Game.Screens.Edit.Compose;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||||
{
|
{
|
||||||
public class PathControlPointVisualiser : CompositeDrawable
|
public class PathControlPointVisualiser : CompositeDrawable, IKeyBindingHandler<PlatformAction>
|
||||||
{
|
{
|
||||||
|
public Action<Vector2[]> ControlPointsChanged;
|
||||||
|
|
||||||
|
internal readonly Container<PathControlPointPiece> Pieces;
|
||||||
private readonly Slider slider;
|
private readonly Slider slider;
|
||||||
|
private readonly bool allowSelection;
|
||||||
|
|
||||||
private readonly Container<PathControlPointPiece> pieces;
|
private InputManager inputManager;
|
||||||
|
|
||||||
public PathControlPointVisualiser(Slider slider)
|
[Resolved(CanBeNull = true)]
|
||||||
|
private IPlacementHandler placementHandler { get; set; }
|
||||||
|
|
||||||
|
public PathControlPointVisualiser(Slider slider, bool allowSelection)
|
||||||
{
|
{
|
||||||
this.slider = slider;
|
this.slider = slider;
|
||||||
|
this.allowSelection = allowSelection;
|
||||||
|
|
||||||
InternalChild = pieces = new Container<PathControlPointPiece> { RelativeSizeAxes = Axes.Both };
|
RelativeSizeAxes = Axes.Both;
|
||||||
|
|
||||||
|
InternalChild = Pieces = new Container<PathControlPointPiece> { RelativeSizeAxes = Axes.Both };
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
inputManager = GetContainingInputManager();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Update()
|
protected override void Update()
|
||||||
{
|
{
|
||||||
base.Update();
|
base.Update();
|
||||||
|
|
||||||
while (slider.Path.ControlPoints.Length > pieces.Count)
|
while (slider.Path.ControlPoints.Length > Pieces.Count)
|
||||||
pieces.Add(new PathControlPointPiece(slider, pieces.Count));
|
{
|
||||||
while (slider.Path.ControlPoints.Length < pieces.Count)
|
var piece = new PathControlPointPiece(slider, Pieces.Count)
|
||||||
pieces.Remove(pieces[pieces.Count - 1]);
|
{
|
||||||
|
ControlPointsChanged = c => ControlPointsChanged?.Invoke(c),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (allowSelection)
|
||||||
|
piece.RequestSelection = selectPiece;
|
||||||
|
|
||||||
|
Pieces.Add(piece);
|
||||||
|
}
|
||||||
|
|
||||||
|
while (slider.Path.ControlPoints.Length < Pieces.Count)
|
||||||
|
Pieces.Remove(Pieces[Pieces.Count - 1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override bool OnClick(ClickEvent e)
|
||||||
|
{
|
||||||
|
foreach (var piece in Pieces)
|
||||||
|
piece.IsSelected.Value = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void selectPiece(int index)
|
||||||
|
{
|
||||||
|
if (inputManager.CurrentState.Keyboard.ControlPressed)
|
||||||
|
Pieces[index].IsSelected.Toggle();
|
||||||
|
else
|
||||||
|
{
|
||||||
|
foreach (var piece in Pieces)
|
||||||
|
piece.IsSelected.Value = piece.Index == index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool OnPressed(PlatformAction action)
|
||||||
|
{
|
||||||
|
switch (action.ActionMethod)
|
||||||
|
{
|
||||||
|
case PlatformActionMethod.Delete:
|
||||||
|
var newControlPoints = new List<Vector2>();
|
||||||
|
|
||||||
|
foreach (var piece in Pieces)
|
||||||
|
{
|
||||||
|
if (!piece.IsSelected.Value)
|
||||||
|
newControlPoints.Add(slider.Path.ControlPoints[piece.Index]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure that there are any points to be deleted
|
||||||
|
if (newControlPoints.Count == slider.Path.ControlPoints.Length)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// If there are 0 remaining control points, treat the slider as being deleted
|
||||||
|
if (newControlPoints.Count == 0)
|
||||||
|
{
|
||||||
|
placementHandler?.Delete(slider);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make control points relative
|
||||||
|
Vector2 first = newControlPoints[0];
|
||||||
|
for (int i = 0; i < newControlPoints.Count; i++)
|
||||||
|
newControlPoints[i] = newControlPoints[i] - first;
|
||||||
|
|
||||||
|
// The slider's position defines the position of the first control point, and all further control points are relative to that point
|
||||||
|
slider.Position = slider.Position + first;
|
||||||
|
|
||||||
|
// Since pieces are re-used, they will not point to the deleted control points while remaining selected
|
||||||
|
foreach (var piece in Pieces)
|
||||||
|
piece.IsSelected.Value = false;
|
||||||
|
|
||||||
|
ControlPointsChanged?.Invoke(newControlPoints.ToArray());
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool OnReleased(PlatformAction action) => action.ActionMethod == PlatformActionMethod.Delete;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -43,5 +43,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
|||||||
Size = body.Size;
|
Size = body.Size;
|
||||||
OriginPosition = body.PathOffset;
|
OriginPosition = body.PathOffset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => body.ReceivePositionalInputAt(screenSpacePos);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -33,6 +33,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
|||||||
|
|
||||||
private PlacementState state;
|
private PlacementState state;
|
||||||
|
|
||||||
|
[Resolved(CanBeNull = true)]
|
||||||
|
private HitObjectComposer composer { get; set; }
|
||||||
|
|
||||||
public SliderPlacementBlueprint()
|
public SliderPlacementBlueprint()
|
||||||
: base(new Objects.Slider())
|
: base(new Objects.Slider())
|
||||||
{
|
{
|
||||||
@ -48,7 +51,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
|||||||
bodyPiece = new SliderBodyPiece(),
|
bodyPiece = new SliderBodyPiece(),
|
||||||
headCirclePiece = new HitCirclePiece(),
|
headCirclePiece = new HitCirclePiece(),
|
||||||
tailCirclePiece = new HitCirclePiece(),
|
tailCirclePiece = new HitCirclePiece(),
|
||||||
new PathControlPointVisualiser(HitObject),
|
new PathControlPointVisualiser(HitObject, false) { ControlPointsChanged = _ => updateSlider() },
|
||||||
};
|
};
|
||||||
|
|
||||||
setState(PlacementState.Initial);
|
setState(PlacementState.Initial);
|
||||||
@ -131,8 +134,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
|||||||
|
|
||||||
private void updateSlider()
|
private void updateSlider()
|
||||||
{
|
{
|
||||||
var newControlPoints = segments.SelectMany(s => s.ControlPoints).Concat(cursor.Yield()).ToArray();
|
Vector2[] newControlPoints = segments.SelectMany(s => s.ControlPoints).Concat(cursor.Yield()).ToArray();
|
||||||
HitObject.Path = new SliderPath(newControlPoints.Length > 2 ? PathType.Bezier : PathType.Linear, newControlPoints);
|
|
||||||
|
var unsnappedPath = new SliderPath(newControlPoints.Length > 2 ? PathType.Bezier : PathType.Linear, newControlPoints);
|
||||||
|
var snappedDistance = composer?.GetSnappedDistanceFromDistance(HitObject.StartTime, (float)unsnappedPath.Distance) ?? (float)unsnappedPath.Distance;
|
||||||
|
|
||||||
|
HitObject.Path = new SliderPath(unsnappedPath.Type, newControlPoints, snappedDistance);
|
||||||
|
|
||||||
bodyPiece.UpdateFrom(HitObject);
|
bodyPiece.UpdateFrom(HitObject);
|
||||||
headCirclePiece.UpdateFrom(HitObject.HeadCircle);
|
headCirclePiece.UpdateFrom(HitObject.HeadCircle);
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Game.Rulesets.Edit;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
|
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||||
@ -14,6 +18,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
|||||||
protected readonly SliderBodyPiece BodyPiece;
|
protected readonly SliderBodyPiece BodyPiece;
|
||||||
protected readonly SliderCircleSelectionBlueprint HeadBlueprint;
|
protected readonly SliderCircleSelectionBlueprint HeadBlueprint;
|
||||||
protected readonly SliderCircleSelectionBlueprint TailBlueprint;
|
protected readonly SliderCircleSelectionBlueprint TailBlueprint;
|
||||||
|
protected readonly PathControlPointVisualiser ControlPointVisualiser;
|
||||||
|
|
||||||
|
[Resolved(CanBeNull = true)]
|
||||||
|
private HitObjectComposer composer { get; set; }
|
||||||
|
|
||||||
public SliderSelectionBlueprint(DrawableSlider slider)
|
public SliderSelectionBlueprint(DrawableSlider slider)
|
||||||
: base(slider)
|
: base(slider)
|
||||||
@ -25,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
|||||||
BodyPiece = new SliderBodyPiece(),
|
BodyPiece = new SliderBodyPiece(),
|
||||||
HeadBlueprint = CreateCircleSelectionBlueprint(slider, SliderPosition.Start),
|
HeadBlueprint = CreateCircleSelectionBlueprint(slider, SliderPosition.Start),
|
||||||
TailBlueprint = CreateCircleSelectionBlueprint(slider, SliderPosition.End),
|
TailBlueprint = CreateCircleSelectionBlueprint(slider, SliderPosition.End),
|
||||||
new PathControlPointVisualiser(sliderObject),
|
ControlPointVisualiser = new PathControlPointVisualiser(sliderObject, true) { ControlPointsChanged = onNewControlPoints },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -36,8 +44,20 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
|||||||
BodyPiece.UpdateFrom(HitObject);
|
BodyPiece.UpdateFrom(HitObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void onNewControlPoints(Vector2[] controlPoints)
|
||||||
|
{
|
||||||
|
var unsnappedPath = new SliderPath(controlPoints.Length > 2 ? PathType.Bezier : PathType.Linear, controlPoints);
|
||||||
|
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;
|
public override Vector2 SelectionPoint => HeadBlueprint.SelectionPoint;
|
||||||
|
|
||||||
|
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => BodyPiece.ReceivePositionalInputAt(screenSpacePos);
|
||||||
|
|
||||||
protected virtual SliderCircleSelectionBlueprint CreateCircleSelectionBlueprint(DrawableSlider slider, SliderPosition position) => new SliderCircleSelectionBlueprint(slider, position);
|
protected virtual SliderCircleSelectionBlueprint CreateCircleSelectionBlueprint(DrawableSlider slider, SliderPosition position) => new SliderCircleSelectionBlueprint(slider, position);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using osu.Game.Beatmaps;
|
|
||||||
using osu.Game.Beatmaps.ControlPoints;
|
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using osu.Game.Screens.Edit.Compose.Components;
|
using osu.Game.Screens.Edit.Compose.Components;
|
||||||
|
|
||||||
@ -10,20 +8,10 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
{
|
{
|
||||||
public class OsuDistanceSnapGrid : CircularDistanceSnapGrid
|
public class OsuDistanceSnapGrid : CircularDistanceSnapGrid
|
||||||
{
|
{
|
||||||
public OsuDistanceSnapGrid(OsuHitObject hitObject)
|
public OsuDistanceSnapGrid(OsuHitObject hitObject, OsuHitObject nextHitObject)
|
||||||
: base(hitObject, hitObject.StackedEndPosition)
|
: base(hitObject, nextHitObject, hitObject.StackedEndPosition)
|
||||||
{
|
{
|
||||||
Masking = true;
|
Masking = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override float GetVelocity(double time, ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
|
|
||||||
{
|
|
||||||
TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(time);
|
|
||||||
DifficultyControlPoint difficultyPoint = controlPointInfo.DifficultyPointAt(time);
|
|
||||||
|
|
||||||
double scoringDistance = OsuHitObject.BASE_SCORING_DISTANCE * difficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier;
|
|
||||||
|
|
||||||
return (float)(scoringDistance / timingPoint.BeatLength);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
@ -60,25 +61,40 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
var objects = selectedHitObjects.ToList();
|
var objects = selectedHitObjects.ToList();
|
||||||
|
|
||||||
if (objects.Count == 0)
|
if (objects.Count == 0)
|
||||||
|
return createGrid(h => h.StartTime <= EditorClock.CurrentTime);
|
||||||
|
|
||||||
|
double minTime = objects.Min(h => h.StartTime);
|
||||||
|
return createGrid(h => h.StartTime < minTime, objects.Count + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a grid from the last <see cref="HitObject"/> matching a predicate to a target <see cref="HitObject"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="sourceSelector">A predicate that matches <see cref="HitObject"/>s where the grid can start from.
|
||||||
|
/// Only the last <see cref="HitObject"/> matching the predicate is used.</param>
|
||||||
|
/// <param name="targetOffset">An offset from the <see cref="HitObject"/> selected via <paramref name="sourceSelector"/> at which the grid should stop.</param>
|
||||||
|
/// <returns>The <see cref="OsuDistanceSnapGrid"/> from a selected <see cref="HitObject"/> to a target <see cref="HitObject"/>.</returns>
|
||||||
|
private OsuDistanceSnapGrid createGrid(Func<HitObject, bool> sourceSelector, int targetOffset = 1)
|
||||||
|
{
|
||||||
|
if (targetOffset < 1) throw new ArgumentOutOfRangeException(nameof(targetOffset));
|
||||||
|
|
||||||
|
int sourceIndex = -1;
|
||||||
|
|
||||||
|
for (int i = 0; i < EditorBeatmap.HitObjects.Count; i++)
|
||||||
{
|
{
|
||||||
var lastObject = EditorBeatmap.HitObjects.LastOrDefault(h => h.StartTime <= EditorClock.CurrentTime);
|
if (!sourceSelector(EditorBeatmap.HitObjects[i]))
|
||||||
|
break;
|
||||||
|
|
||||||
if (lastObject == null)
|
sourceIndex = i;
|
||||||
return null;
|
|
||||||
|
|
||||||
return new OsuDistanceSnapGrid(lastObject);
|
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
double minTime = objects.Min(h => h.StartTime);
|
|
||||||
|
|
||||||
var lastObject = EditorBeatmap.HitObjects.LastOrDefault(h => h.StartTime < minTime);
|
if (sourceIndex == -1)
|
||||||
|
return null;
|
||||||
|
|
||||||
if (lastObject == null)
|
OsuHitObject sourceObject = EditorBeatmap.HitObjects[sourceIndex];
|
||||||
return null;
|
OsuHitObject targetObject = sourceIndex + targetOffset < EditorBeatmap.HitObjects.Count ? EditorBeatmap.HitObjects[sourceIndex + targetOffset] : null;
|
||||||
|
|
||||||
return new OsuDistanceSnapGrid(lastObject);
|
return new OsuDistanceSnapGrid(sourceObject, targetObject);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,13 +4,34 @@
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using osu.Game.Screens.Edit.Compose.Components;
|
using osu.Game.Screens.Edit.Compose.Components;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Edit
|
namespace osu.Game.Rulesets.Osu.Edit
|
||||||
{
|
{
|
||||||
public class OsuSelectionHandler : SelectionHandler
|
public class OsuSelectionHandler : SelectionHandler
|
||||||
{
|
{
|
||||||
public override void HandleMovement(MoveSelectionEvent moveEvent)
|
public override bool HandleMovement(MoveSelectionEvent moveEvent)
|
||||||
{
|
{
|
||||||
|
Vector2 minPosition = new Vector2(float.MaxValue, float.MaxValue);
|
||||||
|
Vector2 maxPosition = new Vector2(float.MinValue, float.MinValue);
|
||||||
|
|
||||||
|
// Go through all hitobjects to make sure they would remain in the bounds of the editor after movement, before any movement is attempted
|
||||||
|
foreach (var h in SelectedHitObjects.OfType<OsuHitObject>())
|
||||||
|
{
|
||||||
|
if (h is Spinner)
|
||||||
|
{
|
||||||
|
// Spinners don't support position adjustments
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stacking is not considered
|
||||||
|
minPosition = Vector2.ComponentMin(minPosition, Vector2.ComponentMin(h.EndPosition + moveEvent.InstantDelta, h.Position + moveEvent.InstantDelta));
|
||||||
|
maxPosition = Vector2.ComponentMax(maxPosition, Vector2.ComponentMax(h.EndPosition + moveEvent.InstantDelta, h.Position + moveEvent.InstantDelta));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (minPosition.X < 0 || minPosition.Y < 0 || maxPosition.X > DrawWidth || maxPosition.Y > DrawHeight)
|
||||||
|
return false;
|
||||||
|
|
||||||
foreach (var h in SelectedHitObjects.OfType<OsuHitObject>())
|
foreach (var h in SelectedHitObjects.OfType<OsuHitObject>())
|
||||||
{
|
{
|
||||||
if (h is Spinner)
|
if (h is Spinner)
|
||||||
@ -22,7 +43,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
h.Position += moveEvent.InstantDelta;
|
h.Position += moveEvent.InstantDelta;
|
||||||
}
|
}
|
||||||
|
|
||||||
base.HandleMovement(moveEvent);
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A single follow point positioned between two adjacent <see cref="DrawableOsuHitObject"/>s.
|
||||||
|
/// </summary>
|
||||||
public class FollowPoint : Container
|
public class FollowPoint : Container
|
||||||
{
|
{
|
||||||
private const float width = 8;
|
private const float width = 8;
|
||||||
|
@ -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.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using osuTK;
|
using System.Linq;
|
||||||
|
using osu.Framework.Extensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Game.Rulesets.Objects.Types;
|
using osu.Framework.Graphics.Containers;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
|
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
|
||||||
{
|
{
|
||||||
public class FollowPointRenderer : ConnectionRenderer<OsuHitObject>
|
/// <summary>
|
||||||
|
/// Visualises connections between <see cref="DrawableOsuHitObject"/>s.
|
||||||
|
/// </summary>
|
||||||
|
public class FollowPointRenderer : CompositeDrawable
|
||||||
{
|
{
|
||||||
private int pointDistance = 32;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Determines how much space there is between points.
|
/// All the <see cref="FollowPointConnection"/>s contained by this <see cref="FollowPointRenderer"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int PointDistance
|
internal IReadOnlyList<FollowPointConnection> Connections => connections;
|
||||||
{
|
|
||||||
get => pointDistance;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (pointDistance == value) return;
|
|
||||||
|
|
||||||
pointDistance = value;
|
private readonly List<FollowPointConnection> connections = new List<FollowPointConnection>();
|
||||||
update();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private int preEmpt = 800;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Follow points to the next hitobject start appearing for this many milliseconds before an hitobject's end time.
|
|
||||||
/// </summary>
|
|
||||||
public int PreEmpt
|
|
||||||
{
|
|
||||||
get => preEmpt;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (preEmpt == value) return;
|
|
||||||
|
|
||||||
preEmpt = value;
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private IEnumerable<OsuHitObject> hitObjects;
|
|
||||||
|
|
||||||
public override IEnumerable<OsuHitObject> HitObjects
|
|
||||||
{
|
|
||||||
get => hitObjects;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
hitObjects = value;
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override bool RemoveCompletedTransforms => false;
|
public override bool RemoveCompletedTransforms => false;
|
||||||
|
|
||||||
private void update()
|
/// <summary>
|
||||||
|
/// Adds the <see cref="FollowPoint"/>s around a <see cref="DrawableOsuHitObject"/>.
|
||||||
|
/// This includes <see cref="FollowPoint"/>s leading into <paramref name="hitObject"/>, and <see cref="FollowPoint"/>s exiting <paramref name="hitObject"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="hitObject">The <see cref="DrawableOsuHitObject"/> to add <see cref="FollowPoint"/>s for.</param>
|
||||||
|
public void AddFollowPoints(DrawableOsuHitObject hitObject)
|
||||||
|
=> addConnection(new FollowPointConnection(hitObject).With(g => g.StartTime.BindValueChanged(_ => onStartTimeChanged(g))));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes the <see cref="FollowPoint"/>s around a <see cref="DrawableOsuHitObject"/>.
|
||||||
|
/// This includes <see cref="FollowPoint"/>s leading into <paramref name="hitObject"/>, and <see cref="FollowPoint"/>s exiting <paramref name="hitObject"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="hitObject">The <see cref="DrawableOsuHitObject"/> to remove <see cref="FollowPoint"/>s for.</param>
|
||||||
|
public void RemoveFollowPoints(DrawableOsuHitObject hitObject) => removeGroup(connections.Single(g => g.Start == hitObject));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a <see cref="FollowPointConnection"/> to this <see cref="FollowPointRenderer"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="connection">The <see cref="FollowPointConnection"/> to add.</param>
|
||||||
|
/// <returns>The index of <paramref name="connection"/> in <see cref="connections"/>.</returns>
|
||||||
|
private void addConnection(FollowPointConnection connection)
|
||||||
{
|
{
|
||||||
ClearInternal();
|
AddInternal(connection);
|
||||||
|
|
||||||
if (hitObjects == null)
|
// Groups are sorted by their start time when added such that the index can be used to post-process other surrounding connections
|
||||||
return;
|
int index = connections.AddInPlace(connection, Comparer<FollowPointConnection>.Create((g1, g2) => g1.StartTime.Value.CompareTo(g2.StartTime.Value)));
|
||||||
|
|
||||||
OsuHitObject prevHitObject = null;
|
if (index < connections.Count - 1)
|
||||||
|
|
||||||
foreach (var currHitObject in hitObjects)
|
|
||||||
{
|
{
|
||||||
if (prevHitObject != null && !currHitObject.NewCombo && !(prevHitObject is Spinner) && !(currHitObject is Spinner))
|
// Update the connection's end point to the next connection's start point
|
||||||
{
|
// h1 -> -> -> h2
|
||||||
Vector2 startPosition = prevHitObject.EndPosition;
|
// connection nextGroup
|
||||||
Vector2 endPosition = currHitObject.Position;
|
|
||||||
double startTime = (prevHitObject as IHasEndTime)?.EndTime ?? prevHitObject.StartTime;
|
|
||||||
double endTime = currHitObject.StartTime;
|
|
||||||
|
|
||||||
Vector2 distanceVector = endPosition - startPosition;
|
FollowPointConnection nextConnection = connections[index + 1];
|
||||||
int distance = (int)distanceVector.Length;
|
connection.End = nextConnection.Start;
|
||||||
float rotation = (float)(Math.Atan2(distanceVector.Y, distanceVector.X) * (180 / Math.PI));
|
|
||||||
double duration = endTime - startTime;
|
|
||||||
|
|
||||||
for (int d = (int)(PointDistance * 1.5); d < distance - PointDistance; d += PointDistance)
|
|
||||||
{
|
|
||||||
float fraction = (float)d / distance;
|
|
||||||
Vector2 pointStartPosition = startPosition + (fraction - 0.1f) * distanceVector;
|
|
||||||
Vector2 pointEndPosition = startPosition + fraction * distanceVector;
|
|
||||||
double fadeOutTime = startTime + fraction * duration;
|
|
||||||
double fadeInTime = fadeOutTime - PreEmpt;
|
|
||||||
|
|
||||||
FollowPoint fp;
|
|
||||||
|
|
||||||
AddInternal(fp = new FollowPoint
|
|
||||||
{
|
|
||||||
Position = pointStartPosition,
|
|
||||||
Rotation = rotation,
|
|
||||||
Alpha = 0,
|
|
||||||
Scale = new Vector2(1.5f * currHitObject.Scale),
|
|
||||||
});
|
|
||||||
|
|
||||||
using (fp.BeginAbsoluteSequence(fadeInTime))
|
|
||||||
{
|
|
||||||
fp.FadeIn(currHitObject.TimeFadeIn);
|
|
||||||
fp.ScaleTo(currHitObject.Scale, currHitObject.TimeFadeIn, Easing.Out);
|
|
||||||
|
|
||||||
fp.MoveTo(pointEndPosition, currHitObject.TimeFadeIn, Easing.Out);
|
|
||||||
|
|
||||||
fp.Delay(fadeOutTime - fadeInTime).FadeOut(currHitObject.TimeFadeIn);
|
|
||||||
}
|
|
||||||
|
|
||||||
fp.Expire(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
prevHitObject = currHitObject;
|
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// The end point may be non-null during re-ordering
|
||||||
|
connection.End = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index > 0)
|
||||||
|
{
|
||||||
|
// Update the previous connection's end point to the current connection's start point
|
||||||
|
// h1 -> -> -> h2
|
||||||
|
// prevGroup connection
|
||||||
|
|
||||||
|
FollowPointConnection previousConnection = connections[index - 1];
|
||||||
|
previousConnection.End = connection.Start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes a <see cref="FollowPointConnection"/> from this <see cref="FollowPointRenderer"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="connection">The <see cref="FollowPointConnection"/> to remove.</param>
|
||||||
|
/// <returns>Whether <paramref name="connection"/> was removed.</returns>
|
||||||
|
private void removeGroup(FollowPointConnection connection)
|
||||||
|
{
|
||||||
|
RemoveInternal(connection);
|
||||||
|
|
||||||
|
int index = connections.IndexOf(connection);
|
||||||
|
|
||||||
|
if (index > 0)
|
||||||
|
{
|
||||||
|
// Update the previous connection's end point to the next connection's start point
|
||||||
|
// h1 -> -> -> h2 -> -> -> h3
|
||||||
|
// prevGroup connection nextGroup
|
||||||
|
// The current connection's end point is used since there may not be a next connection
|
||||||
|
FollowPointConnection previousConnection = connections[index - 1];
|
||||||
|
previousConnection.End = connection.End;
|
||||||
|
}
|
||||||
|
|
||||||
|
connections.Remove(connection);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onStartTimeChanged(FollowPointConnection connection)
|
||||||
|
{
|
||||||
|
// Naive but can be improved if performance becomes an issue
|
||||||
|
removeGroup(connection);
|
||||||
|
addConnection(connection);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,6 @@ using osu.Framework.Graphics.Containers;
|
|||||||
using osu.Framework.Graphics.Effects;
|
using osu.Framework.Graphics.Effects;
|
||||||
using osu.Game.Graphics.Sprites;
|
using osu.Game.Graphics.Sprites;
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
using osu.Framework.Graphics.Shapes;
|
|
||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
|
|
||||||
@ -30,17 +29,15 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
|
|||||||
|
|
||||||
Children = new Drawable[]
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
new CircularContainer
|
new Container
|
||||||
{
|
{
|
||||||
Masking = true,
|
Masking = true,
|
||||||
Origin = Anchor.Centre,
|
|
||||||
EdgeEffect = new EdgeEffectParameters
|
EdgeEffect = new EdgeEffectParameters
|
||||||
{
|
{
|
||||||
Type = EdgeEffectType.Glow,
|
Type = EdgeEffectType.Glow,
|
||||||
Radius = 60,
|
Radius = 60,
|
||||||
Colour = Color4.White.Opacity(0.5f),
|
Colour = Color4.White.Opacity(0.5f),
|
||||||
},
|
},
|
||||||
Child = new Box()
|
|
||||||
},
|
},
|
||||||
number = new SkinnableSpriteText(new OsuSkinComponent(OsuSkinComponents.HitCircleText), _ => new OsuSpriteText
|
number = new SkinnableSpriteText(new OsuSkinComponent(OsuSkinComponents.HitCircleText), _ => new OsuSpriteText
|
||||||
{
|
{
|
||||||
|
@ -37,6 +37,8 @@ namespace osu.Game.Rulesets.Osu.Objects
|
|||||||
{
|
{
|
||||||
PathBindable.Value = value;
|
PathBindable.Value = value;
|
||||||
endPositionCache.Invalidate();
|
endPositionCache.Invalidate();
|
||||||
|
|
||||||
|
updateNestedPositions();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -48,14 +50,9 @@ namespace osu.Game.Rulesets.Osu.Objects
|
|||||||
set
|
set
|
||||||
{
|
{
|
||||||
base.Position = value;
|
base.Position = value;
|
||||||
|
|
||||||
endPositionCache.Invalidate();
|
endPositionCache.Invalidate();
|
||||||
|
|
||||||
if (HeadCircle != null)
|
updateNestedPositions();
|
||||||
HeadCircle.Position = value;
|
|
||||||
|
|
||||||
if (TailCircle != null)
|
|
||||||
TailCircle.Position = EndPosition;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -197,6 +194,15 @@ namespace osu.Game.Rulesets.Osu.Objects
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void updateNestedPositions()
|
||||||
|
{
|
||||||
|
if (HeadCircle != null)
|
||||||
|
HeadCircle.Position = Position;
|
||||||
|
|
||||||
|
if (TailCircle != null)
|
||||||
|
TailCircle.Position = EndPosition;
|
||||||
|
}
|
||||||
|
|
||||||
private List<HitSampleInfo> getNodeSamples(int nodeIndex) =>
|
private List<HitSampleInfo> getNodeSamples(int nodeIndex) =>
|
||||||
nodeIndex < NodeSamples.Count ? NodeSamples[nodeIndex] : Samples;
|
nodeIndex < NodeSamples.Count ? NodeSamples[nodeIndex] : Samples;
|
||||||
|
|
||||||
|
@ -9,7 +9,6 @@ using osu.Game.Rulesets.Osu.Objects;
|
|||||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||||
using osu.Game.Rulesets.Osu.Objects.Drawables.Connections;
|
using osu.Game.Rulesets.Osu.Objects.Drawables.Connections;
|
||||||
using osu.Game.Rulesets.UI;
|
using osu.Game.Rulesets.UI;
|
||||||
using System.Linq;
|
|
||||||
using osu.Game.Rulesets.Judgements;
|
using osu.Game.Rulesets.Judgements;
|
||||||
using osu.Game.Rulesets.Osu.UI.Cursor;
|
using osu.Game.Rulesets.Osu.UI.Cursor;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
@ -20,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.UI
|
|||||||
{
|
{
|
||||||
private readonly ApproachCircleProxyContainer approachCircles;
|
private readonly ApproachCircleProxyContainer approachCircles;
|
||||||
private readonly JudgementContainer<DrawableOsuJudgement> judgementLayer;
|
private readonly JudgementContainer<DrawableOsuJudgement> judgementLayer;
|
||||||
private readonly ConnectionRenderer<OsuHitObject> connectionLayer;
|
private readonly FollowPointRenderer followPoints;
|
||||||
|
|
||||||
public static readonly Vector2 BASE_SIZE = new Vector2(512, 384);
|
public static readonly Vector2 BASE_SIZE = new Vector2(512, 384);
|
||||||
|
|
||||||
@ -30,7 +29,7 @@ namespace osu.Game.Rulesets.Osu.UI
|
|||||||
{
|
{
|
||||||
InternalChildren = new Drawable[]
|
InternalChildren = new Drawable[]
|
||||||
{
|
{
|
||||||
connectionLayer = new FollowPointRenderer
|
followPoints = new FollowPointRenderer
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Depth = 2,
|
Depth = 2,
|
||||||
@ -64,11 +63,18 @@ namespace osu.Game.Rulesets.Osu.UI
|
|||||||
};
|
};
|
||||||
|
|
||||||
base.Add(h);
|
base.Add(h);
|
||||||
|
|
||||||
|
followPoints.AddFollowPoints((DrawableOsuHitObject)h);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void PostProcess()
|
public override bool Remove(DrawableHitObject h)
|
||||||
{
|
{
|
||||||
connectionLayer.HitObjects = HitObjectContainer.Objects.Select(d => d.HitObject).OfType<OsuHitObject>();
|
bool result = base.Remove(h);
|
||||||
|
|
||||||
|
if (result)
|
||||||
|
followPoints.RemoveFollowPoints((DrawableOsuHitObject)h);
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onNewResult(DrawableHitObject judgedObject, JudgementResult result)
|
private void onNewResult(DrawableHitObject judgedObject, JudgementResult result)
|
||||||
|
@ -38,9 +38,10 @@ namespace osu.Game.Rulesets.Osu.UI
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Show()
|
protected override void PopIn()
|
||||||
{
|
{
|
||||||
base.Show();
|
base.PopIn();
|
||||||
|
|
||||||
GameplayCursor.ActiveCursor.Hide();
|
GameplayCursor.ActiveCursor.Hide();
|
||||||
cursorScaleContainer.MoveTo(GameplayCursor.ActiveCursor.Position);
|
cursorScaleContainer.MoveTo(GameplayCursor.ActiveCursor.Position);
|
||||||
clickToResumeCursor.Appear();
|
clickToResumeCursor.Appear();
|
||||||
@ -55,13 +56,13 @@ namespace osu.Game.Rulesets.Osu.UI
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Hide()
|
protected override void PopOut()
|
||||||
{
|
{
|
||||||
|
base.PopOut();
|
||||||
|
|
||||||
localCursorContainer?.Expire();
|
localCursorContainer?.Expire();
|
||||||
localCursorContainer = null;
|
localCursorContainer = null;
|
||||||
GameplayCursor.ActiveCursor.Show();
|
GameplayCursor?.ActiveCursor?.Show();
|
||||||
|
|
||||||
base.Hide();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override bool OnHover(HoverEvent e) => true;
|
protected override bool OnHover(HoverEvent e) => true;
|
||||||
|
@ -66,7 +66,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
|||||||
AddStep("Reset height", () => changePlayfieldSize(6));
|
AddStep("Reset height", () => changePlayfieldSize(6));
|
||||||
|
|
||||||
var controlPointInfo = new ControlPointInfo();
|
var controlPointInfo = new ControlPointInfo();
|
||||||
controlPointInfo.TimingPoints.Add(new TimingControlPoint());
|
controlPointInfo.Add(0, new TimingControlPoint());
|
||||||
|
|
||||||
WorkingBeatmap beatmap = CreateWorkingBeatmap(new Beatmap
|
WorkingBeatmap beatmap = CreateWorkingBeatmap(new Beatmap
|
||||||
{
|
{
|
||||||
@ -142,7 +142,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
|||||||
HitResult hitResult = RNG.Next(2) == 0 ? HitResult.Good : HitResult.Great;
|
HitResult hitResult = RNG.Next(2) == 0 ? HitResult.Good : HitResult.Great;
|
||||||
|
|
||||||
var cpi = new ControlPointInfo();
|
var cpi = new ControlPointInfo();
|
||||||
cpi.EffectPoints.Add(new EffectControlPoint { KiaiMode = kiai });
|
cpi.Add(0, new EffectControlPoint { KiaiMode = kiai });
|
||||||
|
|
||||||
Hit hit = new Hit();
|
Hit hit = new Hit();
|
||||||
hit.ApplyDefaults(cpi, new BeatmapDifficulty());
|
hit.ApplyDefaults(cpi, new BeatmapDifficulty());
|
||||||
@ -157,7 +157,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
|||||||
HitResult hitResult = RNG.Next(2) == 0 ? HitResult.Good : HitResult.Great;
|
HitResult hitResult = RNG.Next(2) == 0 ? HitResult.Good : HitResult.Great;
|
||||||
|
|
||||||
var cpi = new ControlPointInfo();
|
var cpi = new ControlPointInfo();
|
||||||
cpi.EffectPoints.Add(new EffectControlPoint { KiaiMode = kiai });
|
cpi.Add(0, new EffectControlPoint { KiaiMode = kiai });
|
||||||
|
|
||||||
Hit hit = new Hit();
|
Hit hit = new Hit();
|
||||||
hit.ApplyDefaults(cpi, new BeatmapDifficulty());
|
hit.ApplyDefaults(cpi, new BeatmapDifficulty());
|
||||||
|
@ -19,12 +19,7 @@ namespace osu.Game.Rulesets.Taiko.Audio
|
|||||||
{
|
{
|
||||||
this.controlPoints = controlPoints;
|
this.controlPoints = controlPoints;
|
||||||
|
|
||||||
IEnumerable<SampleControlPoint> samplePoints;
|
IEnumerable<SampleControlPoint> samplePoints = controlPoints.SamplePoints.Count == 0 ? new[] { controlPoints.SamplePointAt(double.MinValue) } : controlPoints.SamplePoints;
|
||||||
if (controlPoints.SamplePoints.Count == 0)
|
|
||||||
// Get the default sample point
|
|
||||||
samplePoints = new[] { controlPoints.SamplePointAt(double.MinValue) };
|
|
||||||
else
|
|
||||||
samplePoints = controlPoints.SamplePoints;
|
|
||||||
|
|
||||||
foreach (var s in samplePoints)
|
foreach (var s in samplePoints)
|
||||||
{
|
{
|
||||||
|
@ -167,9 +167,9 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
|||||||
var controlPoints = beatmap.ControlPointInfo;
|
var controlPoints = beatmap.ControlPointInfo;
|
||||||
|
|
||||||
Assert.AreEqual(4, controlPoints.TimingPoints.Count);
|
Assert.AreEqual(4, controlPoints.TimingPoints.Count);
|
||||||
Assert.AreEqual(42, controlPoints.DifficultyPoints.Count);
|
Assert.AreEqual(5, controlPoints.DifficultyPoints.Count);
|
||||||
Assert.AreEqual(42, controlPoints.SamplePoints.Count);
|
Assert.AreEqual(34, controlPoints.SamplePoints.Count);
|
||||||
Assert.AreEqual(42, controlPoints.EffectPoints.Count);
|
Assert.AreEqual(8, controlPoints.EffectPoints.Count);
|
||||||
|
|
||||||
var timingPoint = controlPoints.TimingPointAt(0);
|
var timingPoint = controlPoints.TimingPointAt(0);
|
||||||
Assert.AreEqual(956, timingPoint.Time);
|
Assert.AreEqual(956, timingPoint.Time);
|
||||||
@ -191,7 +191,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
|||||||
Assert.AreEqual(1.0, difficultyPoint.SpeedMultiplier);
|
Assert.AreEqual(1.0, difficultyPoint.SpeedMultiplier);
|
||||||
|
|
||||||
difficultyPoint = controlPoints.DifficultyPointAt(48428);
|
difficultyPoint = controlPoints.DifficultyPointAt(48428);
|
||||||
Assert.AreEqual(48428, difficultyPoint.Time);
|
Assert.AreEqual(0, difficultyPoint.Time);
|
||||||
Assert.AreEqual(1.0, difficultyPoint.SpeedMultiplier);
|
Assert.AreEqual(1.0, difficultyPoint.SpeedMultiplier);
|
||||||
|
|
||||||
difficultyPoint = controlPoints.DifficultyPointAt(116999);
|
difficultyPoint = controlPoints.DifficultyPointAt(116999);
|
||||||
@ -224,7 +224,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
|||||||
Assert.IsFalse(effectPoint.OmitFirstBarLine);
|
Assert.IsFalse(effectPoint.OmitFirstBarLine);
|
||||||
|
|
||||||
effectPoint = controlPoints.EffectPointAt(119637);
|
effectPoint = controlPoints.EffectPointAt(119637);
|
||||||
Assert.AreEqual(119637, effectPoint.Time);
|
Assert.AreEqual(95901, effectPoint.Time);
|
||||||
Assert.IsFalse(effectPoint.KiaiMode);
|
Assert.IsFalse(effectPoint.KiaiMode);
|
||||||
Assert.IsFalse(effectPoint.OmitFirstBarLine);
|
Assert.IsFalse(effectPoint.OmitFirstBarLine);
|
||||||
}
|
}
|
||||||
@ -262,6 +262,21 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestTimingPointResetsSpeedMultiplier()
|
||||||
|
{
|
||||||
|
var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
|
||||||
|
|
||||||
|
using (var resStream = TestResources.OpenResource("timingpoint-speedmultiplier-reset.osu"))
|
||||||
|
using (var stream = new LineBufferedReader(resStream))
|
||||||
|
{
|
||||||
|
var controlPoints = decoder.Decode(stream).ControlPointInfo;
|
||||||
|
|
||||||
|
Assert.That(controlPoints.DifficultyPointAt(0).SpeedMultiplier, Is.EqualTo(0.5).Within(0.1));
|
||||||
|
Assert.That(controlPoints.DifficultyPointAt(2000).SpeedMultiplier, Is.EqualTo(1).Within(0.1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestDecodeBeatmapColours()
|
public void TestDecodeBeatmapColours()
|
||||||
{
|
{
|
||||||
@ -362,6 +377,23 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestDecodeControlPointDifficultyChange()
|
||||||
|
{
|
||||||
|
var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
|
||||||
|
|
||||||
|
using (var resStream = TestResources.OpenResource("controlpoint-difficulty-multiplier.osu"))
|
||||||
|
using (var stream = new LineBufferedReader(resStream))
|
||||||
|
{
|
||||||
|
var controlPointInfo = decoder.Decode(stream).ControlPointInfo;
|
||||||
|
|
||||||
|
Assert.That(controlPointInfo.DifficultyPointAt(5).SpeedMultiplier, Is.EqualTo(1));
|
||||||
|
Assert.That(controlPointInfo.DifficultyPointAt(1000).SpeedMultiplier, Is.EqualTo(10));
|
||||||
|
Assert.That(controlPointInfo.DifficultyPointAt(2000).SpeedMultiplier, Is.EqualTo(1.8518518518518519d));
|
||||||
|
Assert.That(controlPointInfo.DifficultyPointAt(3000).SpeedMultiplier, Is.EqualTo(0.5));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestDecodeControlPointCustomSampleBank()
|
public void TestDecodeControlPointCustomSampleBank()
|
||||||
{
|
{
|
||||||
|
@ -273,6 +273,96 @@ namespace osu.Game.Tests.Chat
|
|||||||
Assert.AreEqual(21, result.Links[0].Length);
|
Assert.AreEqual(21, result.Links[0].Length);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestMarkdownFormatLinkWithInlineTitle()
|
||||||
|
{
|
||||||
|
Message result = MessageFormatter.FormatMessage(new Message { Content = "I haven't seen [this link format](https://osu.ppy.sh \"osu!\") before..." });
|
||||||
|
|
||||||
|
Assert.AreEqual("I haven't seen this link format before...", result.DisplayContent);
|
||||||
|
Assert.AreEqual(1, result.Links.Count);
|
||||||
|
Assert.AreEqual("https://osu.ppy.sh", result.Links[0].Url);
|
||||||
|
Assert.AreEqual(15, result.Links[0].Index);
|
||||||
|
Assert.AreEqual(16, result.Links[0].Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestMarkdownFormatLinkWithInlineTitleAndEscapedQuotes()
|
||||||
|
{
|
||||||
|
Message result = MessageFormatter.FormatMessage(new Message { Content = "I haven't seen [this link format](https://osu.ppy.sh \"inner quote \\\" just to confuse \") before..." });
|
||||||
|
|
||||||
|
Assert.AreEqual("I haven't seen this link format before...", result.DisplayContent);
|
||||||
|
Assert.AreEqual(1, result.Links.Count);
|
||||||
|
Assert.AreEqual("https://osu.ppy.sh", result.Links[0].Url);
|
||||||
|
Assert.AreEqual(15, result.Links[0].Index);
|
||||||
|
Assert.AreEqual(16, result.Links[0].Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestMarkdownFormatLinkWithUrlInTextAndInlineTitle()
|
||||||
|
{
|
||||||
|
Message result = MessageFormatter.FormatMessage(new Message { Content = "I haven't seen [https://osu.ppy.sh](https://osu.ppy.sh \"https://osu.ppy.sh\") before..." });
|
||||||
|
|
||||||
|
Assert.AreEqual("I haven't seen https://osu.ppy.sh before...", result.DisplayContent);
|
||||||
|
Assert.AreEqual(1, result.Links.Count);
|
||||||
|
Assert.AreEqual("https://osu.ppy.sh", result.Links[0].Url);
|
||||||
|
Assert.AreEqual(15, result.Links[0].Index);
|
||||||
|
Assert.AreEqual(18, result.Links[0].Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestMarkdownFormatLinkWithUrlAndTextInTitle()
|
||||||
|
{
|
||||||
|
Message result = MessageFormatter.FormatMessage(new Message { Content = "I haven't seen [oh no, text here! https://osu.ppy.sh](https://osu.ppy.sh) before..." });
|
||||||
|
|
||||||
|
Assert.AreEqual("I haven't seen oh no, text here! https://osu.ppy.sh before...", result.DisplayContent);
|
||||||
|
Assert.AreEqual(1, result.Links.Count);
|
||||||
|
Assert.AreEqual("https://osu.ppy.sh", result.Links[0].Url);
|
||||||
|
Assert.AreEqual(15, result.Links[0].Index);
|
||||||
|
Assert.AreEqual(36, result.Links[0].Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestMarkdownFormatLinkWithMisleadingUrlInText()
|
||||||
|
{
|
||||||
|
Message result = MessageFormatter.FormatMessage(new Message { Content = "I haven't seen [https://google.com](https://osu.ppy.sh) before..." });
|
||||||
|
|
||||||
|
Assert.AreEqual("I haven't seen https://google.com before...", result.DisplayContent);
|
||||||
|
Assert.AreEqual(1, result.Links.Count);
|
||||||
|
Assert.AreEqual("https://osu.ppy.sh", result.Links[0].Url);
|
||||||
|
Assert.AreEqual(15, result.Links[0].Index);
|
||||||
|
Assert.AreEqual(18, result.Links[0].Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestMarkdownFormatLinkThatContractsIntoLargerLink()
|
||||||
|
{
|
||||||
|
Message result = MessageFormatter.FormatMessage(new Message { Content = "super broken https://[osu.ppy](https://reddit.com).sh/" });
|
||||||
|
|
||||||
|
Assert.AreEqual("super broken https://osu.ppy.sh/", result.DisplayContent);
|
||||||
|
Assert.AreEqual(1, result.Links.Count);
|
||||||
|
Assert.AreEqual("https://reddit.com", result.Links[0].Url);
|
||||||
|
Assert.AreEqual(21, result.Links[0].Index);
|
||||||
|
Assert.AreEqual(7, result.Links[0].Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestMarkdownFormatLinkDirectlyNextToRawLink()
|
||||||
|
{
|
||||||
|
// the raw link has a port at the end of it, so that the raw link regex terminates at the port and doesn't consume display text from the formatted one
|
||||||
|
Message result = MessageFormatter.FormatMessage(new Message { Content = "https://localhost:8080[https://osu.ppy.sh](https://osu.ppy.sh) should be two links" });
|
||||||
|
|
||||||
|
Assert.AreEqual("https://localhost:8080https://osu.ppy.sh should be two links", result.DisplayContent);
|
||||||
|
Assert.AreEqual(2, result.Links.Count);
|
||||||
|
|
||||||
|
Assert.AreEqual("https://localhost:8080", result.Links[0].Url);
|
||||||
|
Assert.AreEqual(0, result.Links[0].Index);
|
||||||
|
Assert.AreEqual(22, result.Links[0].Length);
|
||||||
|
|
||||||
|
Assert.AreEqual("https://osu.ppy.sh", result.Links[1].Url);
|
||||||
|
Assert.AreEqual(22, result.Links[1].Index);
|
||||||
|
Assert.AreEqual(18, result.Links[1].Length);
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestChannelLink()
|
public void TestChannelLink()
|
||||||
{
|
{
|
||||||
|
@ -0,0 +1,194 @@
|
|||||||
|
// 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.Testing;
|
||||||
|
using osu.Game.Beatmaps.ControlPoints;
|
||||||
|
using osu.Game.Rulesets.Osu;
|
||||||
|
using osu.Game.Rulesets.Osu.Edit;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
|
using osu.Game.Screens.Edit;
|
||||||
|
using osu.Game.Tests.Visual;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Editor
|
||||||
|
{
|
||||||
|
[HeadlessTest]
|
||||||
|
public class TestSceneHitObjectComposerDistanceSnapping : EditorClockTestScene
|
||||||
|
{
|
||||||
|
private TestHitObjectComposer composer;
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void Setup() => Schedule(() =>
|
||||||
|
{
|
||||||
|
Child = composer = new TestHitObjectComposer();
|
||||||
|
|
||||||
|
BeatDivisor.Value = 1;
|
||||||
|
|
||||||
|
composer.EditorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = 1;
|
||||||
|
composer.EditorBeatmap.ControlPointInfo.Clear();
|
||||||
|
|
||||||
|
composer.EditorBeatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 1 });
|
||||||
|
composer.EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 1000 });
|
||||||
|
});
|
||||||
|
|
||||||
|
[TestCase(1)]
|
||||||
|
[TestCase(2)]
|
||||||
|
public void TestSliderMultiplier(float multiplier)
|
||||||
|
{
|
||||||
|
AddStep($"set multiplier = {multiplier}", () => composer.EditorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = multiplier);
|
||||||
|
|
||||||
|
assertSnapDistance(100 * multiplier);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(1)]
|
||||||
|
[TestCase(2)]
|
||||||
|
public void TestSpeedMultiplier(float multiplier)
|
||||||
|
{
|
||||||
|
AddStep($"set multiplier = {multiplier}", () =>
|
||||||
|
{
|
||||||
|
composer.EditorBeatmap.ControlPointInfo.Clear();
|
||||||
|
composer.EditorBeatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = multiplier });
|
||||||
|
});
|
||||||
|
|
||||||
|
assertSnapDistance(100 * multiplier);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(1)]
|
||||||
|
[TestCase(2)]
|
||||||
|
public void TestBeatDivisor(int divisor)
|
||||||
|
{
|
||||||
|
AddStep($"set divisor = {divisor}", () => BeatDivisor.Value = divisor);
|
||||||
|
|
||||||
|
assertSnapDistance(100f / divisor);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestConvertDurationToDistance()
|
||||||
|
{
|
||||||
|
assertDurationToDistance(500, 50);
|
||||||
|
assertDurationToDistance(1000, 100);
|
||||||
|
|
||||||
|
AddStep("set slider multiplier = 2", () => composer.EditorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = 2);
|
||||||
|
|
||||||
|
assertDurationToDistance(500, 100);
|
||||||
|
assertDurationToDistance(1000, 200);
|
||||||
|
|
||||||
|
AddStep("set beat length = 500", () =>
|
||||||
|
{
|
||||||
|
composer.EditorBeatmap.ControlPointInfo.Clear();
|
||||||
|
composer.EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 500 });
|
||||||
|
});
|
||||||
|
|
||||||
|
assertDurationToDistance(500, 200);
|
||||||
|
assertDurationToDistance(1000, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestConvertDistanceToDuration()
|
||||||
|
{
|
||||||
|
assertDistanceToDuration(50, 500);
|
||||||
|
assertDistanceToDuration(100, 1000);
|
||||||
|
|
||||||
|
AddStep("set slider multiplier = 2", () => composer.EditorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = 2);
|
||||||
|
|
||||||
|
assertDistanceToDuration(100, 500);
|
||||||
|
assertDistanceToDuration(200, 1000);
|
||||||
|
|
||||||
|
AddStep("set beat length = 500", () =>
|
||||||
|
{
|
||||||
|
composer.EditorBeatmap.ControlPointInfo.Clear();
|
||||||
|
composer.EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 500 });
|
||||||
|
});
|
||||||
|
|
||||||
|
assertDistanceToDuration(200, 500);
|
||||||
|
assertDistanceToDuration(400, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestGetSnappedDurationFromDistance()
|
||||||
|
{
|
||||||
|
assertSnappedDuration(50, 0);
|
||||||
|
assertSnappedDuration(100, 1000);
|
||||||
|
assertSnappedDuration(150, 1000);
|
||||||
|
assertSnappedDuration(200, 2000);
|
||||||
|
assertSnappedDuration(250, 2000);
|
||||||
|
|
||||||
|
AddStep("set slider multiplier = 2", () => composer.EditorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = 2);
|
||||||
|
|
||||||
|
assertSnappedDuration(50, 0);
|
||||||
|
assertSnappedDuration(100, 0);
|
||||||
|
assertSnappedDuration(150, 0);
|
||||||
|
assertSnappedDuration(200, 1000);
|
||||||
|
assertSnappedDuration(250, 1000);
|
||||||
|
|
||||||
|
AddStep("set beat length = 500", () =>
|
||||||
|
{
|
||||||
|
composer.EditorBeatmap.ControlPointInfo.Clear();
|
||||||
|
composer.EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 500 });
|
||||||
|
});
|
||||||
|
|
||||||
|
assertSnappedDuration(50, 0);
|
||||||
|
assertSnappedDuration(100, 0);
|
||||||
|
assertSnappedDuration(150, 0);
|
||||||
|
assertSnappedDuration(200, 500);
|
||||||
|
assertSnappedDuration(250, 500);
|
||||||
|
assertSnappedDuration(400, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void GetSnappedDistanceFromDistance()
|
||||||
|
{
|
||||||
|
assertSnappedDistance(50, 0);
|
||||||
|
assertSnappedDistance(100, 100);
|
||||||
|
assertSnappedDistance(150, 100);
|
||||||
|
assertSnappedDistance(200, 200);
|
||||||
|
assertSnappedDistance(250, 200);
|
||||||
|
|
||||||
|
AddStep("set slider multiplier = 2", () => composer.EditorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = 2);
|
||||||
|
|
||||||
|
assertSnappedDistance(50, 0);
|
||||||
|
assertSnappedDistance(100, 0);
|
||||||
|
assertSnappedDistance(150, 0);
|
||||||
|
assertSnappedDistance(200, 200);
|
||||||
|
assertSnappedDistance(250, 200);
|
||||||
|
|
||||||
|
AddStep("set beat length = 500", () =>
|
||||||
|
{
|
||||||
|
composer.EditorBeatmap.ControlPointInfo.Clear();
|
||||||
|
composer.EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 500 });
|
||||||
|
});
|
||||||
|
|
||||||
|
assertSnappedDistance(50, 0);
|
||||||
|
assertSnappedDistance(100, 0);
|
||||||
|
assertSnappedDistance(150, 0);
|
||||||
|
assertSnappedDistance(200, 200);
|
||||||
|
assertSnappedDistance(250, 200);
|
||||||
|
assertSnappedDistance(400, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertSnapDistance(float expectedDistance)
|
||||||
|
=> AddAssert($"distance is {expectedDistance}", () => composer.GetBeatSnapDistanceAt(0) == expectedDistance);
|
||||||
|
|
||||||
|
private void assertDurationToDistance(double duration, float expectedDistance)
|
||||||
|
=> AddAssert($"duration = {duration} -> distance = {expectedDistance}", () => composer.DurationToDistance(0, duration) == expectedDistance);
|
||||||
|
|
||||||
|
private void assertDistanceToDuration(float distance, double expectedDuration)
|
||||||
|
=> AddAssert($"distance = {distance} -> duration = {expectedDuration}", () => composer.DistanceToDuration(0, distance) == expectedDuration);
|
||||||
|
|
||||||
|
private void assertSnappedDuration(float distance, double expectedDuration)
|
||||||
|
=> AddAssert($"distance = {distance} -> duration = {expectedDuration} (snapped)", () => composer.GetSnappedDurationFromDistance(0, distance) == expectedDuration);
|
||||||
|
|
||||||
|
private void assertSnappedDistance(float distance, float expectedDistance)
|
||||||
|
=> AddAssert($"distance = {distance} -> distance = {expectedDistance} (snapped)", () => composer.GetSnappedDistanceFromDistance(0, distance) == expectedDistance);
|
||||||
|
|
||||||
|
private class TestHitObjectComposer : OsuHitObjectComposer
|
||||||
|
{
|
||||||
|
public new EditorBeatmap<OsuHitObject> EditorBeatmap => base.EditorBeatmap;
|
||||||
|
|
||||||
|
public TestHitObjectComposer()
|
||||||
|
: base(new OsuRuleset())
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
227
osu.Game.Tests/NonVisual/ControlPointInfoTest.cs
Normal file
227
osu.Game.Tests/NonVisual/ControlPointInfoTest.cs
Normal file
@ -0,0 +1,227 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Linq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Game.Beatmaps.ControlPoints;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.NonVisual
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class ControlPointInfoTest
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void TestAdd()
|
||||||
|
{
|
||||||
|
var cpi = new ControlPointInfo();
|
||||||
|
|
||||||
|
cpi.Add(0, new TimingControlPoint());
|
||||||
|
cpi.Add(1000, new TimingControlPoint { BeatLength = 500 });
|
||||||
|
|
||||||
|
Assert.That(cpi.Groups.Count, Is.EqualTo(2));
|
||||||
|
Assert.That(cpi.TimingPoints.Count, Is.EqualTo(2));
|
||||||
|
Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(2));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestAddRedundantTiming()
|
||||||
|
{
|
||||||
|
var cpi = new ControlPointInfo();
|
||||||
|
|
||||||
|
cpi.Add(0, new TimingControlPoint()); // is *not* redundant, special exception for first timing point.
|
||||||
|
cpi.Add(1000, new TimingControlPoint()); // is redundant
|
||||||
|
|
||||||
|
Assert.That(cpi.Groups.Count, Is.EqualTo(1));
|
||||||
|
Assert.That(cpi.TimingPoints.Count, Is.EqualTo(1));
|
||||||
|
Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestAddRedundantDifficulty()
|
||||||
|
{
|
||||||
|
var cpi = new ControlPointInfo();
|
||||||
|
|
||||||
|
cpi.Add(0, new DifficultyControlPoint()); // is redundant
|
||||||
|
cpi.Add(1000, new DifficultyControlPoint()); // is redundant
|
||||||
|
|
||||||
|
Assert.That(cpi.Groups.Count, Is.EqualTo(0));
|
||||||
|
Assert.That(cpi.TimingPoints.Count, Is.EqualTo(0));
|
||||||
|
Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(0));
|
||||||
|
|
||||||
|
cpi.Add(1000, new DifficultyControlPoint { SpeedMultiplier = 2 }); // is not redundant
|
||||||
|
|
||||||
|
Assert.That(cpi.Groups.Count, Is.EqualTo(1));
|
||||||
|
Assert.That(cpi.DifficultyPoints.Count, Is.EqualTo(1));
|
||||||
|
Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestAddRedundantSample()
|
||||||
|
{
|
||||||
|
var cpi = new ControlPointInfo();
|
||||||
|
|
||||||
|
cpi.Add(0, new SampleControlPoint()); // is redundant
|
||||||
|
cpi.Add(1000, new SampleControlPoint()); // is redundant
|
||||||
|
|
||||||
|
Assert.That(cpi.Groups.Count, Is.EqualTo(0));
|
||||||
|
Assert.That(cpi.TimingPoints.Count, Is.EqualTo(0));
|
||||||
|
Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(0));
|
||||||
|
|
||||||
|
cpi.Add(1000, new SampleControlPoint { SampleVolume = 50 }); // is not redundant
|
||||||
|
|
||||||
|
Assert.That(cpi.Groups.Count, Is.EqualTo(1));
|
||||||
|
Assert.That(cpi.SamplePoints.Count, Is.EqualTo(1));
|
||||||
|
Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestAddRedundantEffect()
|
||||||
|
{
|
||||||
|
var cpi = new ControlPointInfo();
|
||||||
|
|
||||||
|
cpi.Add(0, new EffectControlPoint()); // is redundant
|
||||||
|
cpi.Add(1000, new EffectControlPoint()); // is redundant
|
||||||
|
|
||||||
|
Assert.That(cpi.Groups.Count, Is.EqualTo(0));
|
||||||
|
Assert.That(cpi.TimingPoints.Count, Is.EqualTo(0));
|
||||||
|
Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(0));
|
||||||
|
|
||||||
|
cpi.Add(1000, new EffectControlPoint { KiaiMode = true }); // is not redundant
|
||||||
|
|
||||||
|
Assert.That(cpi.Groups.Count, Is.EqualTo(1));
|
||||||
|
Assert.That(cpi.EffectPoints.Count, Is.EqualTo(1));
|
||||||
|
Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestAddGroup()
|
||||||
|
{
|
||||||
|
var cpi = new ControlPointInfo();
|
||||||
|
|
||||||
|
var group = cpi.GroupAt(1000, true);
|
||||||
|
var group2 = cpi.GroupAt(1000, true);
|
||||||
|
|
||||||
|
Assert.That(group, Is.EqualTo(group2));
|
||||||
|
Assert.That(cpi.Groups.Count, Is.EqualTo(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestGroupAtLookupOnly()
|
||||||
|
{
|
||||||
|
var cpi = new ControlPointInfo();
|
||||||
|
|
||||||
|
var group = cpi.GroupAt(5000, true);
|
||||||
|
Assert.That(group, Is.Not.Null);
|
||||||
|
|
||||||
|
Assert.That(cpi.Groups.Count, Is.EqualTo(1));
|
||||||
|
Assert.That(cpi.GroupAt(1000), Is.Null);
|
||||||
|
Assert.That(cpi.GroupAt(5000), Is.Not.Null);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestAddRemoveGroup()
|
||||||
|
{
|
||||||
|
var cpi = new ControlPointInfo();
|
||||||
|
|
||||||
|
var group = cpi.GroupAt(1000, true);
|
||||||
|
|
||||||
|
Assert.That(cpi.Groups.Count, Is.EqualTo(1));
|
||||||
|
|
||||||
|
cpi.RemoveGroup(group);
|
||||||
|
|
||||||
|
Assert.That(cpi.Groups.Count, Is.EqualTo(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestAddControlPointToGroup()
|
||||||
|
{
|
||||||
|
var cpi = new ControlPointInfo();
|
||||||
|
|
||||||
|
var group = cpi.GroupAt(1000, true);
|
||||||
|
Assert.That(cpi.Groups.Count, Is.EqualTo(1));
|
||||||
|
|
||||||
|
// usually redundant, but adding to group forces it to be added
|
||||||
|
group.Add(new DifficultyControlPoint());
|
||||||
|
|
||||||
|
Assert.That(group.ControlPoints.Count, Is.EqualTo(1));
|
||||||
|
Assert.That(cpi.DifficultyPoints.Count, Is.EqualTo(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestAddDuplicateControlPointToGroup()
|
||||||
|
{
|
||||||
|
var cpi = new ControlPointInfo();
|
||||||
|
|
||||||
|
var group = cpi.GroupAt(1000, true);
|
||||||
|
Assert.That(cpi.Groups.Count, Is.EqualTo(1));
|
||||||
|
|
||||||
|
group.Add(new DifficultyControlPoint());
|
||||||
|
group.Add(new DifficultyControlPoint { SpeedMultiplier = 2 });
|
||||||
|
|
||||||
|
Assert.That(group.ControlPoints.Count, Is.EqualTo(1));
|
||||||
|
Assert.That(cpi.DifficultyPoints.Count, Is.EqualTo(1));
|
||||||
|
Assert.That(cpi.DifficultyPoints.First().SpeedMultiplier, Is.EqualTo(2));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestRemoveControlPointFromGroup()
|
||||||
|
{
|
||||||
|
var cpi = new ControlPointInfo();
|
||||||
|
|
||||||
|
var group = cpi.GroupAt(1000, true);
|
||||||
|
Assert.That(cpi.Groups.Count, Is.EqualTo(1));
|
||||||
|
|
||||||
|
var difficultyPoint = new DifficultyControlPoint();
|
||||||
|
|
||||||
|
group.Add(difficultyPoint);
|
||||||
|
group.Remove(difficultyPoint);
|
||||||
|
|
||||||
|
Assert.That(group.ControlPoints.Count, Is.EqualTo(0));
|
||||||
|
Assert.That(cpi.DifficultyPoints.Count, Is.EqualTo(0));
|
||||||
|
Assert.That(cpi.AllControlPoints.Count, Is.EqualTo(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestOrdering()
|
||||||
|
{
|
||||||
|
var cpi = new ControlPointInfo();
|
||||||
|
|
||||||
|
cpi.Add(0, new TimingControlPoint());
|
||||||
|
cpi.Add(1000, new TimingControlPoint { BeatLength = 500 });
|
||||||
|
cpi.Add(10000, new TimingControlPoint { BeatLength = 200 });
|
||||||
|
cpi.Add(5000, new TimingControlPoint { BeatLength = 100 });
|
||||||
|
cpi.Add(3000, new DifficultyControlPoint { SpeedMultiplier = 2 });
|
||||||
|
cpi.GroupAt(7000, true).Add(new DifficultyControlPoint { SpeedMultiplier = 4 });
|
||||||
|
cpi.GroupAt(1000).Add(new SampleControlPoint { SampleVolume = 0 });
|
||||||
|
cpi.GroupAt(8000, true).Add(new EffectControlPoint { KiaiMode = true });
|
||||||
|
|
||||||
|
Assert.That(cpi.AllControlPoints.Count, Is.EqualTo(8));
|
||||||
|
|
||||||
|
Assert.That(cpi.Groups, Is.Ordered.Ascending.By(nameof(ControlPointGroup.Time)));
|
||||||
|
|
||||||
|
Assert.That(cpi.AllControlPoints, Is.Ordered.Ascending.By(nameof(ControlPoint.Time)));
|
||||||
|
Assert.That(cpi.TimingPoints, Is.Ordered.Ascending.By(nameof(ControlPoint.Time)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestClear()
|
||||||
|
{
|
||||||
|
var cpi = new ControlPointInfo();
|
||||||
|
|
||||||
|
cpi.Add(0, new TimingControlPoint());
|
||||||
|
cpi.Add(1000, new TimingControlPoint { BeatLength = 500 });
|
||||||
|
cpi.Add(10000, new TimingControlPoint { BeatLength = 200 });
|
||||||
|
cpi.Add(5000, new TimingControlPoint { BeatLength = 100 });
|
||||||
|
cpi.Add(3000, new DifficultyControlPoint { SpeedMultiplier = 2 });
|
||||||
|
cpi.GroupAt(7000, true).Add(new DifficultyControlPoint { SpeedMultiplier = 4 });
|
||||||
|
cpi.GroupAt(1000).Add(new SampleControlPoint { SampleVolume = 0 });
|
||||||
|
cpi.GroupAt(8000, true).Add(new EffectControlPoint { KiaiMode = true });
|
||||||
|
|
||||||
|
cpi.Clear();
|
||||||
|
|
||||||
|
Assert.That(cpi.Groups.Count, Is.EqualTo(0));
|
||||||
|
Assert.That(cpi.DifficultyPoints.Count, Is.EqualTo(0));
|
||||||
|
Assert.That(cpi.AllControlPoints.Count, Is.EqualTo(0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
osu file format v7
|
||||||
|
|
||||||
|
[TimingPoints]
|
||||||
|
0,100,4,2,0,100,1,0
|
||||||
|
12,500,4,2,0,100,1,0
|
||||||
|
1000,-10,4,2,0,100,0,0
|
||||||
|
2000,-54,4,2,0,100,0,0
|
||||||
|
3000,-200,4,2,0,100,0,0
|
@ -0,0 +1,5 @@
|
|||||||
|
osu file format v14
|
||||||
|
|
||||||
|
[TimingPoints]
|
||||||
|
0,-200,4,1,0,100,0,0
|
||||||
|
2000,100,1,1,0,100,1,0
|
@ -34,6 +34,7 @@ namespace osu.Game.Tests.Visual.Components
|
|||||||
PreviewTrack track = null;
|
PreviewTrack track = null;
|
||||||
|
|
||||||
AddStep("get track", () => track = getOwnedTrack());
|
AddStep("get track", () => track = getOwnedTrack());
|
||||||
|
AddUntilStep("wait loaded", () => track.IsLoaded);
|
||||||
AddStep("start", () => track.Start());
|
AddStep("start", () => track.Start());
|
||||||
AddAssert("started", () => track.IsRunning);
|
AddAssert("started", () => track.IsRunning);
|
||||||
AddStep("stop", () => track.Stop());
|
AddStep("stop", () => track.Stop());
|
||||||
@ -52,6 +53,8 @@ namespace osu.Game.Tests.Visual.Components
|
|||||||
track2 = getOwnedTrack();
|
track2 = getOwnedTrack();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
AddUntilStep("wait loaded", () => track1.IsLoaded && track2.IsLoaded);
|
||||||
|
|
||||||
AddStep("start track 1", () => track1.Start());
|
AddStep("start track 1", () => track1.Start());
|
||||||
AddStep("start track 2", () => track2.Start());
|
AddStep("start track 2", () => track2.Start());
|
||||||
AddAssert("track 1 stopped", () => !track1.IsRunning);
|
AddAssert("track 1 stopped", () => !track1.IsRunning);
|
||||||
@ -64,6 +67,7 @@ namespace osu.Game.Tests.Visual.Components
|
|||||||
PreviewTrack track = null;
|
PreviewTrack track = null;
|
||||||
|
|
||||||
AddStep("get track", () => track = getOwnedTrack());
|
AddStep("get track", () => track = getOwnedTrack());
|
||||||
|
AddUntilStep("wait loaded", () => track.IsLoaded);
|
||||||
AddStep("start", () => track.Start());
|
AddStep("start", () => track.Start());
|
||||||
AddStep("stop by owner", () => trackManager.StopAnyPlaying(this));
|
AddStep("stop by owner", () => trackManager.StopAnyPlaying(this));
|
||||||
AddAssert("stopped", () => !track.IsRunning);
|
AddAssert("stopped", () => !track.IsRunning);
|
||||||
@ -76,6 +80,7 @@ namespace osu.Game.Tests.Visual.Components
|
|||||||
PreviewTrack track = null;
|
PreviewTrack track = null;
|
||||||
|
|
||||||
AddStep("get track", () => Add(owner = new TestTrackOwner(track = getTrack())));
|
AddStep("get track", () => Add(owner = new TestTrackOwner(track = getTrack())));
|
||||||
|
AddUntilStep("wait loaded", () => track.IsLoaded);
|
||||||
AddStep("start", () => track.Start());
|
AddStep("start", () => track.Start());
|
||||||
AddStep("attempt stop", () => trackManager.StopAnyPlaying(this));
|
AddStep("attempt stop", () => trackManager.StopAnyPlaying(this));
|
||||||
AddAssert("not stopped", () => track.IsRunning);
|
AddAssert("not stopped", () => track.IsRunning);
|
||||||
@ -89,16 +94,24 @@ namespace osu.Game.Tests.Visual.Components
|
|||||||
{
|
{
|
||||||
var track = getTrack();
|
var track = getTrack();
|
||||||
|
|
||||||
Add(track);
|
LoadComponentAsync(track, Add);
|
||||||
|
|
||||||
return track;
|
return track;
|
||||||
}
|
}
|
||||||
|
|
||||||
private class TestTrackOwner : CompositeDrawable, IPreviewTrackOwner
|
private class TestTrackOwner : CompositeDrawable, IPreviewTrackOwner
|
||||||
{
|
{
|
||||||
|
private readonly PreviewTrack track;
|
||||||
|
|
||||||
public TestTrackOwner(PreviewTrack track)
|
public TestTrackOwner(PreviewTrack track)
|
||||||
{
|
{
|
||||||
AddInternal(track);
|
this.track = track;
|
||||||
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load()
|
||||||
|
{
|
||||||
|
LoadComponentAsync(track, AddInternal);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
|
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
|
||||||
|
@ -1,14 +1,12 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Shapes;
|
using osu.Framework.Graphics.Shapes;
|
||||||
using osu.Framework.MathUtils;
|
|
||||||
using osu.Game.Beatmaps;
|
|
||||||
using osu.Game.Beatmaps.ControlPoints;
|
using osu.Game.Beatmaps.ControlPoints;
|
||||||
|
using osu.Game.Rulesets.Edit;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
using osu.Game.Rulesets.Osu.Beatmaps;
|
using osu.Game.Rulesets.Osu.Beatmaps;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
@ -27,25 +25,27 @@ namespace osu.Game.Tests.Visual.Editor
|
|||||||
[Cached(typeof(IEditorBeatmap))]
|
[Cached(typeof(IEditorBeatmap))]
|
||||||
private readonly EditorBeatmap<OsuHitObject> editorBeatmap;
|
private readonly EditorBeatmap<OsuHitObject> editorBeatmap;
|
||||||
|
|
||||||
private TestDistanceSnapGrid grid;
|
[Cached(typeof(IDistanceSnapProvider))]
|
||||||
|
private readonly SnapProvider snapProvider = new SnapProvider();
|
||||||
|
|
||||||
public TestSceneDistanceSnapGrid()
|
public TestSceneDistanceSnapGrid()
|
||||||
{
|
{
|
||||||
editorBeatmap = new EditorBeatmap<OsuHitObject>(new OsuBeatmap());
|
editorBeatmap = new EditorBeatmap<OsuHitObject>(new OsuBeatmap());
|
||||||
editorBeatmap.ControlPointInfo.TimingPoints.Add(new TimingControlPoint { BeatLength = beat_length });
|
editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = beat_length });
|
||||||
|
|
||||||
createGrid();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[SetUp]
|
[SetUp]
|
||||||
public void Setup() => Schedule(() =>
|
public void Setup() => Schedule(() =>
|
||||||
{
|
{
|
||||||
Clear();
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
editorBeatmap.ControlPointInfo.TimingPoints.Clear();
|
new Box
|
||||||
editorBeatmap.ControlPointInfo.TimingPoints.Add(new TimingControlPoint { BeatLength = beat_length });
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
BeatDivisor.Value = 1;
|
Colour = Color4.SlateGray
|
||||||
|
},
|
||||||
|
new TestDistanceSnapGrid(new HitObject(), grid_position)
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
[TestCase(1)]
|
[TestCase(1)]
|
||||||
@ -56,65 +56,15 @@ namespace osu.Game.Tests.Visual.Editor
|
|||||||
[TestCase(8)]
|
[TestCase(8)]
|
||||||
[TestCase(12)]
|
[TestCase(12)]
|
||||||
[TestCase(16)]
|
[TestCase(16)]
|
||||||
public void TestInitialBeatDivisor(int divisor)
|
public void TestBeatDivisor(int divisor)
|
||||||
{
|
{
|
||||||
AddStep($"set beat divisor = {divisor}", () => BeatDivisor.Value = divisor);
|
AddStep($"set beat divisor = {divisor}", () => BeatDivisor.Value = divisor);
|
||||||
createGrid();
|
|
||||||
|
|
||||||
float expectedDistance = (float)beat_length / divisor;
|
|
||||||
AddAssert($"spacing is {expectedDistance}", () => Precision.AlmostEquals(grid.DistanceSpacing, expectedDistance));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestChangeBeatDivisor()
|
public void TestLimitedDistance()
|
||||||
{
|
{
|
||||||
createGrid();
|
AddStep("create limited grid", () =>
|
||||||
AddStep("set beat divisor = 2", () => BeatDivisor.Value = 2);
|
|
||||||
|
|
||||||
const float expected_distance = (float)beat_length / 2;
|
|
||||||
AddAssert($"spacing is {expected_distance}", () => Precision.AlmostEquals(grid.DistanceSpacing, expected_distance));
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestCase(100)]
|
|
||||||
[TestCase(200)]
|
|
||||||
public void TestBeatLength(double beatLength)
|
|
||||||
{
|
|
||||||
AddStep($"set beat length = {beatLength}", () =>
|
|
||||||
{
|
|
||||||
editorBeatmap.ControlPointInfo.TimingPoints.Clear();
|
|
||||||
editorBeatmap.ControlPointInfo.TimingPoints.Add(new TimingControlPoint { BeatLength = beatLength });
|
|
||||||
});
|
|
||||||
|
|
||||||
createGrid();
|
|
||||||
AddAssert($"spacing is {beatLength}", () => Precision.AlmostEquals(grid.DistanceSpacing, beatLength));
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestCase(1)]
|
|
||||||
[TestCase(2)]
|
|
||||||
public void TestGridVelocity(float velocity)
|
|
||||||
{
|
|
||||||
createGrid(g => g.Velocity = velocity);
|
|
||||||
|
|
||||||
float expectedDistance = (float)beat_length * velocity;
|
|
||||||
AddAssert($"spacing is {expectedDistance}", () => Precision.AlmostEquals(grid.DistanceSpacing, expectedDistance));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void TestGetSnappedTime()
|
|
||||||
{
|
|
||||||
createGrid();
|
|
||||||
|
|
||||||
Vector2 snapPosition = Vector2.Zero;
|
|
||||||
AddStep("get first tick position", () => snapPosition = grid_position + new Vector2((float)beat_length, 0));
|
|
||||||
AddAssert("snap time is 1 beat away", () => Precision.AlmostEquals(beat_length, grid.GetSnapTime(snapPosition), 0.01));
|
|
||||||
|
|
||||||
createGrid(g => g.Velocity = 2, "with velocity = 2");
|
|
||||||
AddAssert("snap time is now 0.5 beats away", () => Precision.AlmostEquals(beat_length / 2, grid.GetSnapTime(snapPosition), 0.01));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void createGrid(Action<TestDistanceSnapGrid> func = null, string description = null)
|
|
||||||
{
|
|
||||||
AddStep($"create grid {description ?? string.Empty}", () =>
|
|
||||||
{
|
{
|
||||||
Children = new Drawable[]
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
@ -123,21 +73,17 @@ namespace osu.Game.Tests.Visual.Editor
|
|||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Colour = Color4.SlateGray
|
Colour = Color4.SlateGray
|
||||||
},
|
},
|
||||||
grid = new TestDistanceSnapGrid(new HitObject(), grid_position)
|
new TestDistanceSnapGrid(new HitObject(), grid_position, new HitObject { StartTime = 100 })
|
||||||
};
|
};
|
||||||
|
|
||||||
func?.Invoke(grid);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private class TestDistanceSnapGrid : DistanceSnapGrid
|
private class TestDistanceSnapGrid : DistanceSnapGrid
|
||||||
{
|
{
|
||||||
public new float Velocity = 1;
|
|
||||||
|
|
||||||
public new float DistanceSpacing => base.DistanceSpacing;
|
public new float DistanceSpacing => base.DistanceSpacing;
|
||||||
|
|
||||||
public TestDistanceSnapGrid(HitObject hitObject, Vector2 centrePosition)
|
public TestDistanceSnapGrid(HitObject hitObject, Vector2 centrePosition, HitObject nextHitObject = null)
|
||||||
: base(hitObject, centrePosition)
|
: base(hitObject, nextHitObject, centrePosition)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -152,7 +98,7 @@ namespace osu.Game.Tests.Visual.Editor
|
|||||||
|
|
||||||
int beatIndex = 0;
|
int beatIndex = 0;
|
||||||
|
|
||||||
for (float s = centrePosition.X + DistanceSpacing; s <= DrawWidth; s += DistanceSpacing, beatIndex++)
|
for (float s = centrePosition.X + DistanceSpacing; s <= DrawWidth && beatIndex < MaxIntervals; s += DistanceSpacing, beatIndex++)
|
||||||
{
|
{
|
||||||
AddInternal(new Circle
|
AddInternal(new Circle
|
||||||
{
|
{
|
||||||
@ -165,7 +111,7 @@ namespace osu.Game.Tests.Visual.Editor
|
|||||||
|
|
||||||
beatIndex = 0;
|
beatIndex = 0;
|
||||||
|
|
||||||
for (float s = centrePosition.X - DistanceSpacing; s >= 0; s -= DistanceSpacing, beatIndex++)
|
for (float s = centrePosition.X - DistanceSpacing; s >= 0 && beatIndex < MaxIntervals; s -= DistanceSpacing, beatIndex++)
|
||||||
{
|
{
|
||||||
AddInternal(new Circle
|
AddInternal(new Circle
|
||||||
{
|
{
|
||||||
@ -178,7 +124,7 @@ namespace osu.Game.Tests.Visual.Editor
|
|||||||
|
|
||||||
beatIndex = 0;
|
beatIndex = 0;
|
||||||
|
|
||||||
for (float s = centrePosition.Y + DistanceSpacing; s <= DrawHeight; s += DistanceSpacing, beatIndex++)
|
for (float s = centrePosition.Y + DistanceSpacing; s <= DrawHeight && beatIndex < MaxIntervals; s += DistanceSpacing, beatIndex++)
|
||||||
{
|
{
|
||||||
AddInternal(new Circle
|
AddInternal(new Circle
|
||||||
{
|
{
|
||||||
@ -191,7 +137,7 @@ namespace osu.Game.Tests.Visual.Editor
|
|||||||
|
|
||||||
beatIndex = 0;
|
beatIndex = 0;
|
||||||
|
|
||||||
for (float s = centrePosition.Y - DistanceSpacing; s >= 0; s -= DistanceSpacing, beatIndex++)
|
for (float s = centrePosition.Y - DistanceSpacing; s >= 0 && beatIndex < MaxIntervals; s -= DistanceSpacing, beatIndex++)
|
||||||
{
|
{
|
||||||
AddInternal(new Circle
|
AddInternal(new Circle
|
||||||
{
|
{
|
||||||
@ -203,11 +149,23 @@ namespace osu.Game.Tests.Visual.Editor
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override float GetVelocity(double time, ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
|
public override (Vector2 position, double time) GetSnappedPosition(Vector2 screenSpacePosition)
|
||||||
=> Velocity;
|
=> (Vector2.Zero, 0);
|
||||||
|
}
|
||||||
|
|
||||||
public override Vector2 GetSnapPosition(Vector2 screenSpacePosition)
|
private class SnapProvider : IDistanceSnapProvider
|
||||||
=> Vector2.Zero;
|
{
|
||||||
|
public (Vector2 position, double time) GetSnappedPosition(Vector2 position, double time) => (position, time);
|
||||||
|
|
||||||
|
public float GetBeatSnapDistanceAt(double referenceTime) => 10;
|
||||||
|
|
||||||
|
public float DurationToDistance(double referenceTime, double duration) => (float)duration;
|
||||||
|
|
||||||
|
public double DistanceToDuration(double referenceTime, float distance) => distance;
|
||||||
|
|
||||||
|
public double GetSnappedDurationFromDistance(double referenceTime, float distance) => 0;
|
||||||
|
|
||||||
|
public float GetSnappedDistanceFromDistance(double referenceTime, float distance) => 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,9 +10,9 @@ using osu.Framework.Bindables;
|
|||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Shapes;
|
using osu.Framework.Graphics.Shapes;
|
||||||
using osu.Framework.Graphics.UserInterface;
|
|
||||||
using osu.Framework.Timing;
|
using osu.Framework.Timing;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Graphics.UserInterface;
|
||||||
using osu.Game.Screens.Edit.Compose.Components.Timeline;
|
using osu.Game.Screens.Edit.Compose.Components.Timeline;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
@ -101,7 +101,7 @@ namespace osu.Game.Tests.Visual.Editor
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class StartStopButton : Button
|
private class StartStopButton : OsuButton
|
||||||
{
|
{
|
||||||
private IAdjustableClock adjustableClock;
|
private IAdjustableClock adjustableClock;
|
||||||
private bool started;
|
private bool started;
|
||||||
|
@ -28,18 +28,7 @@ namespace osu.Game.Tests.Visual.Editor
|
|||||||
{
|
{
|
||||||
var testBeatmap = new Beatmap
|
var testBeatmap = new Beatmap
|
||||||
{
|
{
|
||||||
ControlPointInfo = new ControlPointInfo
|
ControlPointInfo = new ControlPointInfo(),
|
||||||
{
|
|
||||||
TimingPoints =
|
|
||||||
{
|
|
||||||
new TimingControlPoint { Time = 0, BeatLength = 200 },
|
|
||||||
new TimingControlPoint { Time = 100, BeatLength = 400 },
|
|
||||||
new TimingControlPoint { Time = 175, BeatLength = 800 },
|
|
||||||
new TimingControlPoint { Time = 350, BeatLength = 200 },
|
|
||||||
new TimingControlPoint { Time = 450, BeatLength = 100 },
|
|
||||||
new TimingControlPoint { Time = 500, BeatLength = 307.69230769230802 }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
HitObjects =
|
HitObjects =
|
||||||
{
|
{
|
||||||
new HitCircle { StartTime = 0 },
|
new HitCircle { StartTime = 0 },
|
||||||
@ -47,6 +36,13 @@ namespace osu.Game.Tests.Visual.Editor
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
testBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 200 });
|
||||||
|
testBeatmap.ControlPointInfo.Add(100, new TimingControlPoint { BeatLength = 400 });
|
||||||
|
testBeatmap.ControlPointInfo.Add(175, new TimingControlPoint { BeatLength = 800 });
|
||||||
|
testBeatmap.ControlPointInfo.Add(350, new TimingControlPoint { BeatLength = 200 });
|
||||||
|
testBeatmap.ControlPointInfo.Add(450, new TimingControlPoint { BeatLength = 100 });
|
||||||
|
testBeatmap.ControlPointInfo.Add(500, new TimingControlPoint { BeatLength = 307.69230769230802 });
|
||||||
|
|
||||||
Beatmap.Value = CreateWorkingBeatmap(testBeatmap);
|
Beatmap.Value = CreateWorkingBeatmap(testBeatmap);
|
||||||
|
|
||||||
Child = new TimingPointVisualiser(testBeatmap, 5000) { Clock = Clock };
|
Child = new TimingPointVisualiser(testBeatmap, 5000) { Clock = Clock };
|
||||||
|
@ -22,7 +22,7 @@ using osuTK;
|
|||||||
namespace osu.Game.Tests.Visual.Editor
|
namespace osu.Game.Tests.Visual.Editor
|
||||||
{
|
{
|
||||||
[TestFixture]
|
[TestFixture]
|
||||||
public class TestSceneHitObjectComposer : OsuTestScene
|
public class TestSceneHitObjectComposer : EditorClockTestScene
|
||||||
{
|
{
|
||||||
public override IReadOnlyList<Type> RequiredTypes => new[]
|
public override IReadOnlyList<Type> RequiredTypes => new[]
|
||||||
{
|
{
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -47,7 +47,8 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestRelativeBeatLengthScaleSingleTimingPoint()
|
public void TestRelativeBeatLengthScaleSingleTimingPoint()
|
||||||
{
|
{
|
||||||
var beatmap = createBeatmap(new TimingControlPoint { BeatLength = time_range / 2 });
|
var beatmap = createBeatmap();
|
||||||
|
beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = time_range / 2 });
|
||||||
|
|
||||||
createTest(beatmap, d => d.RelativeScaleBeatLengthsOverride = true);
|
createTest(beatmap, d => d.RelativeScaleBeatLengthsOverride = true);
|
||||||
|
|
||||||
@ -61,10 +62,10 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestRelativeBeatLengthScaleTimingPointBeyondEndDoesNotBecomeDominant()
|
public void TestRelativeBeatLengthScaleTimingPointBeyondEndDoesNotBecomeDominant()
|
||||||
{
|
{
|
||||||
var beatmap = createBeatmap(
|
var beatmap = createBeatmap();
|
||||||
new TimingControlPoint { BeatLength = time_range / 2 },
|
beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = time_range / 2 });
|
||||||
new TimingControlPoint { Time = 12000, BeatLength = time_range },
|
beatmap.ControlPointInfo.Add(12000, new TimingControlPoint { BeatLength = time_range });
|
||||||
new TimingControlPoint { Time = 100000, BeatLength = time_range });
|
beatmap.ControlPointInfo.Add(100000, new TimingControlPoint { BeatLength = time_range });
|
||||||
|
|
||||||
createTest(beatmap, d => d.RelativeScaleBeatLengthsOverride = true);
|
createTest(beatmap, d => d.RelativeScaleBeatLengthsOverride = true);
|
||||||
|
|
||||||
@ -75,9 +76,9 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestRelativeBeatLengthScaleFromSecondTimingPoint()
|
public void TestRelativeBeatLengthScaleFromSecondTimingPoint()
|
||||||
{
|
{
|
||||||
var beatmap = createBeatmap(
|
var beatmap = createBeatmap();
|
||||||
new TimingControlPoint { BeatLength = time_range },
|
beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = time_range });
|
||||||
new TimingControlPoint { Time = 3 * time_range, BeatLength = time_range / 2 });
|
beatmap.ControlPointInfo.Add(3 * time_range, new TimingControlPoint { BeatLength = time_range / 2 });
|
||||||
|
|
||||||
createTest(beatmap, d => d.RelativeScaleBeatLengthsOverride = true);
|
createTest(beatmap, d => d.RelativeScaleBeatLengthsOverride = true);
|
||||||
|
|
||||||
@ -97,9 +98,9 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestNonRelativeScale()
|
public void TestNonRelativeScale()
|
||||||
{
|
{
|
||||||
var beatmap = createBeatmap(
|
var beatmap = createBeatmap();
|
||||||
new TimingControlPoint { BeatLength = time_range },
|
beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = time_range });
|
||||||
new TimingControlPoint { Time = 3 * time_range, BeatLength = time_range / 2 });
|
beatmap.ControlPointInfo.Add(3 * time_range, new TimingControlPoint { BeatLength = time_range / 2 });
|
||||||
|
|
||||||
createTest(beatmap);
|
createTest(beatmap);
|
||||||
|
|
||||||
@ -119,7 +120,8 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestSliderMultiplierDoesNotAffectRelativeBeatLength()
|
public void TestSliderMultiplierDoesNotAffectRelativeBeatLength()
|
||||||
{
|
{
|
||||||
var beatmap = createBeatmap(new TimingControlPoint { BeatLength = time_range });
|
var beatmap = createBeatmap();
|
||||||
|
beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = time_range });
|
||||||
beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = 2;
|
beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = 2;
|
||||||
|
|
||||||
createTest(beatmap, d => d.RelativeScaleBeatLengthsOverride = true);
|
createTest(beatmap, d => d.RelativeScaleBeatLengthsOverride = true);
|
||||||
@ -132,7 +134,8 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestSliderMultiplierAffectsNonRelativeBeatLength()
|
public void TestSliderMultiplierAffectsNonRelativeBeatLength()
|
||||||
{
|
{
|
||||||
var beatmap = createBeatmap(new TimingControlPoint { BeatLength = time_range });
|
var beatmap = createBeatmap();
|
||||||
|
beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = time_range });
|
||||||
beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = 2;
|
beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = 2;
|
||||||
|
|
||||||
createTest(beatmap);
|
createTest(beatmap);
|
||||||
@ -154,14 +157,11 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
/// Creates an <see cref="IBeatmap"/>, containing 10 hitobjects and user-provided timing points.
|
/// Creates an <see cref="IBeatmap"/>, containing 10 hitobjects and user-provided timing points.
|
||||||
/// The hitobjects are spaced <see cref="time_range"/> milliseconds apart.
|
/// The hitobjects are spaced <see cref="time_range"/> milliseconds apart.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="timingControlPoints">The timing points to add to the beatmap.</param>
|
|
||||||
/// <returns>The <see cref="IBeatmap"/>.</returns>
|
/// <returns>The <see cref="IBeatmap"/>.</returns>
|
||||||
private IBeatmap createBeatmap(params TimingControlPoint[] timingControlPoints)
|
private IBeatmap createBeatmap()
|
||||||
{
|
{
|
||||||
var beatmap = new Beatmap<HitObject> { BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo } };
|
var beatmap = new Beatmap<HitObject> { BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo } };
|
||||||
|
|
||||||
beatmap.ControlPointInfo.TimingPoints.AddRange(timingControlPoints);
|
|
||||||
|
|
||||||
for (int i = 0; i < 10; i++)
|
for (int i = 0; i < 10; i++)
|
||||||
beatmap.HitObjects.Add(new HitObject { StartTime = i * time_range });
|
beatmap.HitObjects.Add(new HitObject { StartTime = i * time_range });
|
||||||
|
|
||||||
|
@ -69,6 +69,24 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
confirmClockRunning(true);
|
confirmClockRunning(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestPauseWithResumeOverlay()
|
||||||
|
{
|
||||||
|
AddStep("move cursor to center", () => InputManager.MoveMouseTo(Player.ScreenSpaceDrawQuad.Centre));
|
||||||
|
AddUntilStep("wait for hitobjects", () => Player.ScoreProcessor.Health.Value < 1);
|
||||||
|
|
||||||
|
pauseAndConfirm();
|
||||||
|
|
||||||
|
resume();
|
||||||
|
confirmClockRunning(false);
|
||||||
|
confirmPauseOverlayShown(false);
|
||||||
|
|
||||||
|
pauseAndConfirm();
|
||||||
|
|
||||||
|
AddUntilStep("resume overlay is not active", () => Player.DrawableRuleset.ResumeOverlay.State.Value == Visibility.Hidden);
|
||||||
|
confirmPaused();
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestResumeWithResumeOverlaySkipped()
|
public void TestResumeWithResumeOverlaySkipped()
|
||||||
{
|
{
|
||||||
@ -219,6 +237,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
AddUntilStep("player not exited", () => Player.IsCurrentScreen());
|
AddUntilStep("player not exited", () => Player.IsCurrentScreen());
|
||||||
AddStep("exit", () => Player.Exit());
|
AddStep("exit", () => Player.Exit());
|
||||||
confirmExited();
|
confirmExited();
|
||||||
|
confirmNoTrackAdjustments();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void confirmPaused()
|
private void confirmPaused()
|
||||||
@ -240,6 +259,11 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
AddUntilStep("player exited", () => !Player.IsCurrentScreen());
|
AddUntilStep("player exited", () => !Player.IsCurrentScreen());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void confirmNoTrackAdjustments()
|
||||||
|
{
|
||||||
|
AddAssert("track has no adjustments", () => Beatmap.Value.Track.AggregateFrequency.Value == 1);
|
||||||
|
}
|
||||||
|
|
||||||
private void restart() => AddStep("restart", () => Player.Restart());
|
private void restart() => AddStep("restart", () => Player.Restart());
|
||||||
private void pause() => AddStep("pause", () => Player.Pause());
|
private void pause() => AddStep("pause", () => Player.Pause());
|
||||||
private void resume() => AddStep("resume", () => Player.Resume());
|
private void resume() => AddStep("resume", () => Player.Resume());
|
||||||
|
@ -7,11 +7,10 @@ using osu.Game.Online;
|
|||||||
using osu.Game.Online.API.Requests.Responses;
|
using osu.Game.Online.API.Requests.Responses;
|
||||||
using osu.Game.Rulesets.Osu;
|
using osu.Game.Rulesets.Osu;
|
||||||
using osu.Game.Scoring;
|
using osu.Game.Scoring;
|
||||||
using osu.Game.Screens.Play;
|
|
||||||
using osu.Game.Users;
|
using osu.Game.Users;
|
||||||
using osuTK;
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using osu.Game.Screens.Ranking.Pages;
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.Gameplay
|
namespace osu.Game.Tests.Visual.Gameplay
|
||||||
{
|
{
|
||||||
@ -42,7 +41,6 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
{
|
{
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
Size = new Vector2(80, 40),
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Game.Graphics.UserInterface;
|
using osu.Game.Graphics.UserInterface;
|
||||||
using osu.Game.Screens.Play.HUD;
|
using osu.Game.Screens.Play.HUD;
|
||||||
using osu.Game.Screens.Play.PlayerSettings;
|
using osu.Game.Screens.Play.PlayerSettings;
|
||||||
@ -20,6 +21,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
{
|
{
|
||||||
Anchor = Anchor.TopRight,
|
Anchor = Anchor.TopRight,
|
||||||
Origin = Anchor.TopRight,
|
Origin = Anchor.TopRight,
|
||||||
|
State = { Value = Visibility.Visible }
|
||||||
});
|
});
|
||||||
|
|
||||||
Add(container = new ExampleContainer());
|
Add(container = new ExampleContainer());
|
||||||
|
@ -3,11 +3,16 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Screens;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Scoring;
|
using osu.Game.Scoring;
|
||||||
|
using osu.Game.Screens;
|
||||||
using osu.Game.Screens.Play;
|
using osu.Game.Screens.Play;
|
||||||
using osu.Game.Screens.Ranking;
|
using osu.Game.Screens.Ranking;
|
||||||
using osu.Game.Screens.Ranking.Pages;
|
using osu.Game.Screens.Ranking.Pages;
|
||||||
@ -22,11 +27,13 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
|
|
||||||
public override IReadOnlyList<Type> RequiredTypes => new[]
|
public override IReadOnlyList<Type> RequiredTypes => new[]
|
||||||
{
|
{
|
||||||
typeof(ScoreInfo),
|
|
||||||
typeof(Results),
|
typeof(Results),
|
||||||
typeof(ResultsPage),
|
typeof(ResultsPage),
|
||||||
typeof(ScoreResultsPage),
|
typeof(ScoreResultsPage),
|
||||||
typeof(LocalLeaderboardPage)
|
typeof(RetryButton),
|
||||||
|
typeof(ReplayDownloadButton),
|
||||||
|
typeof(LocalLeaderboardPage),
|
||||||
|
typeof(TestPlayer)
|
||||||
};
|
};
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
@ -42,26 +49,82 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
var beatmapInfo = beatmaps.QueryBeatmap(b => b.RulesetID == 0);
|
var beatmapInfo = beatmaps.QueryBeatmap(b => b.RulesetID == 0);
|
||||||
if (beatmapInfo != null)
|
if (beatmapInfo != null)
|
||||||
Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmapInfo);
|
Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmapInfo);
|
||||||
|
}
|
||||||
|
|
||||||
LoadScreen(new SoloResults(new ScoreInfo
|
private TestSoloResults createResultsScreen() => new TestSoloResults(new ScoreInfo
|
||||||
|
{
|
||||||
|
TotalScore = 2845370,
|
||||||
|
Accuracy = 0.98,
|
||||||
|
MaxCombo = 123,
|
||||||
|
Rank = ScoreRank.A,
|
||||||
|
Date = DateTimeOffset.Now,
|
||||||
|
Statistics = new Dictionary<HitResult, int>
|
||||||
{
|
{
|
||||||
TotalScore = 2845370,
|
{ HitResult.Great, 50 },
|
||||||
Accuracy = 0.98,
|
{ HitResult.Good, 20 },
|
||||||
MaxCombo = 123,
|
{ HitResult.Meh, 50 },
|
||||||
Rank = ScoreRank.A,
|
{ HitResult.Miss, 1 }
|
||||||
Date = DateTimeOffset.Now,
|
},
|
||||||
Statistics = new Dictionary<HitResult, int>
|
User = new User
|
||||||
|
{
|
||||||
|
Username = "peppy",
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void ResultsWithoutPlayer()
|
||||||
|
{
|
||||||
|
TestSoloResults screen = null;
|
||||||
|
|
||||||
|
AddStep("load results", () => Child = new OsuScreenStack(screen = createResultsScreen())
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both
|
||||||
|
});
|
||||||
|
AddUntilStep("wait for loaded", () => screen.IsLoaded);
|
||||||
|
AddAssert("retry overlay not present", () => screen.RetryOverlay == null);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void ResultsWithPlayer()
|
||||||
|
{
|
||||||
|
TestSoloResults screen = null;
|
||||||
|
|
||||||
|
AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen()));
|
||||||
|
AddUntilStep("wait for loaded", () => screen.IsLoaded);
|
||||||
|
AddAssert("retry overlay present", () => screen.RetryOverlay != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestResultsContainer : Container
|
||||||
|
{
|
||||||
|
[Cached(typeof(Player))]
|
||||||
|
private readonly Player player = new TestPlayer();
|
||||||
|
|
||||||
|
public TestResultsContainer(IScreen screen)
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both;
|
||||||
|
|
||||||
|
InternalChild = new OsuScreenStack(screen)
|
||||||
{
|
{
|
||||||
{ HitResult.Great, 50 },
|
RelativeSizeAxes = Axes.Both,
|
||||||
{ HitResult.Good, 20 },
|
};
|
||||||
{ HitResult.Meh, 50 },
|
}
|
||||||
{ HitResult.Miss, 1 }
|
}
|
||||||
},
|
|
||||||
User = new User
|
private class TestSoloResults : SoloResults
|
||||||
{
|
{
|
||||||
Username = "peppy",
|
public HotkeyRetryOverlay RetryOverlay;
|
||||||
}
|
|
||||||
}));
|
public TestSoloResults(ScoreInfo score)
|
||||||
|
: base(score)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
RetryOverlay = InternalChildren.OfType<HotkeyRetryOverlay>().SingleOrDefault();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -33,23 +33,15 @@ namespace osu.Game.Tests.Visual.Menus
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestInstantLoad()
|
public void TestInstantLoad()
|
||||||
{
|
{
|
||||||
bool logoVisible = false;
|
// visual only, very impossible to test this using asserts.
|
||||||
|
|
||||||
AddStep("begin loading", () =>
|
AddStep("load immediately", () =>
|
||||||
{
|
{
|
||||||
loader = new TestLoader();
|
loader = new TestLoader();
|
||||||
loader.AllowLoad.Set();
|
loader.AllowLoad.Set();
|
||||||
|
|
||||||
LoadScreen(loader);
|
LoadScreen(loader);
|
||||||
});
|
});
|
||||||
|
|
||||||
AddUntilStep("loaded", () =>
|
|
||||||
{
|
|
||||||
logoVisible = loader.Logo?.Alpha > 0;
|
|
||||||
return loader.Logo != null && loader.ScreenLoaded;
|
|
||||||
});
|
|
||||||
|
|
||||||
AddAssert("logo was not visible", () => !logoVisible);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -58,7 +50,7 @@ namespace osu.Game.Tests.Visual.Menus
|
|||||||
AddStep("begin loading", () => LoadScreen(loader = new TestLoader()));
|
AddStep("begin loading", () => LoadScreen(loader = new TestLoader()));
|
||||||
AddUntilStep("wait for logo visible", () => loader.Logo?.Alpha > 0);
|
AddUntilStep("wait for logo visible", () => loader.Logo?.Alpha > 0);
|
||||||
AddStep("finish loading", () => loader.AllowLoad.Set());
|
AddStep("finish loading", () => loader.AllowLoad.Set());
|
||||||
AddAssert("loaded", () => loader.Logo != null && loader.ScreenLoaded);
|
AddUntilStep("loaded", () => loader.Logo != null && loader.ScreenLoaded);
|
||||||
AddUntilStep("logo gone", () => loader.Logo?.Alpha == 0);
|
AddUntilStep("logo gone", () => loader.Logo?.Alpha == 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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(PreviewButton),
|
||||||
typeof(SuccessRate),
|
typeof(SuccessRate),
|
||||||
typeof(BeatmapAvailability),
|
typeof(BeatmapAvailability),
|
||||||
|
typeof(BeatmapRulesetSelector),
|
||||||
|
typeof(BeatmapRulesetTabItem),
|
||||||
};
|
};
|
||||||
|
|
||||||
protected override bool UseOnlineAPI => true;
|
protected override bool UseOnlineAPI => true;
|
||||||
|
|
||||||
private RulesetInfo taikoRuleset;
|
|
||||||
private RulesetInfo maniaRuleset;
|
|
||||||
|
|
||||||
public TestSceneBeatmapSetOverlay()
|
public TestSceneBeatmapSetOverlay()
|
||||||
{
|
{
|
||||||
Add(overlay = new TestBeatmapSetOverlay());
|
Add(overlay = new TestBeatmapSetOverlay());
|
||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[Resolved]
|
||||||
private void load(RulesetStore rulesets)
|
private RulesetStore rulesets { get; set; }
|
||||||
{
|
|
||||||
taikoRuleset = rulesets.GetRuleset(1);
|
|
||||||
maniaRuleset = rulesets.GetRuleset(3);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestLoading()
|
public void TestLoading()
|
||||||
@ -111,7 +106,7 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
StarDifficulty = 9.99,
|
StarDifficulty = 9.99,
|
||||||
Version = @"TEST",
|
Version = @"TEST",
|
||||||
Length = 456000,
|
Length = 456000,
|
||||||
Ruleset = maniaRuleset,
|
Ruleset = rulesets.GetRuleset(3),
|
||||||
BaseDifficulty = new BeatmapDifficulty
|
BaseDifficulty = new BeatmapDifficulty
|
||||||
{
|
{
|
||||||
CircleSize = 1,
|
CircleSize = 1,
|
||||||
@ -189,7 +184,7 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
StarDifficulty = 5.67,
|
StarDifficulty = 5.67,
|
||||||
Version = @"ANOTHER TEST",
|
Version = @"ANOTHER TEST",
|
||||||
Length = 123000,
|
Length = 123000,
|
||||||
Ruleset = taikoRuleset,
|
Ruleset = rulesets.GetRuleset(1),
|
||||||
BaseDifficulty = new BeatmapDifficulty
|
BaseDifficulty = new BeatmapDifficulty
|
||||||
{
|
{
|
||||||
CircleSize = 9,
|
CircleSize = 9,
|
||||||
@ -217,6 +212,54 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
downloadAssert(false);
|
downloadAssert(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestMultipleRulesets()
|
||||||
|
{
|
||||||
|
AddStep("show multiple rulesets beatmap", () =>
|
||||||
|
{
|
||||||
|
var beatmaps = new List<BeatmapInfo>();
|
||||||
|
|
||||||
|
foreach (var ruleset in rulesets.AvailableRulesets.Skip(1))
|
||||||
|
{
|
||||||
|
beatmaps.Add(new BeatmapInfo
|
||||||
|
{
|
||||||
|
Version = ruleset.Name,
|
||||||
|
Ruleset = ruleset,
|
||||||
|
BaseDifficulty = new BeatmapDifficulty(),
|
||||||
|
OnlineInfo = new BeatmapOnlineInfo(),
|
||||||
|
Metrics = new BeatmapMetrics
|
||||||
|
{
|
||||||
|
Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6).ToArray(),
|
||||||
|
Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6).ToArray(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
overlay.ShowBeatmapSet(new BeatmapSetInfo
|
||||||
|
{
|
||||||
|
Metadata = new BeatmapMetadata
|
||||||
|
{
|
||||||
|
Title = @"multiple rulesets beatmap",
|
||||||
|
Artist = @"none",
|
||||||
|
Author = new User
|
||||||
|
{
|
||||||
|
Username = "BanchoBot",
|
||||||
|
Id = 3,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
OnlineInfo = new BeatmapSetOnlineInfo
|
||||||
|
{
|
||||||
|
Covers = new BeatmapSetOnlineCovers(),
|
||||||
|
},
|
||||||
|
Metrics = new BeatmapSetMetrics { Ratings = Enumerable.Range(0, 11).ToArray() },
|
||||||
|
Beatmaps = beatmaps
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
AddAssert("shown beatmaps of current ruleset", () => overlay.Header.Picker.Difficulties.All(b => b.Beatmap.Ruleset.Equals(overlay.Header.RulesetSelector.Current.Value)));
|
||||||
|
AddAssert("left-most beatmap selected", () => overlay.Header.Picker.Difficulties.First().State == BeatmapPicker.DifficultySelectorState.Selected);
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestHide()
|
public void TestHide()
|
||||||
{
|
{
|
||||||
@ -281,12 +324,12 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
|
|
||||||
private void downloadAssert(bool shown)
|
private void downloadAssert(bool shown)
|
||||||
{
|
{
|
||||||
AddAssert($"is download button {(shown ? "shown" : "hidden")}", () => overlay.DownloadButtonsVisible == shown);
|
AddAssert($"is download button {(shown ? "shown" : "hidden")}", () => overlay.Header.DownloadButtonsVisible == shown);
|
||||||
}
|
}
|
||||||
|
|
||||||
private class TestBeatmapSetOverlay : BeatmapSetOverlay
|
private class TestBeatmapSetOverlay : BeatmapSetOverlay
|
||||||
{
|
{
|
||||||
public bool DownloadButtonsVisible => Header.DownloadButtonsVisible;
|
public new Header Header => base.Header;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,7 +22,8 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
typeof(HeaderButton),
|
typeof(HeaderButton),
|
||||||
typeof(SortTabControl),
|
typeof(SortTabControl),
|
||||||
typeof(ShowChildrenButton),
|
typeof(ShowChildrenButton),
|
||||||
typeof(DeletedChildrenPlaceholder)
|
typeof(DeletedChildrenPlaceholder),
|
||||||
|
typeof(VotePill)
|
||||||
};
|
};
|
||||||
|
|
||||||
protected override bool UseOnlineAPI => true;
|
protected override bool UseOnlineAPI => true;
|
||||||
|
@ -7,6 +7,10 @@ using osu.Game.Online.Chat;
|
|||||||
using osu.Game.Users;
|
using osu.Game.Users;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Game.Graphics.Containers;
|
||||||
|
using osu.Game.Overlays.Chat;
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.Online
|
namespace osu.Game.Tests.Visual.Online
|
||||||
{
|
{
|
||||||
@ -42,14 +46,14 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
[Cached]
|
[Cached]
|
||||||
private ChannelManager channelManager = new ChannelManager();
|
private ChannelManager channelManager = new ChannelManager();
|
||||||
|
|
||||||
private readonly StandAloneChatDisplay chatDisplay;
|
private readonly TestStandAloneChatDisplay chatDisplay;
|
||||||
private readonly StandAloneChatDisplay chatDisplay2;
|
private readonly TestStandAloneChatDisplay chatDisplay2;
|
||||||
|
|
||||||
public TestSceneStandAloneChatDisplay()
|
public TestSceneStandAloneChatDisplay()
|
||||||
{
|
{
|
||||||
Add(channelManager);
|
Add(channelManager);
|
||||||
|
|
||||||
Add(chatDisplay = new StandAloneChatDisplay
|
Add(chatDisplay = new TestStandAloneChatDisplay
|
||||||
{
|
{
|
||||||
Anchor = Anchor.CentreLeft,
|
Anchor = Anchor.CentreLeft,
|
||||||
Origin = Anchor.CentreLeft,
|
Origin = Anchor.CentreLeft,
|
||||||
@ -57,7 +61,7 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
Size = new Vector2(400, 80)
|
Size = new Vector2(400, 80)
|
||||||
});
|
});
|
||||||
|
|
||||||
Add(chatDisplay2 = new StandAloneChatDisplay(true)
|
Add(chatDisplay2 = new TestStandAloneChatDisplay(true)
|
||||||
{
|
{
|
||||||
Anchor = Anchor.CentreRight,
|
Anchor = Anchor.CentreRight,
|
||||||
Origin = Anchor.CentreRight,
|
Origin = Anchor.CentreRight,
|
||||||
@ -119,6 +123,49 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
Content = "Message from the future!",
|
Content = "Message from the future!",
|
||||||
Timestamp = DateTimeOffset.Now
|
Timestamp = DateTimeOffset.Now
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
AddUntilStep("ensure still scrolled to bottom", () => chatDisplay.ScrolledToBottom);
|
||||||
|
|
||||||
|
const int messages_per_call = 10;
|
||||||
|
AddRepeatStep("add many messages", () =>
|
||||||
|
{
|
||||||
|
for (int i = 0; i < messages_per_call; i++)
|
||||||
|
testChannel.AddNewMessages(new Message(sequence++)
|
||||||
|
{
|
||||||
|
Sender = longUsernameUser,
|
||||||
|
Content = "Many messages! " + Guid.NewGuid(),
|
||||||
|
Timestamp = DateTimeOffset.Now
|
||||||
|
});
|
||||||
|
}, Channel.MAX_HISTORY / messages_per_call + 5);
|
||||||
|
|
||||||
|
AddAssert("Ensure no adjacent day separators", () =>
|
||||||
|
{
|
||||||
|
var indices = chatDisplay.FillFlow.OfType<DrawableChannel.DaySeparator>().Select(ds => chatDisplay.FillFlow.IndexOf(ds));
|
||||||
|
|
||||||
|
foreach (var i in indices)
|
||||||
|
if (i < chatDisplay.FillFlow.Count && chatDisplay.FillFlow[i + 1] is DrawableChannel.DaySeparator)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
AddUntilStep("ensure still scrolled to bottom", () => chatDisplay.ScrolledToBottom);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestStandAloneChatDisplay : StandAloneChatDisplay
|
||||||
|
{
|
||||||
|
public TestStandAloneChatDisplay(bool textbox = false)
|
||||||
|
: base(textbox)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected DrawableChannel DrawableChannel => InternalChildren.OfType<DrawableChannel>().First();
|
||||||
|
|
||||||
|
protected OsuScrollContainer ScrollContainer => (OsuScrollContainer)((Container)DrawableChannel.Child).Child;
|
||||||
|
|
||||||
|
public FillFlowContainer FillFlow => (FillFlowContainer)ScrollContainer.Child;
|
||||||
|
|
||||||
|
public bool ScrolledToBottom => ScrollContainer.IsScrolledToEnd(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -245,6 +245,28 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
AddAssert($"Check #{set_count} is at bottom", () => carousel.BeatmapSets.Last().Metadata.Title.EndsWith($"#{set_count}!"));
|
AddAssert($"Check #{set_count} is at bottom", () => carousel.BeatmapSets.Last().Metadata.Title.EndsWith($"#{set_count}!"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestSortingStability()
|
||||||
|
{
|
||||||
|
var sets = new List<BeatmapSetInfo>();
|
||||||
|
|
||||||
|
for (int i = 0; i < 20; i++)
|
||||||
|
{
|
||||||
|
var set = createTestBeatmapSet(i);
|
||||||
|
set.Metadata.Artist = "same artist";
|
||||||
|
set.Metadata.Title = "same title";
|
||||||
|
sets.Add(set);
|
||||||
|
}
|
||||||
|
|
||||||
|
loadBeatmaps(sets);
|
||||||
|
|
||||||
|
AddStep("Sort by artist", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Artist }, false));
|
||||||
|
AddAssert("Items remain in original order", () => carousel.BeatmapSets.Select((set, index) => set.ID == index).All(b => b));
|
||||||
|
|
||||||
|
AddStep("Sort by title", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Title }, false));
|
||||||
|
AddAssert("Items remain in original order", () => carousel.BeatmapSets.Select((set, index) => set.ID == index).All(b => b));
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestSortingWithFiltered()
|
public void TestSortingWithFiltered()
|
||||||
{
|
{
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Audio.Track;
|
using osu.Framework.Audio.Track;
|
||||||
@ -10,7 +11,6 @@ using osu.Framework.Extensions.Color4Extensions;
|
|||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Shapes;
|
using osu.Framework.Graphics.Shapes;
|
||||||
using osu.Framework.Lists;
|
|
||||||
using osu.Framework.Timing;
|
using osu.Framework.Timing;
|
||||||
using osu.Game.Beatmaps.ControlPoints;
|
using osu.Game.Beatmaps.ControlPoints;
|
||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
@ -153,7 +153,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private SortedList<TimingControlPoint> timingPoints => Beatmap.Value.Beatmap.ControlPointInfo.TimingPoints;
|
private List<TimingControlPoint> timingPoints => Beatmap.Value.Beatmap.ControlPointInfo.TimingPoints.ToList();
|
||||||
|
|
||||||
private TimingControlPoint getNextTimingPoint(TimingControlPoint current)
|
private TimingControlPoint getNextTimingPoint(TimingControlPoint current)
|
||||||
{
|
{
|
||||||
|
@ -11,7 +11,7 @@ using osuTK.Graphics;
|
|||||||
|
|
||||||
namespace osu.Game.Tests.Visual.UserInterface
|
namespace osu.Game.Tests.Visual.UserInterface
|
||||||
{
|
{
|
||||||
public class TestSceneLabelledComponent : OsuTestScene
|
public class TestSceneLabelledDrawable : OsuTestScene
|
||||||
{
|
{
|
||||||
[TestCase(false)]
|
[TestCase(false)]
|
||||||
[TestCase(true)]
|
[TestCase(true)]
|
||||||
@ -25,7 +25,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
{
|
{
|
||||||
AddStep("create component", () =>
|
AddStep("create component", () =>
|
||||||
{
|
{
|
||||||
LabelledComponent<Drawable> component;
|
LabelledDrawable<Drawable> component;
|
||||||
|
|
||||||
Child = new Container
|
Child = new Container
|
||||||
{
|
{
|
||||||
@ -33,7 +33,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
Width = 500,
|
Width = 500,
|
||||||
AutoSizeAxes = Axes.Y,
|
AutoSizeAxes = Axes.Y,
|
||||||
Child = component = padded ? (LabelledComponent<Drawable>)new PaddedLabelledComponent() : new NonPaddedLabelledComponent(),
|
Child = component = padded ? (LabelledDrawable<Drawable>)new PaddedLabelledDrawable() : new NonPaddedLabelledDrawable(),
|
||||||
};
|
};
|
||||||
|
|
||||||
component.Label = "a sample component";
|
component.Label = "a sample component";
|
||||||
@ -41,9 +41,9 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private class PaddedLabelledComponent : LabelledComponent<Drawable>
|
private class PaddedLabelledDrawable : LabelledDrawable<Drawable>
|
||||||
{
|
{
|
||||||
public PaddedLabelledComponent()
|
public PaddedLabelledDrawable()
|
||||||
: base(true)
|
: base(true)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
@ -57,9 +57,9 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private class NonPaddedLabelledComponent : LabelledComponent<Drawable>
|
private class NonPaddedLabelledDrawable : LabelledDrawable<Drawable>
|
||||||
{
|
{
|
||||||
public NonPaddedLabelledComponent()
|
public NonPaddedLabelledDrawable()
|
||||||
: base(false)
|
: base(false)
|
||||||
{
|
{
|
||||||
}
|
}
|
@ -7,7 +7,6 @@ using NUnit.Framework;
|
|||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Game.Graphics.UserInterface;
|
|
||||||
using osu.Game.Graphics.UserInterfaceV2;
|
using osu.Game.Graphics.UserInterfaceV2;
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.UserInterface
|
namespace osu.Game.Tests.Visual.UserInterface
|
||||||
@ -28,7 +27,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
{
|
{
|
||||||
AddStep("create component", () =>
|
AddStep("create component", () =>
|
||||||
{
|
{
|
||||||
LabelledComponent<OsuTextBox> component;
|
LabelledTextBox component;
|
||||||
|
|
||||||
Child = new Container
|
Child = new Container
|
||||||
{
|
{
|
||||||
|
@ -89,7 +89,7 @@ namespace osu.Game.Tournament.Screens
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private class ActionableInfo : LabelledComponent<Drawable>
|
private class ActionableInfo : LabelledDrawable<Drawable>
|
||||||
{
|
{
|
||||||
private OsuButton button;
|
private OsuButton button;
|
||||||
|
|
||||||
|
@ -215,7 +215,7 @@ namespace osu.Game.Tournament
|
|||||||
|
|
||||||
foreach (var r in ladder.Rounds)
|
foreach (var r in ladder.Rounds)
|
||||||
foreach (var b in r.Beatmaps)
|
foreach (var b in r.Beatmaps)
|
||||||
if (b.BeatmapInfo == null)
|
if (b.BeatmapInfo == null && b.ID > 0)
|
||||||
{
|
{
|
||||||
var req = new GetBeatmapRequest(new BeatmapInfo { OnlineBeatmapID = b.ID });
|
var req = new GetBeatmapRequest(new BeatmapInfo { OnlineBeatmapID = b.ID });
|
||||||
req.Perform(API);
|
req.Perform(API);
|
||||||
|
@ -9,15 +9,18 @@ using osu.Framework.Threading;
|
|||||||
|
|
||||||
namespace osu.Game.Audio
|
namespace osu.Game.Audio
|
||||||
{
|
{
|
||||||
|
[LongRunningLoad]
|
||||||
public abstract class PreviewTrack : Component
|
public abstract class PreviewTrack : Component
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Invoked when this <see cref="PreviewTrack"/> has stopped playing.
|
/// Invoked when this <see cref="PreviewTrack"/> has stopped playing.
|
||||||
|
/// Not invoked in a thread-safe context.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public event Action Stopped;
|
public event Action Stopped;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Invoked when this <see cref="PreviewTrack"/> has started playing.
|
/// Invoked when this <see cref="PreviewTrack"/> has started playing.
|
||||||
|
/// Not invoked in a thread-safe context.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public event Action Started;
|
public event Action Started;
|
||||||
|
|
||||||
@ -29,7 +32,7 @@ namespace osu.Game.Audio
|
|||||||
{
|
{
|
||||||
track = GetTrack();
|
track = GetTrack();
|
||||||
if (track != null)
|
if (track != null)
|
||||||
track.Completed += () => Schedule(Stop);
|
track.Completed += Stop;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -93,6 +96,7 @@ namespace osu.Game.Audio
|
|||||||
hasStarted = false;
|
hasStarted = false;
|
||||||
|
|
||||||
track.Stop();
|
track.Stop();
|
||||||
|
|
||||||
Stopped?.Invoke();
|
Stopped?.Invoke();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,18 +46,18 @@ namespace osu.Game.Audio
|
|||||||
{
|
{
|
||||||
var track = CreatePreviewTrack(beatmapSetInfo, trackStore);
|
var track = CreatePreviewTrack(beatmapSetInfo, trackStore);
|
||||||
|
|
||||||
track.Started += () =>
|
track.Started += () => Schedule(() =>
|
||||||
{
|
{
|
||||||
current?.Stop();
|
current?.Stop();
|
||||||
current = track;
|
current = track;
|
||||||
audio.Tracks.AddAdjustment(AdjustableProperty.Volume, muteBindable);
|
audio.Tracks.AddAdjustment(AdjustableProperty.Volume, muteBindable);
|
||||||
};
|
});
|
||||||
|
|
||||||
track.Stopped += () =>
|
track.Stopped += () => Schedule(() =>
|
||||||
{
|
{
|
||||||
current = null;
|
current = null;
|
||||||
audio.Tracks.RemoveAdjustment(AdjustableProperty.Volume, muteBindable);
|
audio.Tracks.RemoveAdjustment(AdjustableProperty.Volume, muteBindable);
|
||||||
};
|
});
|
||||||
|
|
||||||
return track;
|
return track;
|
||||||
}
|
}
|
||||||
|
@ -392,8 +392,15 @@ namespace osu.Game.Beatmaps
|
|||||||
|
|
||||||
req.Failure += e => { LogForModel(set, $"Online retrieval failed for {beatmap} ({e.Message})"); };
|
req.Failure += e => { LogForModel(set, $"Online retrieval failed for {beatmap} ({e.Message})"); };
|
||||||
|
|
||||||
// intentionally blocking to limit web request concurrency
|
try
|
||||||
req.Perform(api);
|
{
|
||||||
|
// intentionally blocking to limit web request concurrency
|
||||||
|
req.Perform(api);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
LogForModel(set, $"Online retrieval failed for {beatmap} ({e.Message})");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,25 +1,30 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace osu.Game.Beatmaps.ControlPoints
|
namespace osu.Game.Beatmaps.ControlPoints
|
||||||
{
|
{
|
||||||
public class ControlPoint : IComparable<ControlPoint>, IEquatable<ControlPoint>
|
public abstract class ControlPoint : IComparable<ControlPoint>, IEquatable<ControlPoint>
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The time at which the control point takes effect.
|
/// The time at which the control point takes effect.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public double Time;
|
public double Time => controlPointGroup?.Time ?? 0;
|
||||||
|
|
||||||
/// <summary>
|
private ControlPointGroup controlPointGroup;
|
||||||
/// Whether this timing point was generated internally, as opposed to parsed from the underlying beatmap.
|
|
||||||
/// </summary>
|
public void AttachGroup(ControlPointGroup pointGroup) => this.controlPointGroup = pointGroup;
|
||||||
internal bool AutoGenerated;
|
|
||||||
|
|
||||||
public int CompareTo(ControlPoint other) => Time.CompareTo(other.Time);
|
public int CompareTo(ControlPoint other) => Time.CompareTo(other.Time);
|
||||||
|
|
||||||
public bool Equals(ControlPoint other)
|
/// <summary>
|
||||||
=> Time.Equals(other?.Time);
|
/// Whether this control point is equivalent to another, ignoring time.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="other">Another control point to compare with.</param>
|
||||||
|
/// <returns>Whether equivalent.</returns>
|
||||||
|
public abstract bool EquivalentTo(ControlPoint other);
|
||||||
|
|
||||||
|
public bool Equals(ControlPoint other) => Time.Equals(other?.Time) && EquivalentTo(other);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
50
osu.Game/Beatmaps/ControlPoints/ControlPointGroup.cs
Normal file
50
osu.Game/Beatmaps/ControlPoints/ControlPointGroup.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;
|
||||||
|
using System.Linq;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
|
||||||
|
namespace osu.Game.Beatmaps.ControlPoints
|
||||||
|
{
|
||||||
|
public class ControlPointGroup : IComparable<ControlPointGroup>
|
||||||
|
{
|
||||||
|
public event Action<ControlPoint> ItemAdded;
|
||||||
|
public event Action<ControlPoint> ItemRemoved;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The time at which the control point takes effect.
|
||||||
|
/// </summary>
|
||||||
|
public double Time { get; }
|
||||||
|
|
||||||
|
public IBindableList<ControlPoint> ControlPoints => controlPoints;
|
||||||
|
|
||||||
|
private readonly BindableList<ControlPoint> controlPoints = new BindableList<ControlPoint>();
|
||||||
|
|
||||||
|
public ControlPointGroup(double time)
|
||||||
|
{
|
||||||
|
Time = time;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int CompareTo(ControlPointGroup other) => Time.CompareTo(other.Time);
|
||||||
|
|
||||||
|
public void Add(ControlPoint point)
|
||||||
|
{
|
||||||
|
var existing = controlPoints.FirstOrDefault(p => p.GetType() == point.GetType());
|
||||||
|
|
||||||
|
if (existing != null)
|
||||||
|
Remove(existing);
|
||||||
|
|
||||||
|
point.AttachGroup(this);
|
||||||
|
|
||||||
|
controlPoints.Add(point);
|
||||||
|
ItemAdded?.Invoke(point);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Remove(ControlPoint point)
|
||||||
|
{
|
||||||
|
controlPoints.Remove(point);
|
||||||
|
ItemRemoved?.Invoke(point);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -5,6 +5,7 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Lists;
|
using osu.Framework.Lists;
|
||||||
|
|
||||||
namespace osu.Game.Beatmaps.ControlPoints
|
namespace osu.Game.Beatmaps.ControlPoints
|
||||||
@ -12,57 +13,78 @@ namespace osu.Game.Beatmaps.ControlPoints
|
|||||||
[Serializable]
|
[Serializable]
|
||||||
public class ControlPointInfo
|
public class ControlPointInfo
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// All control points grouped by time.
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty]
|
||||||
|
public IBindableList<ControlPointGroup> Groups => groups;
|
||||||
|
|
||||||
|
private readonly BindableList<ControlPointGroup> groups = new BindableList<ControlPointGroup>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// All timing points.
|
/// All timing points.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonProperty]
|
[JsonProperty]
|
||||||
public SortedList<TimingControlPoint> TimingPoints { get; private set; } = new SortedList<TimingControlPoint>(Comparer<TimingControlPoint>.Default);
|
public IReadOnlyList<TimingControlPoint> TimingPoints => timingPoints;
|
||||||
|
|
||||||
|
private readonly SortedList<TimingControlPoint> timingPoints = new SortedList<TimingControlPoint>(Comparer<TimingControlPoint>.Default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// All difficulty points.
|
/// All difficulty points.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonProperty]
|
[JsonProperty]
|
||||||
public SortedList<DifficultyControlPoint> DifficultyPoints { get; private set; } = new SortedList<DifficultyControlPoint>(Comparer<DifficultyControlPoint>.Default);
|
public IReadOnlyList<DifficultyControlPoint> DifficultyPoints => difficultyPoints;
|
||||||
|
|
||||||
|
private readonly SortedList<DifficultyControlPoint> difficultyPoints = new SortedList<DifficultyControlPoint>(Comparer<DifficultyControlPoint>.Default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// All sound points.
|
/// All sound points.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonProperty]
|
[JsonProperty]
|
||||||
public SortedList<SampleControlPoint> SamplePoints { get; private set; } = new SortedList<SampleControlPoint>(Comparer<SampleControlPoint>.Default);
|
public IReadOnlyList<SampleControlPoint> SamplePoints => samplePoints;
|
||||||
|
|
||||||
|
private readonly SortedList<SampleControlPoint> samplePoints = new SortedList<SampleControlPoint>(Comparer<SampleControlPoint>.Default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// All effect points.
|
/// All effect points.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonProperty]
|
[JsonProperty]
|
||||||
public SortedList<EffectControlPoint> EffectPoints { get; private set; } = new SortedList<EffectControlPoint>(Comparer<EffectControlPoint>.Default);
|
public IReadOnlyList<EffectControlPoint> EffectPoints => effectPoints;
|
||||||
|
|
||||||
|
private readonly SortedList<EffectControlPoint> effectPoints = new SortedList<EffectControlPoint>(Comparer<EffectControlPoint>.Default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// All control points, of all types.
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<ControlPoint> AllControlPoints => Groups.SelectMany(g => g.ControlPoints).ToArray();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Finds the difficulty control point that is active at <paramref name="time"/>.
|
/// Finds the difficulty control point that is active at <paramref name="time"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="time">The time to find the difficulty control point at.</param>
|
/// <param name="time">The time to find the difficulty control point at.</param>
|
||||||
/// <returns>The difficulty control point.</returns>
|
/// <returns>The difficulty control point.</returns>
|
||||||
public DifficultyControlPoint DifficultyPointAt(double time) => binarySearch(DifficultyPoints, time);
|
public DifficultyControlPoint DifficultyPointAt(double time) => binarySearchWithFallback(DifficultyPoints, time);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Finds the effect control point that is active at <paramref name="time"/>.
|
/// Finds the effect control point that is active at <paramref name="time"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="time">The time to find the effect control point at.</param>
|
/// <param name="time">The time to find the effect control point at.</param>
|
||||||
/// <returns>The effect control point.</returns>
|
/// <returns>The effect control point.</returns>
|
||||||
public EffectControlPoint EffectPointAt(double time) => binarySearch(EffectPoints, time);
|
public EffectControlPoint EffectPointAt(double time) => binarySearchWithFallback(EffectPoints, time);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Finds the sound control point that is active at <paramref name="time"/>.
|
/// Finds the sound control point that is active at <paramref name="time"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="time">The time to find the sound control point at.</param>
|
/// <param name="time">The time to find the sound control point at.</param>
|
||||||
/// <returns>The sound control point.</returns>
|
/// <returns>The sound control point.</returns>
|
||||||
public SampleControlPoint SamplePointAt(double time) => binarySearch(SamplePoints, time, SamplePoints.Count > 0 ? SamplePoints[0] : null);
|
public SampleControlPoint SamplePointAt(double time) => binarySearchWithFallback(SamplePoints, time, SamplePoints.Count > 0 ? SamplePoints[0] : null);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Finds the timing control point that is active at <paramref name="time"/>.
|
/// Finds the timing control point that is active at <paramref name="time"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="time">The time to find the timing control point at.</param>
|
/// <param name="time">The time to find the timing control point at.</param>
|
||||||
/// <returns>The timing control point.</returns>
|
/// <returns>The timing control point.</returns>
|
||||||
public TimingControlPoint TimingPointAt(double time) => binarySearch(TimingPoints, time, TimingPoints.Count > 0 ? TimingPoints[0] : null);
|
public TimingControlPoint TimingPointAt(double time) => binarySearchWithFallback(TimingPoints, time, TimingPoints.Count > 0 ? TimingPoints[0] : null);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Finds the maximum BPM represented by any timing control point.
|
/// Finds the maximum BPM represented by any timing control point.
|
||||||
@ -85,24 +107,93 @@ namespace osu.Game.Beatmaps.ControlPoints
|
|||||||
public double BPMMode =>
|
public double BPMMode =>
|
||||||
60000 / (TimingPoints.GroupBy(c => c.BeatLength).OrderByDescending(grp => grp.Count()).FirstOrDefault()?.FirstOrDefault() ?? new TimingControlPoint()).BeatLength;
|
60000 / (TimingPoints.GroupBy(c => c.BeatLength).OrderByDescending(grp => grp.Count()).FirstOrDefault()?.FirstOrDefault() ?? new TimingControlPoint()).BeatLength;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Remove all <see cref="ControlPointGroup"/>s and return to a pristine state.
|
||||||
|
/// </summary>
|
||||||
|
public void Clear()
|
||||||
|
{
|
||||||
|
groups.Clear();
|
||||||
|
timingPoints.Clear();
|
||||||
|
difficultyPoints.Clear();
|
||||||
|
samplePoints.Clear();
|
||||||
|
effectPoints.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add a new <see cref="ControlPoint"/>. Note that the provided control point may not be added if the correct state is already present at the provided time.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="time">The time at which the control point should be added.</param>
|
||||||
|
/// <param name="controlPoint">The control point to add.</param>
|
||||||
|
/// <returns>Whether the control point was added.</returns>
|
||||||
|
public bool Add(double time, ControlPoint controlPoint)
|
||||||
|
{
|
||||||
|
if (checkAlreadyExisting(time, controlPoint))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
GroupAt(time, true).Add(controlPoint);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ControlPointGroup GroupAt(double time, bool addIfNotExisting = false)
|
||||||
|
{
|
||||||
|
var newGroup = new ControlPointGroup(time);
|
||||||
|
|
||||||
|
int i = groups.BinarySearch(newGroup);
|
||||||
|
|
||||||
|
if (i >= 0)
|
||||||
|
return groups[i];
|
||||||
|
|
||||||
|
if (addIfNotExisting)
|
||||||
|
{
|
||||||
|
newGroup.ItemAdded += groupItemAdded;
|
||||||
|
newGroup.ItemRemoved += groupItemRemoved;
|
||||||
|
|
||||||
|
groups.Insert(~i, newGroup);
|
||||||
|
return newGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveGroup(ControlPointGroup group)
|
||||||
|
{
|
||||||
|
group.ItemAdded -= groupItemAdded;
|
||||||
|
group.ItemRemoved -= groupItemRemoved;
|
||||||
|
|
||||||
|
groups.Remove(group);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Binary searches one of the control point lists to find the active control point at <paramref name="time"/>.
|
/// Binary searches one of the control point lists to find the active control point at <paramref name="time"/>.
|
||||||
|
/// Includes logic for returning a specific point when no matching point is found.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="list">The list to search.</param>
|
/// <param name="list">The list to search.</param>
|
||||||
/// <param name="time">The time to find the control point at.</param>
|
/// <param name="time">The time to find the control point at.</param>
|
||||||
/// <param name="prePoint">The control point to use when <paramref name="time"/> is before any control points. If null, a new control point will be constructed.</param>
|
/// <param name="prePoint">The control point to use when <paramref name="time"/> is before any control points. If null, a new control point will be constructed.</param>
|
||||||
/// <returns>The active control point at <paramref name="time"/>.</returns>
|
/// <returns>The active control point at <paramref name="time"/>, or a fallback <see cref="ControlPoint"/> if none found.</returns>
|
||||||
private T binarySearch<T>(SortedList<T> list, double time, T prePoint = null)
|
private T binarySearchWithFallback<T>(IReadOnlyList<T> list, double time, T prePoint = null)
|
||||||
where T : ControlPoint, new()
|
where T : ControlPoint, new()
|
||||||
|
{
|
||||||
|
return binarySearch(list, time) ?? prePoint ?? new T();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Binary searches one of the control point lists to find the active control point at <paramref name="time"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="list">The list to search.</param>
|
||||||
|
/// <param name="time">The time to find the control point at.</param>
|
||||||
|
/// <returns>The active control point at <paramref name="time"/>.</returns>
|
||||||
|
private T binarySearch<T>(IReadOnlyList<T> list, double time)
|
||||||
|
where T : ControlPoint
|
||||||
{
|
{
|
||||||
if (list == null)
|
if (list == null)
|
||||||
throw new ArgumentNullException(nameof(list));
|
throw new ArgumentNullException(nameof(list));
|
||||||
|
|
||||||
if (list.Count == 0)
|
if (list.Count == 0)
|
||||||
return new T();
|
return null;
|
||||||
|
|
||||||
if (time < list[0].Time)
|
if (time < list[0].Time)
|
||||||
return prePoint ?? new T();
|
return null;
|
||||||
|
|
||||||
if (time >= list[list.Count - 1].Time)
|
if (time >= list[list.Count - 1].Time)
|
||||||
return list[list.Count - 1];
|
return list[list.Count - 1];
|
||||||
@ -125,5 +216,82 @@ namespace osu.Game.Beatmaps.ControlPoints
|
|||||||
// l will be the first control point with Time > time, but we want the one before it
|
// l will be the first control point with Time > time, but we want the one before it
|
||||||
return list[l - 1];
|
return list[l - 1];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check whether <see cref="newPoint"/> should be added.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="time">The time to find the timing control point at.</param>
|
||||||
|
/// <param name="newPoint">A point to be added.</param>
|
||||||
|
/// <returns>Whether the new point should be added.</returns>
|
||||||
|
private bool checkAlreadyExisting(double time, ControlPoint newPoint)
|
||||||
|
{
|
||||||
|
ControlPoint existing = null;
|
||||||
|
|
||||||
|
switch (newPoint)
|
||||||
|
{
|
||||||
|
case TimingControlPoint _:
|
||||||
|
// Timing points are a special case and need to be added regardless of fallback availability.
|
||||||
|
existing = binarySearch(TimingPoints, time);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case EffectControlPoint _:
|
||||||
|
existing = EffectPointAt(time);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SampleControlPoint _:
|
||||||
|
existing = SamplePointAt(time);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case DifficultyControlPoint _:
|
||||||
|
existing = DifficultyPointAt(time);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return existing?.EquivalentTo(newPoint) == true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void groupItemAdded(ControlPoint controlPoint)
|
||||||
|
{
|
||||||
|
switch (controlPoint)
|
||||||
|
{
|
||||||
|
case TimingControlPoint typed:
|
||||||
|
timingPoints.Add(typed);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case EffectControlPoint typed:
|
||||||
|
effectPoints.Add(typed);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SampleControlPoint typed:
|
||||||
|
samplePoints.Add(typed);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case DifficultyControlPoint typed:
|
||||||
|
difficultyPoints.Add(typed);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void groupItemRemoved(ControlPoint controlPoint)
|
||||||
|
{
|
||||||
|
switch (controlPoint)
|
||||||
|
{
|
||||||
|
case TimingControlPoint typed:
|
||||||
|
timingPoints.Remove(typed);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case EffectControlPoint typed:
|
||||||
|
effectPoints.Remove(typed);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SampleControlPoint typed:
|
||||||
|
samplePoints.Remove(typed);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case DifficultyControlPoint typed:
|
||||||
|
difficultyPoints.Remove(typed);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,26 +1,33 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using osu.Framework.Bindables;
|
||||||
using osuTK;
|
|
||||||
|
|
||||||
namespace osu.Game.Beatmaps.ControlPoints
|
namespace osu.Game.Beatmaps.ControlPoints
|
||||||
{
|
{
|
||||||
public class DifficultyControlPoint : ControlPoint, IEquatable<DifficultyControlPoint>
|
public class DifficultyControlPoint : ControlPoint
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The speed multiplier at this control point.
|
||||||
|
/// </summary>
|
||||||
|
public readonly BindableDouble SpeedMultiplierBindable = new BindableDouble(1)
|
||||||
|
{
|
||||||
|
Precision = 0.1,
|
||||||
|
Default = 1,
|
||||||
|
MinValue = 0.1,
|
||||||
|
MaxValue = 10
|
||||||
|
};
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The speed multiplier at this control point.
|
/// The speed multiplier at this control point.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public double SpeedMultiplier
|
public double SpeedMultiplier
|
||||||
{
|
{
|
||||||
get => speedMultiplier;
|
get => SpeedMultiplierBindable.Value;
|
||||||
set => speedMultiplier = MathHelper.Clamp(value, 0.1, 10);
|
set => SpeedMultiplierBindable.Value = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
private double speedMultiplier = 1;
|
public override bool EquivalentTo(ControlPoint other) =>
|
||||||
|
other is DifficultyControlPoint otherTyped && otherTyped.SpeedMultiplier.Equals(SpeedMultiplier);
|
||||||
public bool Equals(DifficultyControlPoint other)
|
|
||||||
=> base.Equals(other)
|
|
||||||
&& SpeedMultiplier.Equals(other?.SpeedMultiplier);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,24 +1,42 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using osu.Framework.Bindables;
|
||||||
|
|
||||||
namespace osu.Game.Beatmaps.ControlPoints
|
namespace osu.Game.Beatmaps.ControlPoints
|
||||||
{
|
{
|
||||||
public class EffectControlPoint : ControlPoint, IEquatable<EffectControlPoint>
|
public class EffectControlPoint : ControlPoint
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether this control point enables Kiai mode.
|
/// Whether the first bar line of this control point is ignored.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool KiaiMode;
|
public readonly BindableBool OmitFirstBarLineBindable = new BindableBool();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether the first bar line of this control point is ignored.
|
/// Whether the first bar line of this control point is ignored.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool OmitFirstBarLine;
|
public bool OmitFirstBarLine
|
||||||
|
{
|
||||||
|
get => OmitFirstBarLineBindable.Value;
|
||||||
|
set => OmitFirstBarLineBindable.Value = value;
|
||||||
|
}
|
||||||
|
|
||||||
public bool Equals(EffectControlPoint other)
|
/// <summary>
|
||||||
=> base.Equals(other)
|
/// Whether this control point enables Kiai mode.
|
||||||
&& KiaiMode == other?.KiaiMode && OmitFirstBarLine == other.OmitFirstBarLine;
|
/// </summary>
|
||||||
|
public readonly BindableBool KiaiModeBindable = new BindableBool();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether this control point enables Kiai mode.
|
||||||
|
/// </summary>
|
||||||
|
public bool KiaiMode
|
||||||
|
{
|
||||||
|
get => KiaiModeBindable.Value;
|
||||||
|
set => KiaiModeBindable.Value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool EquivalentTo(ControlPoint other) =>
|
||||||
|
other is EffectControlPoint otherTyped &&
|
||||||
|
KiaiMode == otherTyped.KiaiMode && OmitFirstBarLine == otherTyped.OmitFirstBarLine;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,24 +1,47 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using osu.Framework.Bindables;
|
||||||
using osu.Game.Audio;
|
using osu.Game.Audio;
|
||||||
|
|
||||||
namespace osu.Game.Beatmaps.ControlPoints
|
namespace osu.Game.Beatmaps.ControlPoints
|
||||||
{
|
{
|
||||||
public class SampleControlPoint : ControlPoint, IEquatable<SampleControlPoint>
|
public class SampleControlPoint : ControlPoint
|
||||||
{
|
{
|
||||||
public const string DEFAULT_BANK = "normal";
|
public const string DEFAULT_BANK = "normal";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The default sample bank at this control point.
|
/// The default sample bank at this control point.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string SampleBank = DEFAULT_BANK;
|
public readonly Bindable<string> SampleBankBindable = new Bindable<string>(DEFAULT_BANK) { Default = DEFAULT_BANK };
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The speed multiplier at this control point.
|
||||||
|
/// </summary>
|
||||||
|
public string SampleBank
|
||||||
|
{
|
||||||
|
get => SampleBankBindable.Value;
|
||||||
|
set => SampleBankBindable.Value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The default sample bank at this control point.
|
||||||
|
/// </summary>
|
||||||
|
public readonly BindableInt SampleVolumeBindable = new BindableInt(100)
|
||||||
|
{
|
||||||
|
MinValue = 0,
|
||||||
|
MaxValue = 100,
|
||||||
|
Default = 100
|
||||||
|
};
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The default sample volume at this control point.
|
/// The default sample volume at this control point.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int SampleVolume = 100;
|
public int SampleVolume
|
||||||
|
{
|
||||||
|
get => SampleVolumeBindable.Value;
|
||||||
|
set => SampleVolumeBindable.Value = value;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a SampleInfo based on the sample settings in this control point.
|
/// Create a SampleInfo based on the sample settings in this control point.
|
||||||
@ -45,8 +68,8 @@ namespace osu.Game.Beatmaps.ControlPoints
|
|||||||
return newSampleInfo;
|
return newSampleInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Equals(SampleControlPoint other)
|
public override bool EquivalentTo(ControlPoint other) =>
|
||||||
=> base.Equals(other)
|
other is SampleControlPoint otherTyped &&
|
||||||
&& string.Equals(SampleBank, other?.SampleBank) && SampleVolume == other?.SampleVolume;
|
string.Equals(SampleBank, otherTyped.SampleBank) && SampleVolume == otherTyped.SampleVolume;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,34 +1,55 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using osu.Framework.Bindables;
|
||||||
using osuTK;
|
|
||||||
using osu.Game.Beatmaps.Timing;
|
using osu.Game.Beatmaps.Timing;
|
||||||
|
|
||||||
namespace osu.Game.Beatmaps.ControlPoints
|
namespace osu.Game.Beatmaps.ControlPoints
|
||||||
{
|
{
|
||||||
public class TimingControlPoint : ControlPoint, IEquatable<TimingControlPoint>
|
public class TimingControlPoint : ControlPoint
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The time signature at this control point.
|
/// The time signature at this control point.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public TimeSignatures TimeSignature = TimeSignatures.SimpleQuadruple;
|
public readonly Bindable<TimeSignatures> TimeSignatureBindable = new Bindable<TimeSignatures>(TimeSignatures.SimpleQuadruple) { Default = TimeSignatures.SimpleQuadruple };
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The time signature at this control point.
|
||||||
|
/// </summary>
|
||||||
|
public TimeSignatures TimeSignature
|
||||||
|
{
|
||||||
|
get => TimeSignatureBindable.Value;
|
||||||
|
set => TimeSignatureBindable.Value = value;
|
||||||
|
}
|
||||||
|
|
||||||
public const double DEFAULT_BEAT_LENGTH = 1000;
|
public const double DEFAULT_BEAT_LENGTH = 1000;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The beat length at this control point.
|
/// The beat length at this control point.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual double BeatLength
|
public readonly BindableDouble BeatLengthBindable = new BindableDouble(DEFAULT_BEAT_LENGTH)
|
||||||
{
|
{
|
||||||
get => beatLength;
|
Default = DEFAULT_BEAT_LENGTH,
|
||||||
set => beatLength = MathHelper.Clamp(value, 6, 60000);
|
MinValue = 6,
|
||||||
|
MaxValue = 60000
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The beat length at this control point.
|
||||||
|
/// </summary>
|
||||||
|
public double BeatLength
|
||||||
|
{
|
||||||
|
get => BeatLengthBindable.Value;
|
||||||
|
set => BeatLengthBindable.Value = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
private double beatLength = DEFAULT_BEAT_LENGTH;
|
/// <summary>
|
||||||
|
/// The BPM at this control point.
|
||||||
|
/// </summary>
|
||||||
|
public double BPM => 60000 / BeatLength;
|
||||||
|
|
||||||
public bool Equals(TimingControlPoint other)
|
public override bool EquivalentTo(ControlPoint other) =>
|
||||||
=> base.Equals(other)
|
other is TimingControlPoint otherTyped
|
||||||
&& TimeSignature == other?.TimeSignature && beatLength.Equals(other.beatLength);
|
&& TimeSignature == otherTyped.TimeSignature && BeatLength.Equals(otherTyped.BeatLength);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ using osu.Framework.Graphics.Textures;
|
|||||||
|
|
||||||
namespace osu.Game.Beatmaps.Drawables
|
namespace osu.Game.Beatmaps.Drawables
|
||||||
{
|
{
|
||||||
|
[LongRunningLoad]
|
||||||
public class BeatmapSetCover : Sprite
|
public class BeatmapSetCover : Sprite
|
||||||
{
|
{
|
||||||
private readonly BeatmapSetInfo set;
|
private readonly BeatmapSetInfo set;
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.IO.File;
|
using osu.Framework.IO.File;
|
||||||
@ -50,6 +51,8 @@ namespace osu.Game.Beatmaps.Formats
|
|||||||
|
|
||||||
base.ParseStreamInto(stream, beatmap);
|
base.ParseStreamInto(stream, beatmap);
|
||||||
|
|
||||||
|
flushPendingPoints();
|
||||||
|
|
||||||
// Objects may be out of order *only* if a user has manually edited an .osu file.
|
// Objects may be out of order *only* if a user has manually edited an .osu file.
|
||||||
// Unfortunately there are ranked maps in this state (example: https://osu.ppy.sh/s/594828).
|
// Unfortunately there are ranked maps in this state (example: https://osu.ppy.sh/s/594828).
|
||||||
// OrderBy is used to guarantee that the parsing order of hitobjects with equal start times is maintained (stably-sorted)
|
// OrderBy is used to guarantee that the parsing order of hitobjects with equal start times is maintained (stably-sorted)
|
||||||
@ -369,104 +372,64 @@ namespace osu.Game.Beatmaps.Formats
|
|||||||
if (timingChange)
|
if (timingChange)
|
||||||
{
|
{
|
||||||
var controlPoint = CreateTimingControlPoint();
|
var controlPoint = CreateTimingControlPoint();
|
||||||
controlPoint.Time = time;
|
|
||||||
controlPoint.BeatLength = beatLength;
|
controlPoint.BeatLength = beatLength;
|
||||||
controlPoint.TimeSignature = timeSignature;
|
controlPoint.TimeSignature = timeSignature;
|
||||||
|
|
||||||
handleTimingControlPoint(controlPoint);
|
addControlPoint(time, controlPoint, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDifficultyControlPoint(new DifficultyControlPoint
|
addControlPoint(time, new LegacyDifficultyControlPoint
|
||||||
{
|
{
|
||||||
Time = time,
|
|
||||||
SpeedMultiplier = speedMultiplier,
|
SpeedMultiplier = speedMultiplier,
|
||||||
AutoGenerated = timingChange
|
}, timingChange);
|
||||||
});
|
|
||||||
|
|
||||||
handleEffectControlPoint(new EffectControlPoint
|
addControlPoint(time, new EffectControlPoint
|
||||||
{
|
{
|
||||||
Time = time,
|
|
||||||
KiaiMode = kiaiMode,
|
KiaiMode = kiaiMode,
|
||||||
OmitFirstBarLine = omitFirstBarSignature,
|
OmitFirstBarLine = omitFirstBarSignature,
|
||||||
AutoGenerated = timingChange
|
}, timingChange);
|
||||||
});
|
|
||||||
|
|
||||||
handleSampleControlPoint(new LegacySampleControlPoint
|
addControlPoint(time, new LegacySampleControlPoint
|
||||||
{
|
{
|
||||||
Time = time,
|
|
||||||
SampleBank = stringSampleSet,
|
SampleBank = stringSampleSet,
|
||||||
SampleVolume = sampleVolume,
|
SampleVolume = sampleVolume,
|
||||||
CustomSampleBank = customSampleBank,
|
CustomSampleBank = customSampleBank,
|
||||||
AutoGenerated = timingChange
|
}, timingChange);
|
||||||
});
|
|
||||||
|
// To handle the scenario where a non-timing line shares the same time value as a subsequent timing line but
|
||||||
|
// appears earlier in the file, we buffer non-timing control points and rewrite them *after* control points from the timing line
|
||||||
|
// with the same time value (allowing them to overwrite as necessary).
|
||||||
|
//
|
||||||
|
// The expected outcome is that we prefer the non-timing line's adjustments over the timing line's adjustments when time is equal.
|
||||||
|
if (timingChange)
|
||||||
|
flushPendingPoints();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleTimingControlPoint(TimingControlPoint newPoint)
|
private readonly List<ControlPoint> pendingControlPoints = new List<ControlPoint>();
|
||||||
|
private double pendingControlPointsTime;
|
||||||
|
|
||||||
|
private void addControlPoint(double time, ControlPoint point, bool timingChange)
|
||||||
{
|
{
|
||||||
var existing = beatmap.ControlPointInfo.TimingPointAt(newPoint.Time);
|
if (time != pendingControlPointsTime)
|
||||||
|
flushPendingPoints();
|
||||||
|
|
||||||
if (existing.Time == newPoint.Time)
|
if (timingChange)
|
||||||
{
|
{
|
||||||
// autogenerated points should not replace non-autogenerated.
|
beatmap.ControlPointInfo.Add(time, point);
|
||||||
// this allows for incorrectly ordered timing points to still be correctly handled.
|
return;
|
||||||
if (newPoint.AutoGenerated && !existing.AutoGenerated)
|
|
||||||
return;
|
|
||||||
|
|
||||||
beatmap.ControlPointInfo.TimingPoints.Remove(existing);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
beatmap.ControlPointInfo.TimingPoints.Add(newPoint);
|
pendingControlPoints.Add(point);
|
||||||
|
pendingControlPointsTime = time;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleDifficultyControlPoint(DifficultyControlPoint newPoint)
|
private void flushPendingPoints()
|
||||||
{
|
{
|
||||||
var existing = beatmap.ControlPointInfo.DifficultyPointAt(newPoint.Time);
|
foreach (var p in pendingControlPoints)
|
||||||
|
beatmap.ControlPointInfo.Add(pendingControlPointsTime, p);
|
||||||
|
|
||||||
if (existing.Time == newPoint.Time)
|
pendingControlPoints.Clear();
|
||||||
{
|
|
||||||
// autogenerated points should not replace non-autogenerated.
|
|
||||||
// this allows for incorrectly ordered timing points to still be correctly handled.
|
|
||||||
if (newPoint.AutoGenerated && !existing.AutoGenerated)
|
|
||||||
return;
|
|
||||||
|
|
||||||
beatmap.ControlPointInfo.DifficultyPoints.Remove(existing);
|
|
||||||
}
|
|
||||||
|
|
||||||
beatmap.ControlPointInfo.DifficultyPoints.Add(newPoint);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleEffectControlPoint(EffectControlPoint newPoint)
|
|
||||||
{
|
|
||||||
var existing = beatmap.ControlPointInfo.EffectPointAt(newPoint.Time);
|
|
||||||
|
|
||||||
if (existing.Time == newPoint.Time)
|
|
||||||
{
|
|
||||||
// autogenerated points should not replace non-autogenerated.
|
|
||||||
// this allows for incorrectly ordered timing points to still be correctly handled.
|
|
||||||
if (newPoint.AutoGenerated && !existing.AutoGenerated)
|
|
||||||
return;
|
|
||||||
|
|
||||||
beatmap.ControlPointInfo.EffectPoints.Remove(existing);
|
|
||||||
}
|
|
||||||
|
|
||||||
beatmap.ControlPointInfo.EffectPoints.Add(newPoint);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleSampleControlPoint(SampleControlPoint newPoint)
|
|
||||||
{
|
|
||||||
var existing = beatmap.ControlPointInfo.SamplePointAt(newPoint.Time);
|
|
||||||
|
|
||||||
if (existing.Time == newPoint.Time)
|
|
||||||
{
|
|
||||||
// autogenerated points should not replace non-autogenerated.
|
|
||||||
// this allows for incorrectly ordered timing points to still be correctly handled.
|
|
||||||
if (newPoint.AutoGenerated && !existing.AutoGenerated)
|
|
||||||
return;
|
|
||||||
|
|
||||||
beatmap.ControlPointInfo.SamplePoints.Remove(existing);
|
|
||||||
}
|
|
||||||
|
|
||||||
beatmap.ControlPointInfo.SamplePoints.Add(newPoint);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleHitObject(string line)
|
private void handleHitObject(string line)
|
||||||
|
@ -189,7 +189,15 @@ namespace osu.Game.Beatmaps.Formats
|
|||||||
Foreground = 3
|
Foreground = 3
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class LegacySampleControlPoint : SampleControlPoint, IEquatable<LegacySampleControlPoint>
|
internal class LegacyDifficultyControlPoint : DifficultyControlPoint
|
||||||
|
{
|
||||||
|
public LegacyDifficultyControlPoint()
|
||||||
|
{
|
||||||
|
SpeedMultiplierBindable.Precision = double.Epsilon;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class LegacySampleControlPoint : SampleControlPoint
|
||||||
{
|
{
|
||||||
public int CustomSampleBank;
|
public int CustomSampleBank;
|
||||||
|
|
||||||
@ -203,9 +211,9 @@ namespace osu.Game.Beatmaps.Formats
|
|||||||
return baseInfo;
|
return baseInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Equals(LegacySampleControlPoint other)
|
public override bool EquivalentTo(ControlPoint other) =>
|
||||||
=> base.Equals(other)
|
base.EquivalentTo(other) && other is LegacySampleControlPoint otherTyped &&
|
||||||
&& CustomSampleBank == other?.CustomSampleBank;
|
CustomSampleBank == otherTyped.CustomSampleBank;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,11 +28,15 @@ namespace osu.Game.Beatmaps.Formats
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected override TimingControlPoint CreateTimingControlPoint()
|
protected override TimingControlPoint CreateTimingControlPoint()
|
||||||
=> new LegacyDifficultyCalculatorControlPoint();
|
=> new LegacyDifficultyCalculatorTimingControlPoint();
|
||||||
|
|
||||||
private class LegacyDifficultyCalculatorControlPoint : TimingControlPoint
|
private class LegacyDifficultyCalculatorTimingControlPoint : TimingControlPoint
|
||||||
{
|
{
|
||||||
public override double BeatLength { get; set; } = DEFAULT_BEAT_LENGTH;
|
public LegacyDifficultyCalculatorTimingControlPoint()
|
||||||
|
{
|
||||||
|
BeatLengthBindable.MinValue = double.MinValue;
|
||||||
|
BeatLengthBindable.MaxValue = double.MaxValue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -108,7 +108,7 @@ namespace osu.Game.Database
|
|||||||
return Import(notification, paths);
|
return Import(notification, paths);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async Task Import(ProgressNotification notification, params string[] paths)
|
protected async Task<IEnumerable<TModel>> Import(ProgressNotification notification, params string[] paths)
|
||||||
{
|
{
|
||||||
notification.Progress = 0;
|
notification.Progress = 0;
|
||||||
notification.Text = $"{HumanisedModelName.Humanize(LetterCasing.Title)} import is initialising...";
|
notification.Text = $"{HumanisedModelName.Humanize(LetterCasing.Title)} import is initialising...";
|
||||||
@ -168,6 +168,8 @@ namespace osu.Game.Database
|
|||||||
|
|
||||||
notification.State = ProgressNotificationState.Completed;
|
notification.State = ProgressNotificationState.Completed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return imported;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -76,21 +76,17 @@ namespace osu.Game.Database
|
|||||||
Task.Factory.StartNew(async () =>
|
Task.Factory.StartNew(async () =>
|
||||||
{
|
{
|
||||||
// This gets scheduled back to the update thread, but we want the import to run in the background.
|
// This gets scheduled back to the update thread, but we want the import to run in the background.
|
||||||
await Import(notification, filename);
|
var imported = await Import(notification, filename);
|
||||||
|
|
||||||
|
// for now a failed import will be marked as a failed download for simplicity.
|
||||||
|
if (!imported.Any())
|
||||||
|
DownloadFailed?.Invoke(request);
|
||||||
|
|
||||||
currentDownloads.Remove(request);
|
currentDownloads.Remove(request);
|
||||||
}, TaskCreationOptions.LongRunning);
|
}, TaskCreationOptions.LongRunning);
|
||||||
};
|
};
|
||||||
|
|
||||||
request.Failure += error =>
|
request.Failure += triggerFailure;
|
||||||
{
|
|
||||||
DownloadFailed?.Invoke(request);
|
|
||||||
|
|
||||||
if (error is OperationCanceledException) return;
|
|
||||||
|
|
||||||
notification.State = ProgressNotificationState.Cancelled;
|
|
||||||
Logger.Error(error, $"{HumanisedModelName.Titleize()} download failed!");
|
|
||||||
currentDownloads.Remove(request);
|
|
||||||
};
|
|
||||||
|
|
||||||
notification.CancelRequested += () =>
|
notification.CancelRequested += () =>
|
||||||
{
|
{
|
||||||
@ -103,11 +99,31 @@ namespace osu.Game.Database
|
|||||||
currentDownloads.Add(request);
|
currentDownloads.Add(request);
|
||||||
PostNotification?.Invoke(notification);
|
PostNotification?.Invoke(notification);
|
||||||
|
|
||||||
Task.Factory.StartNew(() => request.Perform(api), TaskCreationOptions.LongRunning);
|
Task.Factory.StartNew(() =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
request.Perform(api);
|
||||||
|
}
|
||||||
|
catch (Exception error)
|
||||||
|
{
|
||||||
|
triggerFailure(error);
|
||||||
|
}
|
||||||
|
}, TaskCreationOptions.LongRunning);
|
||||||
|
|
||||||
DownloadBegan?.Invoke(request);
|
DownloadBegan?.Invoke(request);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
|
void triggerFailure(Exception error)
|
||||||
|
{
|
||||||
|
DownloadFailed?.Invoke(request);
|
||||||
|
|
||||||
|
if (error is OperationCanceledException) return;
|
||||||
|
|
||||||
|
notification.State = ProgressNotificationState.Cancelled;
|
||||||
|
Logger.Error(error, $"{HumanisedModelName.Titleize()} download failed!");
|
||||||
|
currentDownloads.Remove(request);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsAvailableLocally(TModel model) => CheckLocalAvailability(model, modelStore.ConsumableItems.Where(m => !m.DeletePending));
|
public bool IsAvailableLocally(TModel model) => CheckLocalAvailability(model, modelStore.ConsumableItems.Where(m => !m.DeletePending));
|
||||||
|
@ -166,19 +166,6 @@ namespace osu.Game.Database
|
|||||||
// no-op. called by tooling.
|
// no-op. called by tooling.
|
||||||
}
|
}
|
||||||
|
|
||||||
private class OsuDbLoggerProvider : ILoggerProvider
|
|
||||||
{
|
|
||||||
#region Disposal
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
public ILogger CreateLogger(string categoryName) => new OsuDbLogger();
|
|
||||||
}
|
|
||||||
|
|
||||||
private class OsuDbLogger : ILogger
|
private class OsuDbLogger : ILogger
|
||||||
{
|
{
|
||||||
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
|
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
|
||||||
|
@ -104,14 +104,10 @@ namespace osu.Game.Graphics.Containers
|
|||||||
defaultTiming = new TimingControlPoint
|
defaultTiming = new TimingControlPoint
|
||||||
{
|
{
|
||||||
BeatLength = default_beat_length,
|
BeatLength = default_beat_length,
|
||||||
AutoGenerated = true,
|
|
||||||
Time = 0
|
|
||||||
};
|
};
|
||||||
|
|
||||||
defaultEffect = new EffectControlPoint
|
defaultEffect = new EffectControlPoint
|
||||||
{
|
{
|
||||||
Time = 0,
|
|
||||||
AutoGenerated = true,
|
|
||||||
KiaiMode = false,
|
KiaiMode = false,
|
||||||
OmitFirstBarLine = false
|
OmitFirstBarLine = false
|
||||||
};
|
};
|
||||||
|
@ -8,9 +8,6 @@ using osu.Framework.Allocation;
|
|||||||
using osu.Framework.Graphics.Sprites;
|
using osu.Framework.Graphics.Sprites;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Logging;
|
|
||||||
using osu.Game.Overlays;
|
|
||||||
using osu.Game.Overlays.Notifications;
|
|
||||||
using osu.Game.Users;
|
using osu.Game.Users;
|
||||||
|
|
||||||
namespace osu.Game.Graphics.Containers
|
namespace osu.Game.Graphics.Containers
|
||||||
@ -23,21 +20,12 @@ namespace osu.Game.Graphics.Containers
|
|||||||
}
|
}
|
||||||
|
|
||||||
private OsuGame game;
|
private OsuGame game;
|
||||||
private ChannelManager channelManager;
|
|
||||||
private Action showNotImplementedError;
|
|
||||||
|
|
||||||
[BackgroundDependencyLoader(true)]
|
[BackgroundDependencyLoader(true)]
|
||||||
private void load(OsuGame game, NotificationOverlay notifications, ChannelManager channelManager)
|
private void load(OsuGame game)
|
||||||
{
|
{
|
||||||
// will be null in tests
|
// will be null in tests
|
||||||
this.game = game;
|
this.game = game;
|
||||||
this.channelManager = channelManager;
|
|
||||||
|
|
||||||
showNotImplementedError = () => notifications?.Post(new SimpleNotification
|
|
||||||
{
|
|
||||||
Text = @"This link type is not yet supported!",
|
|
||||||
Icon = FontAwesome.Solid.LifeRing,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AddLinks(string text, List<Link> links)
|
public void AddLinks(string text, List<Link> links)
|
||||||
@ -56,85 +44,47 @@ namespace osu.Game.Graphics.Containers
|
|||||||
foreach (var link in links)
|
foreach (var link in links)
|
||||||
{
|
{
|
||||||
AddText(text.Substring(previousLinkEnd, link.Index - previousLinkEnd));
|
AddText(text.Substring(previousLinkEnd, link.Index - previousLinkEnd));
|
||||||
AddLink(text.Substring(link.Index, link.Length), link.Url, link.Action, link.Argument);
|
AddLink(text.Substring(link.Index, link.Length), link.Action, link.Argument ?? link.Url);
|
||||||
previousLinkEnd = link.Index + link.Length;
|
previousLinkEnd = link.Index + link.Length;
|
||||||
}
|
}
|
||||||
|
|
||||||
AddText(text.Substring(previousLinkEnd));
|
AddText(text.Substring(previousLinkEnd));
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<Drawable> AddLink(string text, string url, LinkAction linkType = LinkAction.External, string linkArgument = null, string tooltipText = null, Action<SpriteText> creationParameters = null)
|
public void AddLink(string text, string url, Action<SpriteText> creationParameters = null) =>
|
||||||
=> createLink(AddText(text, creationParameters), text, url, linkType, linkArgument, tooltipText);
|
createLink(AddText(text, creationParameters), new LinkDetails(LinkAction.External, url), url);
|
||||||
|
|
||||||
public IEnumerable<Drawable> AddLink(string text, Action action, string tooltipText = null, Action<SpriteText> creationParameters = null)
|
public void AddLink(string text, Action action, string tooltipText = null, Action<SpriteText> creationParameters = null)
|
||||||
=> createLink(AddText(text, creationParameters), text, tooltipText: tooltipText, action: action);
|
=> createLink(AddText(text, creationParameters), new LinkDetails(LinkAction.Custom, null), tooltipText, action);
|
||||||
|
|
||||||
public IEnumerable<Drawable> AddLink(IEnumerable<SpriteText> text, string url, LinkAction linkType = LinkAction.External, string linkArgument = null, string tooltipText = null)
|
public void AddLink(string text, LinkAction action, string argument, string tooltipText = null, Action<SpriteText> creationParameters = null)
|
||||||
|
=> createLink(AddText(text, creationParameters), new LinkDetails(action, argument), null);
|
||||||
|
|
||||||
|
public void AddLink(IEnumerable<SpriteText> text, LinkAction action = LinkAction.External, string linkArgument = null, string tooltipText = null)
|
||||||
{
|
{
|
||||||
foreach (var t in text)
|
foreach (var t in text)
|
||||||
AddArbitraryDrawable(t);
|
AddArbitraryDrawable(t);
|
||||||
|
|
||||||
return createLink(text, null, url, linkType, linkArgument, tooltipText);
|
createLink(text, new LinkDetails(action, linkArgument), tooltipText);
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<Drawable> AddUserLink(User user, Action<SpriteText> creationParameters = null)
|
public void AddUserLink(User user, Action<SpriteText> creationParameters = null)
|
||||||
=> createLink(AddText(user.Username, creationParameters), user.Username, null, LinkAction.OpenUserProfile, user.Id.ToString(), "View profile");
|
=> createLink(AddText(user.Username, creationParameters), new LinkDetails(LinkAction.OpenUserProfile, user.Id.ToString()), "View Profile");
|
||||||
|
|
||||||
private IEnumerable<Drawable> createLink(IEnumerable<Drawable> drawables, string text, string url = null, LinkAction linkType = LinkAction.External, string linkArgument = null, string tooltipText = null, Action action = null)
|
private void createLink(IEnumerable<Drawable> drawables, LinkDetails link, string tooltipText, Action action = null)
|
||||||
{
|
{
|
||||||
AddInternal(new DrawableLinkCompiler(drawables.OfType<SpriteText>().ToList())
|
AddInternal(new DrawableLinkCompiler(drawables.OfType<SpriteText>().ToList())
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
TooltipText = tooltipText ?? (url != text ? url : string.Empty),
|
TooltipText = tooltipText,
|
||||||
Action = action ?? (() =>
|
Action = () =>
|
||||||
{
|
{
|
||||||
switch (linkType)
|
if (action != null)
|
||||||
{
|
action();
|
||||||
case LinkAction.OpenBeatmap:
|
else
|
||||||
// TODO: proper query params handling
|
game.HandleLink(link);
|
||||||
if (linkArgument != null && int.TryParse(linkArgument.Contains('?') ? linkArgument.Split('?')[0] : linkArgument, out int beatmapId))
|
},
|
||||||
game?.ShowBeatmap(beatmapId);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case LinkAction.OpenBeatmapSet:
|
|
||||||
if (int.TryParse(linkArgument, out int setId))
|
|
||||||
game?.ShowBeatmapSet(setId);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case LinkAction.OpenChannel:
|
|
||||||
try
|
|
||||||
{
|
|
||||||
channelManager?.OpenChannel(linkArgument);
|
|
||||||
}
|
|
||||||
catch (ChannelNotFoundException)
|
|
||||||
{
|
|
||||||
Logger.Log($"The requested channel \"{linkArgument}\" does not exist");
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
|
|
||||||
case LinkAction.OpenEditorTimestamp:
|
|
||||||
case LinkAction.JoinMultiplayerMatch:
|
|
||||||
case LinkAction.Spectate:
|
|
||||||
showNotImplementedError?.Invoke();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case LinkAction.External:
|
|
||||||
game?.OpenUrlExternally(url);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case LinkAction.OpenUserProfile:
|
|
||||||
if (long.TryParse(linkArgument, out long userId))
|
|
||||||
game?.ShowUser(userId);
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
throw new NotImplementedException($"This {nameof(LinkAction)} ({linkType.ToString()}) is missing an associated action.");
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return drawables;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// We want the compilers to always be visible no matter where they are, so RelativeSizeAxes is used.
|
// We want the compilers to always be visible no matter where they are, so RelativeSizeAxes is used.
|
||||||
|
85
osu.Game/Graphics/UserInterface/LoadingButton.cs
Normal file
85
osu.Game/Graphics/UserInterface/LoadingButton.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.Graphics;
|
||||||
|
using osu.Framework.Input.Events;
|
||||||
|
using osu.Game.Graphics.Containers;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
|
namespace osu.Game.Graphics.UserInterface
|
||||||
|
{
|
||||||
|
public abstract class LoadingButton : OsuHoverContainer
|
||||||
|
{
|
||||||
|
private bool isLoading;
|
||||||
|
|
||||||
|
public bool IsLoading
|
||||||
|
{
|
||||||
|
get => isLoading;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
isLoading = value;
|
||||||
|
|
||||||
|
Enabled.Value = !isLoading;
|
||||||
|
|
||||||
|
if (value)
|
||||||
|
{
|
||||||
|
loading.Show();
|
||||||
|
OnLoadStarted();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
loading.Hide();
|
||||||
|
OnLoadFinished();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Vector2 LoadingAnimationSize
|
||||||
|
{
|
||||||
|
get => loading.Size;
|
||||||
|
set => loading.Size = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly LoadingAnimation loading;
|
||||||
|
|
||||||
|
protected LoadingButton()
|
||||||
|
{
|
||||||
|
AddRange(new[]
|
||||||
|
{
|
||||||
|
CreateContent(),
|
||||||
|
loading = new LoadingAnimation
|
||||||
|
{
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
Size = new Vector2(12)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool OnClick(ClickEvent e)
|
||||||
|
{
|
||||||
|
if (!Enabled.Value)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return base.OnClick(e);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
// run afterwards as this will disable this button.
|
||||||
|
IsLoading = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void OnLoadStarted()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void OnLoadFinished()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract Drawable CreateContent();
|
||||||
|
}
|
||||||
|
}
|
@ -1,10 +1,12 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Diagnostics;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Extensions.Color4Extensions;
|
using osu.Framework.Extensions.Color4Extensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Shapes;
|
using osu.Framework.Graphics.Shapes;
|
||||||
using osu.Framework.Graphics.Sprites;
|
using osu.Framework.Graphics.Sprites;
|
||||||
using osu.Framework.Graphics.UserInterface;
|
using osu.Framework.Graphics.UserInterface;
|
||||||
@ -19,53 +21,104 @@ namespace osu.Game.Graphics.UserInterface
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class OsuButton : Button
|
public class OsuButton : Button
|
||||||
{
|
{
|
||||||
private Box hover;
|
public string Text
|
||||||
|
{
|
||||||
|
get => SpriteText?.Text;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (SpriteText != null)
|
||||||
|
SpriteText.Text = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
public OsuButton()
|
||||||
{
|
{
|
||||||
Height = 40;
|
Height = 40;
|
||||||
|
|
||||||
Content.Masking = true;
|
AddInternal(Content = new Container
|
||||||
Content.CornerRadius = 5;
|
{
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
Masking = true,
|
||||||
|
CornerRadius = 5,
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
Background = new Box
|
||||||
|
{
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
},
|
||||||
|
Hover = new Box
|
||||||
|
{
|
||||||
|
Alpha = 0,
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Colour = Color4.White.Opacity(.1f),
|
||||||
|
Blending = BlendingParameters.Additive,
|
||||||
|
Depth = float.MinValue
|
||||||
|
},
|
||||||
|
SpriteText = CreateText(),
|
||||||
|
new HoverClickSounds(HoverSampleSet.Loud),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Enabled.BindValueChanged(enabledChanged, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(OsuColour colours)
|
private void load(OsuColour colours)
|
||||||
{
|
{
|
||||||
BackgroundColour = colours.BlueDark;
|
if (backgroundColour == null)
|
||||||
|
BackgroundColour = colours.BlueDark;
|
||||||
AddRange(new Drawable[]
|
|
||||||
{
|
|
||||||
hover = new Box
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.Both,
|
|
||||||
Blending = BlendingParameters.Additive,
|
|
||||||
Colour = Color4.White.Opacity(0.1f),
|
|
||||||
Alpha = 0,
|
|
||||||
Depth = -1
|
|
||||||
},
|
|
||||||
new HoverClickSounds(HoverSampleSet.Loud),
|
|
||||||
});
|
|
||||||
|
|
||||||
Enabled.ValueChanged += enabledChanged;
|
Enabled.ValueChanged += enabledChanged;
|
||||||
Enabled.TriggerChange();
|
Enabled.TriggerChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void enabledChanged(ValueChangedEvent<bool> e)
|
protected override bool OnClick(ClickEvent e)
|
||||||
{
|
{
|
||||||
this.FadeColour(e.NewValue ? Color4.White : Color4.Gray, 200, Easing.OutQuint);
|
if (Enabled.Value)
|
||||||
|
{
|
||||||
|
Debug.Assert(backgroundColour != null);
|
||||||
|
Background.FlashColour(backgroundColour.Value, 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
return base.OnClick(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override bool OnHover(HoverEvent e)
|
protected override bool OnHover(HoverEvent e)
|
||||||
{
|
{
|
||||||
hover.FadeIn(200);
|
if (Enabled.Value)
|
||||||
|
Hover.FadeIn(200, Easing.OutQuint);
|
||||||
|
|
||||||
return base.OnHover(e);
|
return base.OnHover(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnHoverLost(HoverLostEvent e)
|
protected override void OnHoverLost(HoverLostEvent e)
|
||||||
{
|
{
|
||||||
hover.FadeOut(200);
|
|
||||||
base.OnHoverLost(e);
|
base.OnHoverLost(e);
|
||||||
|
|
||||||
|
Hover.FadeOut(300);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override bool OnMouseDown(MouseDownEvent e)
|
protected override bool OnMouseDown(MouseDownEvent e)
|
||||||
@ -80,12 +133,17 @@ namespace osu.Game.Graphics.UserInterface
|
|||||||
return base.OnMouseUp(e);
|
return base.OnMouseUp(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override SpriteText CreateText() => new OsuSpriteText
|
protected virtual SpriteText CreateText() => new OsuSpriteText
|
||||||
{
|
{
|
||||||
Depth = -1,
|
Depth = -1,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Font = OsuFont.GetFont(weight: FontWeight.Bold)
|
Font = OsuFont.GetFont(weight: FontWeight.Bold)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private void enabledChanged(ValueChangedEvent<bool> e)
|
||||||
|
{
|
||||||
|
this.FadeColour(e.NewValue ? Color4.White : Color4.Gray, 200, Easing.OutQuint);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,8 +5,6 @@ using osu.Framework.Graphics;
|
|||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Shapes;
|
using osu.Framework.Graphics.Shapes;
|
||||||
using osu.Framework.Graphics.Sprites;
|
using osu.Framework.Graphics.Sprites;
|
||||||
using osu.Framework.Input.Events;
|
|
||||||
using osu.Game.Graphics.Containers;
|
|
||||||
using osu.Game.Graphics.Sprites;
|
using osu.Game.Graphics.Sprites;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
@ -14,9 +12,9 @@ using System.Collections.Generic;
|
|||||||
|
|
||||||
namespace osu.Game.Graphics.UserInterface
|
namespace osu.Game.Graphics.UserInterface
|
||||||
{
|
{
|
||||||
public class ShowMoreButton : OsuHoverContainer
|
public class ShowMoreButton : LoadingButton
|
||||||
{
|
{
|
||||||
private const float fade_duration = 200;
|
private const int duration = 200;
|
||||||
|
|
||||||
private Color4 chevronIconColour;
|
private Color4 chevronIconColour;
|
||||||
|
|
||||||
@ -32,100 +30,55 @@ namespace osu.Game.Graphics.UserInterface
|
|||||||
set => text.Text = value;
|
set => text.Text = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool isLoading;
|
|
||||||
|
|
||||||
public bool IsLoading
|
|
||||||
{
|
|
||||||
get => isLoading;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
isLoading = value;
|
|
||||||
|
|
||||||
Enabled.Value = !isLoading;
|
|
||||||
|
|
||||||
if (value)
|
|
||||||
{
|
|
||||||
loading.Show();
|
|
||||||
content.FadeOut(fade_duration, Easing.OutQuint);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
loading.Hide();
|
|
||||||
content.FadeIn(fade_duration, Easing.OutQuint);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly Box background;
|
|
||||||
private readonly LoadingAnimation loading;
|
|
||||||
private readonly FillFlowContainer content;
|
|
||||||
private readonly ChevronIcon leftChevron;
|
|
||||||
private readonly ChevronIcon rightChevron;
|
|
||||||
private readonly SpriteText text;
|
|
||||||
|
|
||||||
protected override IEnumerable<Drawable> EffectTargets => new[] { background };
|
protected override IEnumerable<Drawable> EffectTargets => new[] { background };
|
||||||
|
|
||||||
|
private ChevronIcon leftChevron;
|
||||||
|
private ChevronIcon rightChevron;
|
||||||
|
private SpriteText text;
|
||||||
|
private Box background;
|
||||||
|
private FillFlowContainer textContainer;
|
||||||
|
|
||||||
public ShowMoreButton()
|
public ShowMoreButton()
|
||||||
{
|
{
|
||||||
AutoSizeAxes = Axes.Both;
|
AutoSizeAxes = Axes.Both;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Drawable CreateContent() => new CircularContainer
|
||||||
|
{
|
||||||
|
Masking = true,
|
||||||
|
Size = new Vector2(140, 30),
|
||||||
Children = new Drawable[]
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
new CircularContainer
|
background = new Box
|
||||||
{
|
{
|
||||||
Masking = true,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Size = new Vector2(140, 30),
|
},
|
||||||
|
textContainer = new FillFlowContainer
|
||||||
|
{
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
AutoSizeAxes = Axes.Both,
|
||||||
|
Direction = FillDirection.Horizontal,
|
||||||
|
Spacing = new Vector2(7),
|
||||||
Children = new Drawable[]
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
background = new Box
|
leftChevron = new ChevronIcon(),
|
||||||
{
|
text = new OsuSpriteText
|
||||||
RelativeSizeAxes = Axes.Both,
|
|
||||||
},
|
|
||||||
content = new FillFlowContainer
|
|
||||||
{
|
{
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
AutoSizeAxes = Axes.Both,
|
Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold),
|
||||||
Direction = FillDirection.Horizontal,
|
Text = "show more".ToUpper(),
|
||||||
Spacing = new Vector2(7),
|
|
||||||
Children = new Drawable[]
|
|
||||||
{
|
|
||||||
leftChevron = new ChevronIcon(),
|
|
||||||
text = new OsuSpriteText
|
|
||||||
{
|
|
||||||
Anchor = Anchor.Centre,
|
|
||||||
Origin = Anchor.Centre,
|
|
||||||
Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold),
|
|
||||||
Text = "show more".ToUpper(),
|
|
||||||
},
|
|
||||||
rightChevron = new ChevronIcon(),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
loading = new LoadingAnimation
|
|
||||||
{
|
|
||||||
Anchor = Anchor.Centre,
|
|
||||||
Origin = Anchor.Centre,
|
|
||||||
Size = new Vector2(12)
|
|
||||||
},
|
},
|
||||||
|
rightChevron = new ChevronIcon(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override bool OnClick(ClickEvent e)
|
|
||||||
{
|
|
||||||
if (!Enabled.Value)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return base.OnClick(e);
|
|
||||||
}
|
}
|
||||||
finally
|
};
|
||||||
{
|
|
||||||
// run afterwards as this will disable this button.
|
protected override void OnLoadStarted() => textContainer.FadeOut(duration, Easing.OutQuint);
|
||||||
IsLoading = true;
|
|
||||||
}
|
protected override void OnLoadFinished() => textContainer.FadeIn(duration, Easing.OutQuint);
|
||||||
}
|
|
||||||
|
|
||||||
private class ChevronIcon : SpriteIcon
|
private class ChevronIcon : SpriteIcon
|
||||||
{
|
{
|
||||||
|
@ -1,132 +1,24 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.UserInterface;
|
||||||
using osu.Framework.Graphics.Shapes;
|
|
||||||
using osu.Game.Graphics.Containers;
|
|
||||||
using osuTK;
|
|
||||||
|
|
||||||
namespace osu.Game.Graphics.UserInterfaceV2
|
namespace osu.Game.Graphics.UserInterfaceV2
|
||||||
{
|
{
|
||||||
public abstract class LabelledComponent<T> : CompositeDrawable
|
public abstract class LabelledComponent<T, U> : LabelledDrawable<T>, IHasCurrentValue<U>
|
||||||
where T : Drawable
|
where T : Drawable, IHasCurrentValue<U>
|
||||||
{
|
{
|
||||||
protected const float CONTENT_PADDING_VERTICAL = 10;
|
|
||||||
protected const float CONTENT_PADDING_HORIZONTAL = 15;
|
|
||||||
protected const float CORNER_RADIUS = 15;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The component that is being displayed.
|
|
||||||
/// </summary>
|
|
||||||
protected readonly T Component;
|
|
||||||
|
|
||||||
private readonly OsuTextFlowContainer labelText;
|
|
||||||
private readonly OsuTextFlowContainer descriptionText;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates a new <see cref="LabelledComponent{T}"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="padded">Whether the component should be padded or should be expanded to the bounds of this <see cref="LabelledComponent{T}"/>.</param>
|
|
||||||
protected LabelledComponent(bool padded)
|
protected LabelledComponent(bool padded)
|
||||||
|
: base(padded)
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.X;
|
|
||||||
AutoSizeAxes = Axes.Y;
|
|
||||||
|
|
||||||
CornerRadius = CORNER_RADIUS;
|
|
||||||
Masking = true;
|
|
||||||
|
|
||||||
InternalChildren = new Drawable[]
|
|
||||||
{
|
|
||||||
new Box
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.Both,
|
|
||||||
Colour = OsuColour.FromHex("1c2125"),
|
|
||||||
},
|
|
||||||
new FillFlowContainer
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.X,
|
|
||||||
AutoSizeAxes = Axes.Y,
|
|
||||||
Direction = FillDirection.Vertical,
|
|
||||||
Padding = padded
|
|
||||||
? new MarginPadding { Horizontal = CONTENT_PADDING_HORIZONTAL, Vertical = CONTENT_PADDING_VERTICAL }
|
|
||||||
: new MarginPadding { Left = CONTENT_PADDING_HORIZONTAL },
|
|
||||||
Spacing = new Vector2(0, 12),
|
|
||||||
Children = new Drawable[]
|
|
||||||
{
|
|
||||||
new GridContainer
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.X,
|
|
||||||
AutoSizeAxes = Axes.Y,
|
|
||||||
Content = new[]
|
|
||||||
{
|
|
||||||
new Drawable[]
|
|
||||||
{
|
|
||||||
labelText = new OsuTextFlowContainer(s => s.Font = OsuFont.GetFont(weight: FontWeight.Bold))
|
|
||||||
{
|
|
||||||
Anchor = Anchor.CentreLeft,
|
|
||||||
Origin = Anchor.CentreLeft,
|
|
||||||
AutoSizeAxes = Axes.Both,
|
|
||||||
Padding = new MarginPadding { Right = 20 }
|
|
||||||
},
|
|
||||||
new Container
|
|
||||||
{
|
|
||||||
Anchor = Anchor.CentreRight,
|
|
||||||
Origin = Anchor.CentreRight,
|
|
||||||
RelativeSizeAxes = Axes.X,
|
|
||||||
AutoSizeAxes = Axes.Y,
|
|
||||||
Child = Component = CreateComponent().With(d =>
|
|
||||||
{
|
|
||||||
d.Anchor = Anchor.CentreRight;
|
|
||||||
d.Origin = Anchor.CentreRight;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) },
|
|
||||||
ColumnDimensions = new[] { new Dimension(GridSizeMode.AutoSize) }
|
|
||||||
},
|
|
||||||
descriptionText = new OsuTextFlowContainer(s => s.Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold, italics: true))
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.X,
|
|
||||||
AutoSizeAxes = Axes.Y,
|
|
||||||
Padding = new MarginPadding { Bottom = padded ? 0 : CONTENT_PADDING_VERTICAL },
|
|
||||||
Alpha = 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
public Bindable<U> Current
|
||||||
private void load(OsuColour osuColour)
|
|
||||||
{
|
{
|
||||||
descriptionText.Colour = osuColour.Yellow;
|
get => Component.Current;
|
||||||
|
set => Component.Current = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string Label
|
|
||||||
{
|
|
||||||
set => labelText.Text = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Description
|
|
||||||
{
|
|
||||||
set
|
|
||||||
{
|
|
||||||
descriptionText.Text = value;
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(value))
|
|
||||||
descriptionText.Show();
|
|
||||||
else
|
|
||||||
descriptionText.Hide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates the component that should be displayed.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>The component.</returns>
|
|
||||||
protected abstract T CreateComponent();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
132
osu.Game/Graphics/UserInterfaceV2/LabelledDrawable.cs
Normal file
132
osu.Game/Graphics/UserInterfaceV2/LabelledDrawable.cs
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Shapes;
|
||||||
|
using osu.Game.Graphics.Containers;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
|
namespace osu.Game.Graphics.UserInterfaceV2
|
||||||
|
{
|
||||||
|
public abstract class LabelledDrawable<T> : CompositeDrawable
|
||||||
|
where T : Drawable
|
||||||
|
{
|
||||||
|
protected const float CONTENT_PADDING_VERTICAL = 10;
|
||||||
|
protected const float CONTENT_PADDING_HORIZONTAL = 15;
|
||||||
|
protected const float CORNER_RADIUS = 15;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The component that is being displayed.
|
||||||
|
/// </summary>
|
||||||
|
protected readonly T Component;
|
||||||
|
|
||||||
|
private readonly OsuTextFlowContainer labelText;
|
||||||
|
private readonly OsuTextFlowContainer descriptionText;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new <see cref="LabelledComponent{T, U}"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="padded">Whether the component should be padded or should be expanded to the bounds of this <see cref="LabelledComponent{T, U}"/>.</param>
|
||||||
|
protected LabelledDrawable(bool padded)
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.X;
|
||||||
|
AutoSizeAxes = Axes.Y;
|
||||||
|
|
||||||
|
CornerRadius = CORNER_RADIUS;
|
||||||
|
Masking = true;
|
||||||
|
|
||||||
|
InternalChildren = new Drawable[]
|
||||||
|
{
|
||||||
|
new Box
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Colour = OsuColour.FromHex("1c2125"),
|
||||||
|
},
|
||||||
|
new FillFlowContainer
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
AutoSizeAxes = Axes.Y,
|
||||||
|
Direction = FillDirection.Vertical,
|
||||||
|
Padding = padded
|
||||||
|
? new MarginPadding { Horizontal = CONTENT_PADDING_HORIZONTAL, Vertical = CONTENT_PADDING_VERTICAL }
|
||||||
|
: new MarginPadding { Left = CONTENT_PADDING_HORIZONTAL },
|
||||||
|
Spacing = new Vector2(0, 12),
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
new GridContainer
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
AutoSizeAxes = Axes.Y,
|
||||||
|
Content = new[]
|
||||||
|
{
|
||||||
|
new Drawable[]
|
||||||
|
{
|
||||||
|
labelText = new OsuTextFlowContainer(s => s.Font = OsuFont.GetFont(weight: FontWeight.Bold))
|
||||||
|
{
|
||||||
|
Anchor = Anchor.CentreLeft,
|
||||||
|
Origin = Anchor.CentreLeft,
|
||||||
|
AutoSizeAxes = Axes.Both,
|
||||||
|
Padding = new MarginPadding { Right = 20 }
|
||||||
|
},
|
||||||
|
new Container
|
||||||
|
{
|
||||||
|
Anchor = Anchor.CentreRight,
|
||||||
|
Origin = Anchor.CentreRight,
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
AutoSizeAxes = Axes.Y,
|
||||||
|
Child = Component = CreateComponent().With(d =>
|
||||||
|
{
|
||||||
|
d.Anchor = Anchor.CentreRight;
|
||||||
|
d.Origin = Anchor.CentreRight;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) },
|
||||||
|
ColumnDimensions = new[] { new Dimension(GridSizeMode.AutoSize) }
|
||||||
|
},
|
||||||
|
descriptionText = new OsuTextFlowContainer(s => s.Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold, italics: true))
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
AutoSizeAxes = Axes.Y,
|
||||||
|
Padding = new MarginPadding { Bottom = padded ? 0 : CONTENT_PADDING_VERTICAL },
|
||||||
|
Alpha = 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load(OsuColour osuColour)
|
||||||
|
{
|
||||||
|
descriptionText.Colour = osuColour.Yellow;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Label
|
||||||
|
{
|
||||||
|
set => labelText.Text = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Description
|
||||||
|
{
|
||||||
|
set
|
||||||
|
{
|
||||||
|
descriptionText.Text = value;
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(value))
|
||||||
|
descriptionText.Show();
|
||||||
|
else
|
||||||
|
descriptionText.Hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates the component that should be displayed.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The component.</returns>
|
||||||
|
protected abstract T CreateComponent();
|
||||||
|
}
|
||||||
|
}
|
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
namespace osu.Game.Graphics.UserInterfaceV2
|
namespace osu.Game.Graphics.UserInterfaceV2
|
||||||
{
|
{
|
||||||
public class LabelledSwitchButton : LabelledComponent<SwitchButton>
|
public class LabelledSwitchButton : LabelledComponent<SwitchButton, bool>
|
||||||
{
|
{
|
||||||
public LabelledSwitchButton()
|
public LabelledSwitchButton()
|
||||||
: base(true)
|
: base(true)
|
||||||
|
@ -8,7 +8,7 @@ using osu.Game.Graphics.UserInterface;
|
|||||||
|
|
||||||
namespace osu.Game.Graphics.UserInterfaceV2
|
namespace osu.Game.Graphics.UserInterfaceV2
|
||||||
{
|
{
|
||||||
public class LabelledTextBox : LabelledComponent<OsuTextBox>
|
public class LabelledTextBox : LabelledComponent<OsuTextBox, string>
|
||||||
{
|
{
|
||||||
public event TextBox.OnCommitHandler OnCommit;
|
public event TextBox.OnCommitHandler OnCommit;
|
||||||
|
|
||||||
|
@ -1,30 +1,16 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using osu.Framework.IO.Stores;
|
||||||
using SharpCompress.Archives.Zip;
|
using SharpCompress.Archives.Zip;
|
||||||
using SharpCompress.Common;
|
|
||||||
|
|
||||||
namespace osu.Game.IO.Archives
|
namespace osu.Game.IO.Archives
|
||||||
{
|
{
|
||||||
public sealed class ZipArchiveReader : ArchiveReader
|
public sealed class ZipArchiveReader : ArchiveReader
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// List of substrings that indicate a file should be ignored during the import process
|
|
||||||
/// (usually due to representing no useful data and being autogenerated by the OS).
|
|
||||||
/// </summary>
|
|
||||||
private static readonly string[] filename_ignore_list =
|
|
||||||
{
|
|
||||||
// Mac-specific
|
|
||||||
"__MACOSX",
|
|
||||||
".DS_Store",
|
|
||||||
// Windows-specific
|
|
||||||
"Thumbs.db"
|
|
||||||
};
|
|
||||||
|
|
||||||
private readonly Stream archiveStream;
|
private readonly Stream archiveStream;
|
||||||
private readonly ZipArchive archive;
|
private readonly ZipArchive archive;
|
||||||
|
|
||||||
@ -58,9 +44,7 @@ namespace osu.Game.IO.Archives
|
|||||||
archiveStream.Dispose();
|
archiveStream.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool canBeIgnored(IEntry entry) => filename_ignore_list.Any(ignoredName => entry.Key.IndexOf(ignoredName, StringComparison.OrdinalIgnoreCase) >= 0);
|
public override IEnumerable<string> Filenames => archive.Entries.Select(e => e.Key).ExcludeSystemFileNames();
|
||||||
|
|
||||||
public override IEnumerable<string> Filenames => archive.Entries.Where(e => !canBeIgnored(e)).Select(e => e.Key).ToArray();
|
|
||||||
|
|
||||||
public override Stream GetUnderlyingStream() => archiveStream;
|
public override Stream GetUnderlyingStream() => archiveStream;
|
||||||
}
|
}
|
||||||
|
36
osu.Game/Online/API/Requests/CommentVoteRequest.cs
Normal file
36
osu.Game/Online/API/Requests/CommentVoteRequest.cs
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using osu.Framework.IO.Network;
|
||||||
|
using osu.Game.Online.API.Requests.Responses;
|
||||||
|
using System.Net.Http;
|
||||||
|
|
||||||
|
namespace osu.Game.Online.API.Requests
|
||||||
|
{
|
||||||
|
public class CommentVoteRequest : APIRequest<CommentBundle>
|
||||||
|
{
|
||||||
|
private readonly long id;
|
||||||
|
private readonly CommentVoteAction action;
|
||||||
|
|
||||||
|
public CommentVoteRequest(long id, CommentVoteAction action)
|
||||||
|
{
|
||||||
|
this.id = id;
|
||||||
|
this.action = action;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override WebRequest CreateWebRequest()
|
||||||
|
{
|
||||||
|
var req = base.CreateWebRequest();
|
||||||
|
req.Method = action == CommentVoteAction.Vote ? HttpMethod.Post : HttpMethod.Delete;
|
||||||
|
return req;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override string Target => $@"comments/{id}/vote";
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum CommentVoteAction
|
||||||
|
{
|
||||||
|
Vote,
|
||||||
|
UnVote
|
||||||
|
}
|
||||||
|
}
|
@ -72,6 +72,8 @@ namespace osu.Game.Online.API.Requests.Responses
|
|||||||
|
|
||||||
public bool HasMessage => !string.IsNullOrEmpty(MessageHtml);
|
public bool HasMessage => !string.IsNullOrEmpty(MessageHtml);
|
||||||
|
|
||||||
|
public bool IsVoted { get; set; }
|
||||||
|
|
||||||
public string GetMessage => HasMessage ? WebUtility.HtmlDecode(Regex.Replace(MessageHtml, @"<(.|\n)*?>", string.Empty)) : string.Empty;
|
public string GetMessage => HasMessage ? WebUtility.HtmlDecode(Regex.Replace(MessageHtml, @"<(.|\n)*?>", string.Empty)) : string.Empty;
|
||||||
|
|
||||||
public int DeletedChildrenCount => ChildComments.Count(c => c.IsDeleted);
|
public int DeletedChildrenCount => ChildComments.Count(c => c.IsDeleted);
|
||||||
|
@ -47,6 +47,22 @@ namespace osu.Game.Online.API.Requests.Responses
|
|||||||
[JsonProperty(@"included_comments")]
|
[JsonProperty(@"included_comments")]
|
||||||
public List<Comment> IncludedComments { get; set; }
|
public List<Comment> IncludedComments { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty(@"user_votes")]
|
||||||
|
private List<long> userVotes
|
||||||
|
{
|
||||||
|
set
|
||||||
|
{
|
||||||
|
value.ForEach(v =>
|
||||||
|
{
|
||||||
|
Comments.ForEach(c =>
|
||||||
|
{
|
||||||
|
if (v == c.Id)
|
||||||
|
c.IsVoted = true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private List<User> users;
|
private List<User> users;
|
||||||
|
|
||||||
[JsonProperty(@"users")]
|
[JsonProperty(@"users")]
|
||||||
|
@ -14,7 +14,7 @@ namespace osu.Game.Online.Chat
|
|||||||
{
|
{
|
||||||
public class Channel
|
public class Channel
|
||||||
{
|
{
|
||||||
public readonly int MaxHistory = 300;
|
public const int MAX_HISTORY = 300;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Contains every joined user except the current logged in user. Currently only returned for PM channels.
|
/// Contains every joined user except the current logged in user. Currently only returned for PM channels.
|
||||||
@ -80,8 +80,6 @@ namespace osu.Game.Online.Chat
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public Bindable<bool> Joined = new Bindable<bool>();
|
public Bindable<bool> Joined = new Bindable<bool>();
|
||||||
|
|
||||||
public const int MAX_HISTORY = 300;
|
|
||||||
|
|
||||||
[JsonConstructor]
|
[JsonConstructor]
|
||||||
public Channel()
|
public Channel()
|
||||||
{
|
{
|
||||||
@ -162,8 +160,8 @@ namespace osu.Game.Online.Chat
|
|||||||
{
|
{
|
||||||
// never purge local echos
|
// never purge local echos
|
||||||
int messageCount = Messages.Count - pendingMessages.Count;
|
int messageCount = Messages.Count - pendingMessages.Count;
|
||||||
if (messageCount > MaxHistory)
|
if (messageCount > MAX_HISTORY)
|
||||||
Messages.RemoveRange(0, messageCount - MaxHistory);
|
Messages.RemoveRange(0, messageCount - MAX_HISTORY);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,7 @@ namespace osu.Game.Online.Chat
|
|||||||
private static readonly Regex new_link_regex = new Regex(@"\[(?<url>[a-z]+://[^ ]+) (?<text>(((?<=\\)[\[\]])|[^\[\]])*(((?<open>\[)(((?<=\\)[\[\]])|[^\[\]])*)+((?<close-open>\])(((?<=\\)[\[\]])|[^\[\]])*)+)*(?(open)(?!)))\]");
|
private static readonly Regex new_link_regex = new Regex(@"\[(?<url>[a-z]+://[^ ]+) (?<text>(((?<=\\)[\[\]])|[^\[\]])*(((?<open>\[)(((?<=\\)[\[\]])|[^\[\]])*)+((?<close-open>\])(((?<=\\)[\[\]])|[^\[\]])*)+)*(?(open)(?!)))\]");
|
||||||
|
|
||||||
// [test](https://osu.ppy.sh/b/1234) -> test (https://osu.ppy.sh/b/1234) aka correct markdown format
|
// [test](https://osu.ppy.sh/b/1234) -> test (https://osu.ppy.sh/b/1234) aka correct markdown format
|
||||||
private static readonly Regex markdown_link_regex = new Regex(@"\[(?<text>(((?<=\\)[\[\]])|[^\[\]])*(((?<open>\[)(((?<=\\)[\[\]])|[^\[\]])*)+((?<close-open>\])(((?<=\\)[\[\]])|[^\[\]])*)+)*(?(open)(?!)))\]\((?<url>[a-z]+://[^ ]+)\)");
|
private static readonly Regex markdown_link_regex = new Regex(@"\[(?<text>(((?<=\\)[\[\]])|[^\[\]])*(((?<open>\[)(((?<=\\)[\[\]])|[^\[\]])*)+((?<close-open>\])(((?<=\\)[\[\]])|[^\[\]])*)+)*(?(open)(?!)))\]\((?<url>[a-z]+://[^ ]+)(\s+(?<title>""([^""]|(?<=\\)"")*""))?\)");
|
||||||
|
|
||||||
// advanced, RFC-compatible regular expression that matches any possible URL, *but* allows certain invalid characters that are widely used
|
// advanced, RFC-compatible regular expression that matches any possible URL, *but* allows certain invalid characters that are widely used
|
||||||
// This is in the format (<required>, [optional]):
|
// This is in the format (<required>, [optional]):
|
||||||
@ -81,7 +81,7 @@ namespace osu.Game.Online.Chat
|
|||||||
//since we just changed the line display text, offset any already processed links.
|
//since we just changed the line display text, offset any already processed links.
|
||||||
result.Links.ForEach(l => l.Index -= l.Index > index ? m.Length - displayText.Length : 0);
|
result.Links.ForEach(l => l.Index -= l.Index > index ? m.Length - displayText.Length : 0);
|
||||||
|
|
||||||
var details = getLinkDetails(linkText);
|
var details = GetLinkDetails(linkText);
|
||||||
result.Links.Add(new Link(linkText, index, displayText.Length, linkActionOverride ?? details.Action, details.Argument));
|
result.Links.Add(new Link(linkText, index, displayText.Length, linkActionOverride ?? details.Action, details.Argument));
|
||||||
|
|
||||||
//adjust the offset for processing the current matches group.
|
//adjust the offset for processing the current matches group.
|
||||||
@ -95,15 +95,21 @@ namespace osu.Game.Online.Chat
|
|||||||
foreach (Match m in regex.Matches(result.Text, startIndex))
|
foreach (Match m in regex.Matches(result.Text, startIndex))
|
||||||
{
|
{
|
||||||
var index = m.Index;
|
var index = m.Index;
|
||||||
var link = m.Groups["link"].Value;
|
var linkText = m.Groups["link"].Value;
|
||||||
var indexLength = link.Length;
|
var indexLength = linkText.Length;
|
||||||
|
|
||||||
var details = getLinkDetails(link);
|
var details = GetLinkDetails(linkText);
|
||||||
result.Links.Add(new Link(link, index, indexLength, details.Action, details.Argument));
|
var link = new Link(linkText, index, indexLength, details.Action, details.Argument);
|
||||||
|
|
||||||
|
// sometimes an already-processed formatted link can reduce to a simple URL, too
|
||||||
|
// (example: [mean example - https://osu.ppy.sh](https://osu.ppy.sh))
|
||||||
|
// therefore we need to check if any of the pre-existing links contains the raw one we found
|
||||||
|
if (result.Links.All(existingLink => !existingLink.Overlaps(link)))
|
||||||
|
result.Links.Add(link);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static LinkDetails getLinkDetails(string url)
|
public static LinkDetails GetLinkDetails(string url)
|
||||||
{
|
{
|
||||||
var args = url.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
|
var args = url.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
|
||||||
args[0] = args[0].TrimEnd(':');
|
args[0] = args[0].TrimEnd(':');
|
||||||
@ -249,17 +255,17 @@ namespace osu.Game.Online.Chat
|
|||||||
OriginalText = Text = text;
|
OriginalText = Text = text;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public class LinkDetails
|
public class LinkDetails
|
||||||
|
{
|
||||||
|
public LinkAction Action;
|
||||||
|
public string Argument;
|
||||||
|
|
||||||
|
public LinkDetails(LinkAction action, string argument)
|
||||||
{
|
{
|
||||||
public LinkAction Action;
|
Action = action;
|
||||||
public string Argument;
|
Argument = argument;
|
||||||
|
|
||||||
public LinkDetails(LinkAction action, string argument)
|
|
||||||
{
|
|
||||||
Action = action;
|
|
||||||
Argument = argument;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -273,6 +279,7 @@ namespace osu.Game.Online.Chat
|
|||||||
JoinMultiplayerMatch,
|
JoinMultiplayerMatch,
|
||||||
Spectate,
|
Spectate,
|
||||||
OpenUserProfile,
|
OpenUserProfile,
|
||||||
|
Custom
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Link : IComparable<Link>
|
public class Link : IComparable<Link>
|
||||||
@ -292,6 +299,8 @@ namespace osu.Game.Online.Chat
|
|||||||
Argument = argument;
|
Argument = argument;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool Overlaps(Link otherLink) => Index < otherLink.Index + otherLink.Length && otherLink.Index < Index + Length;
|
||||||
|
|
||||||
public int CompareTo(Link otherLink) => Index > otherLink.Index ? 1 : -1;
|
public int CompareTo(Link otherLink) => Index > otherLink.Index ? 1 : -1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,7 @@ using osu.Game.Users.Drawables;
|
|||||||
using osuTK;
|
using osuTK;
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
using Humanizer;
|
using Humanizer;
|
||||||
|
using osu.Game.Online.API;
|
||||||
|
|
||||||
namespace osu.Game.Online.Leaderboards
|
namespace osu.Game.Online.Leaderboards
|
||||||
{
|
{
|
||||||
@ -37,6 +38,7 @@ namespace osu.Game.Online.Leaderboards
|
|||||||
|
|
||||||
private readonly ScoreInfo score;
|
private readonly ScoreInfo score;
|
||||||
private readonly int rank;
|
private readonly int rank;
|
||||||
|
private readonly bool allowHighlight;
|
||||||
|
|
||||||
private Box background;
|
private Box background;
|
||||||
private Container content;
|
private Container content;
|
||||||
@ -49,17 +51,18 @@ namespace osu.Game.Online.Leaderboards
|
|||||||
|
|
||||||
private List<ScoreComponentLabel> statisticsLabels;
|
private List<ScoreComponentLabel> statisticsLabels;
|
||||||
|
|
||||||
public LeaderboardScore(ScoreInfo score, int rank)
|
public LeaderboardScore(ScoreInfo score, int rank, bool allowHighlight = true)
|
||||||
{
|
{
|
||||||
this.score = score;
|
this.score = score;
|
||||||
this.rank = rank;
|
this.rank = rank;
|
||||||
|
this.allowHighlight = allowHighlight;
|
||||||
|
|
||||||
RelativeSizeAxes = Axes.X;
|
RelativeSizeAxes = Axes.X;
|
||||||
Height = HEIGHT;
|
Height = HEIGHT;
|
||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load()
|
private void load(IAPIProvider api, OsuColour colour)
|
||||||
{
|
{
|
||||||
var user = score.User;
|
var user = score.User;
|
||||||
|
|
||||||
@ -100,7 +103,7 @@ namespace osu.Game.Online.Leaderboards
|
|||||||
background = new Box
|
background = new Box
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Colour = Color4.Black,
|
Colour = user.Id == api.LocalUser.Value.Id && allowHighlight ? colour.Green : Color4.Black,
|
||||||
Alpha = background_alpha,
|
Alpha = background_alpha,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -215,31 +215,102 @@ namespace osu.Game
|
|||||||
|
|
||||||
private ExternalLinkOpener externalLinkOpener;
|
private ExternalLinkOpener externalLinkOpener;
|
||||||
|
|
||||||
public void OpenUrlExternally(string url)
|
/// <summary>
|
||||||
|
/// Handle an arbitrary URL. Displays via in-game overlays where possible.
|
||||||
|
/// This can be called from a non-thread-safe non-game-loaded state.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="url">The URL to load.</param>
|
||||||
|
public void HandleLink(string url) => HandleLink(MessageFormatter.GetLinkDetails(url));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handle a specific <see cref="LinkDetails"/>.
|
||||||
|
/// This can be called from a non-thread-safe non-game-loaded state.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="link">The link to load.</param>
|
||||||
|
public void HandleLink(LinkDetails link) => Schedule(() =>
|
||||||
|
{
|
||||||
|
switch (link.Action)
|
||||||
|
{
|
||||||
|
case LinkAction.OpenBeatmap:
|
||||||
|
// TODO: proper query params handling
|
||||||
|
if (link.Argument != null && int.TryParse(link.Argument.Contains('?') ? link.Argument.Split('?')[0] : link.Argument, out int beatmapId))
|
||||||
|
ShowBeatmap(beatmapId);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case LinkAction.OpenBeatmapSet:
|
||||||
|
if (int.TryParse(link.Argument, out int setId))
|
||||||
|
ShowBeatmapSet(setId);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case LinkAction.OpenChannel:
|
||||||
|
ShowChannel(link.Argument);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case LinkAction.OpenEditorTimestamp:
|
||||||
|
case LinkAction.JoinMultiplayerMatch:
|
||||||
|
case LinkAction.Spectate:
|
||||||
|
waitForReady(() => notifications, _ => notifications?.Post(new SimpleNotification
|
||||||
|
{
|
||||||
|
Text = @"This link type is not yet supported!",
|
||||||
|
Icon = FontAwesome.Solid.LifeRing,
|
||||||
|
}));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case LinkAction.External:
|
||||||
|
OpenUrlExternally(link.Argument);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case LinkAction.OpenUserProfile:
|
||||||
|
if (long.TryParse(link.Argument, out long userId))
|
||||||
|
ShowUser(userId);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new NotImplementedException($"This {nameof(LinkAction)} ({link.Action.ToString()}) is missing an associated action.");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
public void OpenUrlExternally(string url) => waitForReady(() => externalLinkOpener, _ =>
|
||||||
{
|
{
|
||||||
if (url.StartsWith("/"))
|
if (url.StartsWith("/"))
|
||||||
url = $"{API.Endpoint}{url}";
|
url = $"{API.Endpoint}{url}";
|
||||||
|
|
||||||
externalLinkOpener.OpenUrlExternally(url);
|
externalLinkOpener.OpenUrlExternally(url);
|
||||||
}
|
});
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Open a specific channel in chat.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="channel">The channel to display.</param>
|
||||||
|
public void ShowChannel(string channel) => waitForReady(() => channelManager, _ =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
channelManager.OpenChannel(channel);
|
||||||
|
}
|
||||||
|
catch (ChannelNotFoundException)
|
||||||
|
{
|
||||||
|
Logger.Log($"The requested channel \"{channel}\" does not exist");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Show a beatmap set as an overlay.
|
/// Show a beatmap set as an overlay.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="setId">The set to display.</param>
|
/// <param name="setId">The set to display.</param>
|
||||||
public void ShowBeatmapSet(int setId) => beatmapSetOverlay.FetchAndShowBeatmapSet(setId);
|
public void ShowBeatmapSet(int setId) => waitForReady(() => beatmapSetOverlay, _ => beatmapSetOverlay.FetchAndShowBeatmapSet(setId));
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Show a user's profile as an overlay.
|
/// Show a user's profile as an overlay.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="userId">The user to display.</param>
|
/// <param name="userId">The user to display.</param>
|
||||||
public void ShowUser(long userId) => userProfile.ShowUser(userId);
|
public void ShowUser(long userId) => waitForReady(() => userProfile, _ => userProfile.ShowUser(userId));
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Show a beatmap's set as an overlay, displaying the given beatmap.
|
/// Show a beatmap's set as an overlay, displaying the given beatmap.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="beatmapId">The beatmap to show.</param>
|
/// <param name="beatmapId">The beatmap to show.</param>
|
||||||
public void ShowBeatmap(int beatmapId) => beatmapSetOverlay.FetchAndShowBeatmap(beatmapId);
|
public void ShowBeatmap(int beatmapId) => waitForReady(() => beatmapSetOverlay, _ => beatmapSetOverlay.FetchAndShowBeatmap(beatmapId));
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Present a beatmap at song select immediately.
|
/// Present a beatmap at song select immediately.
|
||||||
@ -397,6 +468,23 @@ namespace osu.Game
|
|||||||
performFromMainMenuTask = Schedule(() => performFromMainMenu(action, taskName));
|
performFromMainMenuTask = Schedule(() => performFromMainMenu(action, taskName));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Wait for the game (and target component) to become loaded and then run an action.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="retrieveInstance">A function to retrieve a (potentially not-yet-constructed) target instance.</param>
|
||||||
|
/// <param name="action">The action to perform on the instance when load is confirmed.</param>
|
||||||
|
/// <typeparam name="T">The type of the target instance.</typeparam>
|
||||||
|
private void waitForReady<T>(Func<T> retrieveInstance, Action<T> action)
|
||||||
|
where T : Drawable
|
||||||
|
{
|
||||||
|
var instance = retrieveInstance();
|
||||||
|
|
||||||
|
if (ScreenStack == null || ScreenStack.CurrentScreen is StartupScreen || instance?.IsLoaded != true)
|
||||||
|
Schedule(() => waitForReady(retrieveInstance, action));
|
||||||
|
else
|
||||||
|
action(instance);
|
||||||
|
}
|
||||||
|
|
||||||
protected override void Dispose(bool isDisposing)
|
protected override void Dispose(bool isDisposing)
|
||||||
{
|
{
|
||||||
base.Dispose(isDisposing);
|
base.Dispose(isDisposing);
|
||||||
|
@ -17,6 +17,7 @@ using osu.Game.Beatmaps.Drawables;
|
|||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Graphics.Containers;
|
using osu.Game.Graphics.Containers;
|
||||||
using osu.Game.Graphics.Sprites;
|
using osu.Game.Graphics.Sprites;
|
||||||
|
using osu.Game.Rulesets;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
|
|
||||||
@ -27,10 +28,11 @@ namespace osu.Game.Overlays.BeatmapSet
|
|||||||
private const float tile_icon_padding = 7;
|
private const float tile_icon_padding = 7;
|
||||||
private const float tile_spacing = 2;
|
private const float tile_spacing = 2;
|
||||||
|
|
||||||
private readonly DifficultiesContainer difficulties;
|
|
||||||
private readonly OsuSpriteText version, starRating;
|
private readonly OsuSpriteText version, starRating;
|
||||||
private readonly Statistic plays, favourites;
|
private readonly Statistic plays, favourites;
|
||||||
|
|
||||||
|
public readonly DifficultiesContainer Difficulties;
|
||||||
|
|
||||||
public readonly Bindable<BeatmapInfo> Beatmap = new Bindable<BeatmapInfo>();
|
public readonly Bindable<BeatmapInfo> Beatmap = new Bindable<BeatmapInfo>();
|
||||||
|
|
||||||
private BeatmapSetInfo beatmapSet;
|
private BeatmapSetInfo beatmapSet;
|
||||||
@ -43,38 +45,10 @@ namespace osu.Game.Overlays.BeatmapSet
|
|||||||
if (value == beatmapSet) return;
|
if (value == beatmapSet) return;
|
||||||
|
|
||||||
beatmapSet = value;
|
beatmapSet = value;
|
||||||
|
|
||||||
updateDisplay();
|
updateDisplay();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateDisplay()
|
|
||||||
{
|
|
||||||
difficulties.Clear();
|
|
||||||
|
|
||||||
if (BeatmapSet != null)
|
|
||||||
{
|
|
||||||
difficulties.ChildrenEnumerable = BeatmapSet.Beatmaps.OrderBy(beatmap => beatmap.StarDifficulty).Select(b => new DifficultySelectorButton(b)
|
|
||||||
{
|
|
||||||
State = DifficultySelectorState.NotSelected,
|
|
||||||
OnHovered = beatmap =>
|
|
||||||
{
|
|
||||||
showBeatmap(beatmap);
|
|
||||||
starRating.Text = beatmap.StarDifficulty.ToString("Star Difficulty 0.##");
|
|
||||||
starRating.FadeIn(100);
|
|
||||||
},
|
|
||||||
OnClicked = beatmap => { Beatmap.Value = beatmap; },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
starRating.FadeOut(100);
|
|
||||||
Beatmap.Value = BeatmapSet?.Beatmaps.FirstOrDefault();
|
|
||||||
plays.Value = BeatmapSet?.OnlineInfo.PlayCount ?? 0;
|
|
||||||
favourites.Value = BeatmapSet?.OnlineInfo.FavouriteCount ?? 0;
|
|
||||||
|
|
||||||
updateDifficultyButtons();
|
|
||||||
}
|
|
||||||
|
|
||||||
public BeatmapPicker()
|
public BeatmapPicker()
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.X;
|
RelativeSizeAxes = Axes.X;
|
||||||
@ -89,7 +63,7 @@ namespace osu.Game.Overlays.BeatmapSet
|
|||||||
Direction = FillDirection.Vertical,
|
Direction = FillDirection.Vertical,
|
||||||
Children = new Drawable[]
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
difficulties = new DifficultiesContainer
|
Difficulties = new DifficultiesContainer
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.X,
|
RelativeSizeAxes = Axes.X,
|
||||||
AutoSizeAxes = Axes.Y,
|
AutoSizeAxes = Axes.Y,
|
||||||
@ -147,6 +121,9 @@ namespace osu.Game.Overlays.BeatmapSet
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private IBindable<RulesetInfo> ruleset { get; set; }
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(OsuColour colours)
|
private void load(OsuColour colours)
|
||||||
{
|
{
|
||||||
@ -158,10 +135,39 @@ namespace osu.Game.Overlays.BeatmapSet
|
|||||||
{
|
{
|
||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
|
|
||||||
|
ruleset.ValueChanged += r => updateDisplay();
|
||||||
|
|
||||||
// done here so everything can bind in intialization and get the first trigger
|
// done here so everything can bind in intialization and get the first trigger
|
||||||
Beatmap.TriggerChange();
|
Beatmap.TriggerChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void updateDisplay()
|
||||||
|
{
|
||||||
|
Difficulties.Clear();
|
||||||
|
|
||||||
|
if (BeatmapSet != null)
|
||||||
|
{
|
||||||
|
Difficulties.ChildrenEnumerable = BeatmapSet.Beatmaps.Where(b => b.Ruleset.Equals(ruleset.Value)).OrderBy(b => b.StarDifficulty).Select(b => new DifficultySelectorButton(b)
|
||||||
|
{
|
||||||
|
State = DifficultySelectorState.NotSelected,
|
||||||
|
OnHovered = beatmap =>
|
||||||
|
{
|
||||||
|
showBeatmap(beatmap);
|
||||||
|
starRating.Text = beatmap.StarDifficulty.ToString("Star Difficulty 0.##");
|
||||||
|
starRating.FadeIn(100);
|
||||||
|
},
|
||||||
|
OnClicked = beatmap => { Beatmap.Value = beatmap; },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
starRating.FadeOut(100);
|
||||||
|
Beatmap.Value = Difficulties.FirstOrDefault()?.Beatmap;
|
||||||
|
plays.Value = BeatmapSet?.OnlineInfo.PlayCount ?? 0;
|
||||||
|
favourites.Value = BeatmapSet?.OnlineInfo.FavouriteCount ?? 0;
|
||||||
|
|
||||||
|
updateDifficultyButtons();
|
||||||
|
}
|
||||||
|
|
||||||
private void showBeatmap(BeatmapInfo beatmap)
|
private void showBeatmap(BeatmapInfo beatmap)
|
||||||
{
|
{
|
||||||
version.Text = beatmap?.Version;
|
version.Text = beatmap?.Version;
|
||||||
@ -169,10 +175,10 @@ namespace osu.Game.Overlays.BeatmapSet
|
|||||||
|
|
||||||
private void updateDifficultyButtons()
|
private void updateDifficultyButtons()
|
||||||
{
|
{
|
||||||
difficulties.Children.ToList().ForEach(diff => diff.State = diff.Beatmap == Beatmap.Value ? DifficultySelectorState.Selected : DifficultySelectorState.NotSelected);
|
Difficulties.Children.ToList().ForEach(diff => diff.State = diff.Beatmap == Beatmap.Value ? DifficultySelectorState.Selected : DifficultySelectorState.NotSelected);
|
||||||
}
|
}
|
||||||
|
|
||||||
private class DifficultiesContainer : FillFlowContainer<DifficultySelectorButton>
|
public class DifficultiesContainer : FillFlowContainer<DifficultySelectorButton>
|
||||||
{
|
{
|
||||||
public Action OnLostHover;
|
public Action OnLostHover;
|
||||||
|
|
||||||
@ -183,7 +189,7 @@ namespace osu.Game.Overlays.BeatmapSet
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class DifficultySelectorButton : OsuClickableContainer, IStateful<DifficultySelectorState>
|
public class DifficultySelectorButton : OsuClickableContainer, IStateful<DifficultySelectorState>
|
||||||
{
|
{
|
||||||
private const float transition_duration = 100;
|
private const float transition_duration = 100;
|
||||||
private const float size = 52;
|
private const float size = 52;
|
||||||
@ -320,7 +326,7 @@ namespace osu.Game.Overlays.BeatmapSet
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private enum DifficultySelectorState
|
public enum DifficultySelectorState
|
||||||
{
|
{
|
||||||
Selected,
|
Selected,
|
||||||
NotSelected,
|
NotSelected,
|
||||||
|
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 System.Linq;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Extensions.Color4Extensions;
|
using osu.Framework.Extensions.Color4Extensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Colour;
|
using osu.Framework.Graphics.Colour;
|
||||||
@ -16,6 +17,7 @@ using osu.Game.Graphics.UserInterface;
|
|||||||
using osu.Game.Online;
|
using osu.Game.Online;
|
||||||
using osu.Game.Overlays.BeatmapSet.Buttons;
|
using osu.Game.Overlays.BeatmapSet.Buttons;
|
||||||
using osu.Game.Overlays.Direct;
|
using osu.Game.Overlays.Direct;
|
||||||
|
using osu.Game.Rulesets;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
|
|
||||||
@ -39,6 +41,7 @@ namespace osu.Game.Overlays.BeatmapSet
|
|||||||
|
|
||||||
public bool DownloadButtonsVisible => downloadButtonsContainer.Any();
|
public bool DownloadButtonsVisible => downloadButtonsContainer.Any();
|
||||||
|
|
||||||
|
public readonly BeatmapRulesetSelector RulesetSelector;
|
||||||
public readonly BeatmapPicker Picker;
|
public readonly BeatmapPicker Picker;
|
||||||
|
|
||||||
private readonly FavouriteButton favouriteButton;
|
private readonly FavouriteButton favouriteButton;
|
||||||
@ -47,6 +50,9 @@ namespace osu.Game.Overlays.BeatmapSet
|
|||||||
|
|
||||||
private readonly LoadingAnimation loading;
|
private readonly LoadingAnimation loading;
|
||||||
|
|
||||||
|
[Cached(typeof(IBindable<RulesetInfo>))]
|
||||||
|
private readonly Bindable<RulesetInfo> ruleset = new Bindable<RulesetInfo>();
|
||||||
|
|
||||||
public Header()
|
public Header()
|
||||||
{
|
{
|
||||||
ExternalLinkButton externalLink;
|
ExternalLinkButton externalLink;
|
||||||
@ -69,12 +75,18 @@ namespace osu.Game.Overlays.BeatmapSet
|
|||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.X,
|
RelativeSizeAxes = Axes.X,
|
||||||
Height = tabs_height,
|
Height = tabs_height,
|
||||||
Children = new[]
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
tabsBg = new Box
|
tabsBg = new Box
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
},
|
},
|
||||||
|
RulesetSelector = new BeatmapRulesetSelector
|
||||||
|
{
|
||||||
|
Current = ruleset,
|
||||||
|
Anchor = Anchor.BottomCentre,
|
||||||
|
Origin = Anchor.BottomCentre,
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
new Container
|
new Container
|
||||||
@ -223,7 +235,7 @@ namespace osu.Game.Overlays.BeatmapSet
|
|||||||
|
|
||||||
BeatmapSet.BindValueChanged(setInfo =>
|
BeatmapSet.BindValueChanged(setInfo =>
|
||||||
{
|
{
|
||||||
Picker.BeatmapSet = author.BeatmapSet = beatmapAvailability.BeatmapSet = Details.BeatmapSet = setInfo.NewValue;
|
Picker.BeatmapSet = RulesetSelector.BeatmapSet = author.BeatmapSet = beatmapAvailability.BeatmapSet = Details.BeatmapSet = setInfo.NewValue;
|
||||||
cover.BeatmapSet = setInfo.NewValue;
|
cover.BeatmapSet = setInfo.NewValue;
|
||||||
|
|
||||||
if (setInfo.NewValue == null)
|
if (setInfo.NewValue == null)
|
||||||
|
@ -110,7 +110,7 @@ namespace osu.Game.Overlays.Changelog
|
|||||||
t.Font = fontLarge;
|
t.Font = fontLarge;
|
||||||
t.Colour = entryColour;
|
t.Colour = entryColour;
|
||||||
});
|
});
|
||||||
title.AddLink($"{entry.Repository.Replace("ppy/", "")}#{entry.GithubPullRequestId}", entry.GithubUrl, Online.Chat.LinkAction.External,
|
title.AddLink($"{entry.Repository.Replace("ppy/", "")}#{entry.GithubPullRequestId}", entry.GithubUrl,
|
||||||
creationParameters: t =>
|
creationParameters: t =>
|
||||||
{
|
{
|
||||||
t.Font = fontLarge;
|
t.Font = fontLarge;
|
||||||
@ -140,7 +140,7 @@ namespace osu.Game.Overlays.Changelog
|
|||||||
t.Colour = entryColour;
|
t.Colour = entryColour;
|
||||||
});
|
});
|
||||||
else if (entry.GithubUser.GithubUrl != null)
|
else if (entry.GithubUser.GithubUrl != null)
|
||||||
title.AddLink(entry.GithubUser.DisplayName, entry.GithubUser.GithubUrl, Online.Chat.LinkAction.External, null, null, t =>
|
title.AddLink(entry.GithubUser.DisplayName, entry.GithubUser.GithubUrl, t =>
|
||||||
{
|
{
|
||||||
t.Font = fontMedium;
|
t.Font = fontMedium;
|
||||||
t.Colour = entryColour;
|
t.Colour = entryColour;
|
||||||
|
@ -44,7 +44,17 @@ namespace osu.Game.Overlays.Changelog
|
|||||||
req.Failure += _ => complete = true;
|
req.Failure += _ => complete = true;
|
||||||
|
|
||||||
// This is done on a separate thread to support cancellation below
|
// This is done on a separate thread to support cancellation below
|
||||||
Task.Run(() => req.Perform(api));
|
Task.Run(() =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
req.Perform(api);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
complete = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
while (!complete)
|
while (!complete)
|
||||||
{
|
{
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user