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

Merge remote-tracking branch 'refs/remotes/ppy/master' into vote-pill-fix

This commit is contained in:
Andrei Zavatski 2019-11-06 12:26:41 +03:00
commit 1f3b209a6c
35 changed files with 1621 additions and 238 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,21 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Objects;
using System.Collections.Generic;
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
{
/// <summary>
/// Connects hit objects visually, for example with follow points.
/// </summary>
public abstract class ConnectionRenderer<T> : LifetimeManagementContainer
where T : HitObject
{
/// <summary>
/// Hit objects to create connections for
/// </summary>
public abstract IEnumerable<T> HitObjects { get; set; }
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,35 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Edit.Timing;
namespace osu.Game.Tests.Visual.Editor
{
[TestFixture]
public class TestSceneTimingScreen : EditorClockTestScene
{
public override IReadOnlyList<Type> RequiredTypes => new[]
{
typeof(ControlPointTable),
typeof(ControlPointSettings),
typeof(Section<>),
typeof(TimingSection),
typeof(EffectSection),
typeof(SampleSection),
typeof(DifficultySection),
typeof(RowAttribute)
};
[BackgroundDependencyLoader]
private void load()
{
Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
Child = new TimingScreen();
}
}
}

View File

@ -237,6 +237,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("player not exited", () => Player.IsCurrentScreen());
AddStep("exit", () => Player.Exit());
confirmExited();
confirmNoTrackAdjustments();
}
private void confirmPaused()
@ -258,6 +259,11 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("player exited", () => !Player.IsCurrentScreen());
}
private void confirmNoTrackAdjustments()
{
AddAssert("track has no adjustments", () => Beatmap.Value.Track.AggregateFrequency.Value == 1);
}
private void restart() => AddStep("restart", () => Player.Restart());
private void pause() => AddStep("pause", () => Player.Pause());
private void resume() => AddStep("resume", () => Player.Resume());

View File

@ -215,7 +215,7 @@ namespace osu.Game.Tournament
foreach (var r in ladder.Rounds)
foreach (var b in r.Beatmaps)
if (b.BeatmapInfo == null)
if (b.BeatmapInfo == null && b.ID > 0)
{
var req = new GetBeatmapRequest(new BeatmapInfo { OnlineBeatmapID = b.ID });
req.Perform(API);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,50 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
namespace osu.Game.Screens.Edit.Timing
{
public class ControlPointSettings : CompositeDrawable
{
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
RelativeSizeAxes = Axes.Both;
InternalChildren = new Drawable[]
{
new Box
{
Colour = colours.Gray3,
RelativeSizeAxes = Axes.Both,
},
new OsuScrollContainer
{
RelativeSizeAxes = Axes.Both,
Child = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Children = createSections()
},
}
};
}
private IReadOnlyList<Drawable> createSections() => new Drawable[]
{
new TimingSection(),
new DifficultySection(),
new SampleSection(),
new EffectSection(),
};
}
}

View File

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

View File

@ -0,0 +1,48 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Bindables;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Overlays.Settings;
namespace osu.Game.Screens.Edit.Timing
{
internal class DifficultySection : Section<DifficultyControlPoint>
{
private SettingsSlider<double> multiplier;
[BackgroundDependencyLoader]
private void load()
{
Flow.AddRange(new[]
{
multiplier = new SettingsSlider<double>
{
LabelText = "Speed Multiplier",
Bindable = new DifficultyControlPoint().SpeedMultiplierBindable,
RelativeSizeAxes = Axes.X,
}
});
}
protected override void OnControlPointChanged(ValueChangedEvent<DifficultyControlPoint> point)
{
if (point.NewValue != null)
{
multiplier.Bindable = point.NewValue.SpeedMultiplierBindable;
}
}
protected override DifficultyControlPoint CreatePoint()
{
var reference = Beatmap.Value.Beatmap.ControlPointInfo.DifficultyPointAt(SelectedGroup.Value.Time);
return new DifficultyControlPoint
{
SpeedMultiplier = reference.SpeedMultiplier,
};
}
}
}

View File

@ -0,0 +1,46 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.UserInterfaceV2;
namespace osu.Game.Screens.Edit.Timing
{
internal class EffectSection : Section<EffectControlPoint>
{
private LabelledSwitchButton kiai;
private LabelledSwitchButton omitBarLine;
[BackgroundDependencyLoader]
private void load()
{
Flow.AddRange(new[]
{
kiai = new LabelledSwitchButton { Label = "Kiai Time" },
omitBarLine = new LabelledSwitchButton { Label = "Skip Bar Line" },
});
}
protected override void OnControlPointChanged(ValueChangedEvent<EffectControlPoint> point)
{
if (point.NewValue != null)
{
kiai.Current = point.NewValue.KiaiModeBindable;
omitBarLine.Current = point.NewValue.OmitFirstBarLineBindable;
}
}
protected override EffectControlPoint CreatePoint()
{
var reference = Beatmap.Value.Beatmap.ControlPointInfo.EffectPointAt(SelectedGroup.Value.Time);
return new EffectControlPoint
{
KiaiMode = reference.KiaiMode,
OmitFirstBarLine = reference.OmitFirstBarLine
};
}
}
}

View File

@ -0,0 +1,60 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
namespace osu.Game.Screens.Edit.Timing
{
public class RowAttribute : CompositeDrawable, IHasTooltip
{
private readonly string header;
private readonly Func<string> content;
public RowAttribute(string header, Func<string> content)
{
this.header = header;
this.content = content;
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
AutoSizeAxes = Axes.X;
Height = 20;
Anchor = Anchor.CentreLeft;
Origin = Anchor.CentreLeft;
Masking = true;
CornerRadius = 5;
InternalChildren = new Drawable[]
{
new Box
{
Colour = colours.Yellow,
RelativeSizeAxes = Axes.Both,
},
new OsuSpriteText
{
Padding = new MarginPadding(2),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Font = OsuFont.Default.With(weight: FontWeight.SemiBold, size: 12),
Text = header,
Colour = colours.Gray3
},
};
}
public string TooltipText => content();
}
}

View File

@ -0,0 +1,55 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Bindables;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Overlays.Settings;
namespace osu.Game.Screens.Edit.Timing
{
internal class SampleSection : Section<SampleControlPoint>
{
private LabelledTextBox bank;
private SettingsSlider<int> volume;
[BackgroundDependencyLoader]
private void load()
{
Flow.AddRange(new Drawable[]
{
bank = new LabelledTextBox
{
Label = "Bank Name",
},
volume = new SettingsSlider<int>
{
Bindable = new SampleControlPoint().SampleVolumeBindable,
LabelText = "Volume",
}
});
}
protected override void OnControlPointChanged(ValueChangedEvent<SampleControlPoint> point)
{
if (point.NewValue != null)
{
bank.Current = point.NewValue.SampleBankBindable;
volume.Bindable = point.NewValue.SampleVolumeBindable;
}
}
protected override SampleControlPoint CreatePoint()
{
var reference = Beatmap.Value.Beatmap.ControlPointInfo.SamplePointAt(SelectedGroup.Value.Time);
return new SampleControlPoint
{
SampleBank = reference.SampleBank,
SampleVolume = reference.SampleVolume,
};
}
}
}

View File

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

View File

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

View File

@ -0,0 +1,85 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Timing;
using osu.Game.Overlays.Settings;
namespace osu.Game.Screens.Edit.Timing
{
internal class TimingSection : Section<TimingControlPoint>
{
private SettingsSlider<double> bpm;
private SettingsEnumDropdown<TimeSignatures> timeSignature;
[BackgroundDependencyLoader]
private void load()
{
Flow.AddRange(new Drawable[]
{
bpm = new BPMSlider
{
Bindable = new TimingControlPoint().BeatLengthBindable,
LabelText = "BPM",
},
timeSignature = new SettingsEnumDropdown<TimeSignatures>
{
LabelText = "Time Signature"
},
});
}
protected override void OnControlPointChanged(ValueChangedEvent<TimingControlPoint> point)
{
if (point.NewValue != null)
{
bpm.Bindable = point.NewValue.BeatLengthBindable;
timeSignature.Bindable = point.NewValue.TimeSignatureBindable;
}
}
protected override TimingControlPoint CreatePoint()
{
var reference = Beatmap.Value.Beatmap.ControlPointInfo.TimingPointAt(SelectedGroup.Value.Time);
return new TimingControlPoint
{
BeatLength = reference.BeatLength,
TimeSignature = reference.TimeSignature
};
}
private class BPMSlider : SettingsSlider<double>
{
private readonly BindableDouble beatLengthBindable = new BindableDouble();
private BindableDouble bpmBindable;
public override Bindable<double> Bindable
{
get => base.Bindable;
set
{
// incoming will be beatlength
beatLengthBindable.UnbindBindings();
beatLengthBindable.BindTo(value);
base.Bindable = bpmBindable = new BindableDouble(beatLengthToBpm(beatLengthBindable.Value))
{
MinValue = beatLengthToBpm(beatLengthBindable.MaxValue),
MaxValue = beatLengthToBpm(beatLengthBindable.MinValue),
Default = beatLengthToBpm(beatLengthBindable.Default),
};
bpmBindable.BindValueChanged(bpm => beatLengthBindable.Value = beatLengthToBpm(bpm.NewValue));
}
}
private double beatLengthToBpm(double beatLength) => 60000 / beatLength;
}
}
}

View File

@ -162,16 +162,12 @@ namespace osu.Game.Screens.Play
if (sourceClock != beatmap.Track)
return;
removeSourceClockAdjustments();
sourceClock = new TrackVirtual(beatmap.Track.Length);
adjustableClock.ChangeSource(sourceClock);
}
public void ResetLocalAdjustments()
{
// In the case of replays, we may have changed the playback rate.
UserPlaybackRate.Value = 1;
}
protected override void Update()
{
if (!IsPaused.Value)
@ -198,6 +194,14 @@ namespace osu.Game.Screens.Play
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
removeSourceClockAdjustments();
sourceClock = null;
}
private void removeSourceClockAdjustments()
{
sourceClock.ResetSpeedAdjustments();
(sourceClock as IAdjustableAudioComponent)?.RemoveAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust);
}
}

View File

@ -536,8 +536,6 @@ namespace osu.Game.Screens.Play
return true;
}
GameplayClockContainer.ResetLocalAdjustments();
// GameplayClockContainer performs seeks / start / stop operations on the beatmap's track.
// as we are no longer the current screen, we cannot guarantee the track is still usable.
GameplayClockContainer.StopUsingBeatmapClock();

View File

@ -26,7 +26,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2019.1010.0" />
<PackageReference Include="ppy.osu.Framework" Version="2019.1029.0" />
<PackageReference Include="ppy.osu.Framework" Version="2019.1106.0" />
<PackageReference Include="SharpCompress" Version="0.24.0" />
<PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="SharpRaven" Version="2.4.0" />

View File

@ -118,8 +118,8 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.1" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.1" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2019.1010.0" />
<PackageReference Include="ppy.osu.Framework" Version="2019.1029.0" />
<PackageReference Include="ppy.osu.Framework.iOS" Version="2019.1029.0" />
<PackageReference Include="ppy.osu.Framework" Version="2019.1106.0" />
<PackageReference Include="ppy.osu.Framework.iOS" Version="2019.1106.0" />
<PackageReference Include="SharpCompress" Version="0.24.0" />
<PackageReference Include="NUnit" Version="3.11.0" />
<PackageReference Include="SharpRaven" Version="2.4.0" />