1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-18 10:40:12 +08:00

Compare commits

...

309 Commits

133 changed files with 3736 additions and 1351 deletions
@@ -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
View File
@@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: osu!stable issues
url: https://github.com/ppy/osu-stable-issues
about: For issues regarding osu!stable (not osu!lazer), open them here.
@@ -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:**
+1 -1
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.1021.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2019.1029.0" />
</ItemGroup>
</Project>
@@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Catch.Tests
private void load()
{
var controlPointInfo = new ControlPointInfo();
controlPointInfo.TimingPoints.Add(new TimingControlPoint());
controlPointInfo.Add(0, new TimingControlPoint());
WorkingBeatmap beatmap = CreateWorkingBeatmap(new Beatmap
{
@@ -2,6 +2,7 @@
// 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.Framework.Graphics;
@@ -10,10 +11,12 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Framework.MathUtils;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Edit;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Compose.Components;
using osu.Game.Tests.Visual;
using osuTK;
using osuTK.Graphics;
@@ -25,32 +28,44 @@ namespace osu.Game.Rulesets.Osu.Tests
private const double beat_length = 100;
private static readonly Vector2 grid_position = new Vector2(512, 384);
public override IReadOnlyList<Type> RequiredTypes => new[]
{
typeof(CircularDistanceSnapGrid)
};
[Cached(typeof(IEditorBeatmap))]
private readonly EditorBeatmap<OsuHitObject> editorBeatmap;
[Cached]
private readonly BindableBeatDivisor beatDivisor = new BindableBeatDivisor();
private TestOsuDistanceSnapGrid grid;
[Cached(typeof(IDistanceSnapProvider))]
private readonly SnapProvider snapProvider = new SnapProvider();
private readonly TestOsuDistanceSnapGrid grid;
public TestSceneOsuDistanceSnapGrid()
{
editorBeatmap = new EditorBeatmap<OsuHitObject>(new OsuBeatmap());
createGrid();
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 }
};
}
[SetUp]
public void Setup() => Schedule(() =>
{
Clear();
editorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = 1;
editorBeatmap.ControlPointInfo.DifficultyPoints.Clear();
editorBeatmap.ControlPointInfo.TimingPoints.Clear();
editorBeatmap.ControlPointInfo.TimingPoints.Add(new TimingControlPoint { BeatLength = beat_length });
beatDivisor.Value = 1;
editorBeatmap.ControlPointInfo.Clear();
editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = beat_length });
});
[TestCase(1)]
@@ -64,53 +79,11 @@ namespace osu.Game.Rulesets.Osu.Tests
public void TestBeatDivisor(int 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]
public void TestCursorInCentre()
{
createGrid();
AddStep("move mouse to centre", () => InputManager.MoveMouseTo(grid.ToScreenSpace(grid_position)));
assertSnappedDistance((float)beat_length);
}
@@ -118,8 +91,6 @@ namespace osu.Game.Rulesets.Osu.Tests
[Test]
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)));
assertSnappedDistance((float)beat_length);
}
@@ -127,37 +98,17 @@ namespace osu.Game.Rulesets.Osu.Tests
[Test]
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)));
assertSnappedDistance((float)beat_length * 2);
}
private void assertSnappedDistance(float expectedDistance) => AddAssert($"snap distance = {expectedDistance}", () =>
{
Vector2 snappedPosition = grid.GetSnapPosition(grid.ToLocalSpace(InputManager.CurrentState.Mouse.Position));
float distance = Vector2.Distance(snappedPosition, grid_position);
Vector2 snappedPosition = grid.GetSnappedPosition(grid.ToLocalSpace(InputManager.CurrentState.Mouse.Position)).position;
return Precision.AlmostEquals(expectedDistance, distance);
return Precision.AlmostEquals(expectedDistance, Vector2.Distance(snappedPosition, grid_position));
});
private void createGrid()
{
AddStep("create grid", () =>
{
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.SlateGray
},
grid = new TestOsuDistanceSnapGrid(new HitCircle { Position = grid_position }),
new SnappingCursorContainer { GetSnapPosition = v => grid.GetSnapPosition(grid.ToLocalSpace(v)) }
};
});
}
private class SnappingCursorContainer : CompositeDrawable
{
public Func<Vector2, Vector2> GetSnapPosition;
@@ -206,5 +157,20 @@ namespace osu.Game.Rulesets.Osu.Tests
{
}
}
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) => 0;
public double DistanceToDuration(double referenceTime, float distance) => 0;
public double GetSnappedDurationFromDistance(double referenceTime, float distance) => 0;
public float GetSnappedDistanceFromDistance(double referenceTime, float distance) => 0;
}
}
}
+16 -1
View File
@@ -111,6 +111,21 @@ namespace osu.Game.Rulesets.Osu.Tests
AddStep("Distance Overflow 1 Repeat", () => SetContents(() => testDistanceOverflow(1)));
}
[Test]
public void TestChangeStackHeight()
{
DrawableSlider slider = null;
AddStep("create slider", () =>
{
slider = (DrawableSlider)createSlider(repeats: 1);
Add(slider);
});
AddStep("change stack height", () => slider.HitObject.StackHeight = 10);
AddAssert("body positioned correctly", () => slider.Position == slider.HitObject.StackedPosition);
}
private Drawable testSimpleBig(int repeats = 0) => createSlider(2, repeats: repeats);
private Drawable testSimpleBigLargeStackOffset(int repeats = 0) => createSlider(2, repeats: repeats, stackHeight: 10);
@@ -293,7 +308,7 @@ namespace osu.Game.Rulesets.Osu.Tests
private Drawable createDrawable(Slider slider, float circleSize, double speedMultiplier)
{
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 });
@@ -313,10 +313,6 @@ namespace osu.Game.Rulesets.Osu.Tests
}, 25),
}
},
ControlPointInfo =
{
DifficultyPoints = { new DifficultyControlPoint { SpeedMultiplier = 0.1f } }
},
BeatmapInfo =
{
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 } });
p.OnLoadComplete += _ =>
@@ -16,6 +16,7 @@ using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Tests.Visual;
using osuTK;
using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Tests
{
@@ -78,6 +79,100 @@ namespace osu.Game.Rulesets.Osu.Tests
checkPositions();
}
[Test]
public void TestStackedHitObject()
{
AddStep("set stacking", () => slider.StackHeight = 5);
checkPositions();
}
[Test]
public void TestSingleControlPointSelection()
{
moveMouseToControlPoint(0);
AddStep("click", () => InputManager.Click(MouseButton.Left));
checkControlPointSelected(0, true);
checkControlPointSelected(1, false);
}
[Test]
public void TestSingleControlPointDeselectionViaOtherControlPoint()
{
moveMouseToControlPoint(0);
AddStep("click", () => InputManager.Click(MouseButton.Left));
moveMouseToControlPoint(1);
AddStep("click", () => InputManager.Click(MouseButton.Left));
checkControlPointSelected(0, false);
checkControlPointSelected(1, true);
}
[Test]
public void TestSingleControlPointDeselectionViaClickOutside()
{
moveMouseToControlPoint(0);
AddStep("click", () => InputManager.Click(MouseButton.Left));
AddStep("move mouse outside control point", () => InputManager.MoveMouseTo(drawableObject));
AddStep("click", () => InputManager.Click(MouseButton.Left));
checkControlPointSelected(0, false);
checkControlPointSelected(1, false);
}
[Test]
public void TestMultipleControlPointSelection()
{
moveMouseToControlPoint(0);
AddStep("click", () => InputManager.Click(MouseButton.Left));
moveMouseToControlPoint(1);
AddStep("ctrl + click", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.Click(MouseButton.Left);
InputManager.ReleaseKey(Key.ControlLeft);
});
checkControlPointSelected(0, true);
checkControlPointSelected(1, true);
}
[Test]
public void TestMultipleControlPointDeselectionViaOtherControlPoint()
{
moveMouseToControlPoint(0);
AddStep("click", () => InputManager.Click(MouseButton.Left));
moveMouseToControlPoint(1);
AddStep("ctrl + click", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.Click(MouseButton.Left);
InputManager.ReleaseKey(Key.ControlLeft);
});
moveMouseToControlPoint(2);
AddStep("click", () => InputManager.Click(MouseButton.Left));
checkControlPointSelected(0, false);
checkControlPointSelected(1, false);
}
[Test]
public void TestMultipleControlPointDeselectionViaClickOutside()
{
moveMouseToControlPoint(0);
AddStep("click", () => InputManager.Click(MouseButton.Left));
moveMouseToControlPoint(1);
AddStep("ctrl + click", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.Click(MouseButton.Left);
InputManager.ReleaseKey(Key.ControlLeft);
});
AddStep("move mouse outside control point", () => InputManager.MoveMouseTo(drawableObject));
AddStep("click", () => InputManager.Click(MouseButton.Left));
checkControlPointSelected(0, false);
checkControlPointSelected(1, false);
}
private void moveHitObject()
{
AddStep("move hitobject", () =>
@@ -88,7 +183,7 @@ namespace osu.Game.Rulesets.Osu.Tests
private void checkPositions()
{
AddAssert("body positioned correctly", () => blueprint.BodyPiece.Position == slider.Position);
AddAssert("body positioned correctly", () => blueprint.BodyPiece.Position == slider.StackedPosition);
AddAssert("head positioned correctly",
() => Precision.AlmostEquals(blueprint.HeadBlueprint.CirclePiece.ScreenSpaceDrawQuad.Centre, drawableObject.HeadCircle.ScreenSpaceDrawQuad.Centre));
@@ -97,11 +192,24 @@ namespace osu.Game.Rulesets.Osu.Tests
() => Precision.AlmostEquals(blueprint.TailBlueprint.CirclePiece.ScreenSpaceDrawQuad.Centre, drawableObject.TailCircle.ScreenSpaceDrawQuad.Centre));
}
private void moveMouseToControlPoint(int index)
{
AddStep($"move mouse to control point {index}", () =>
{
Vector2 position = slider.Position + slider.Path.ControlPoints[index];
InputManager.MoveMouseTo(drawableObject.Parent.ToScreenSpace(position));
});
}
private void checkControlPointSelected(int index, bool selected)
=> AddAssert($"control point {index} {(selected ? "selected" : "not selected")}", () => blueprint.ControlPointVisualiser.Pieces[index].IsSelected.Value == selected);
private class TestSliderBlueprint : SliderSelectionBlueprint
{
public new SliderBodyPiece BodyPiece => base.BodyPiece;
public new TestSliderCircleBlueprint HeadBlueprint => (TestSliderCircleBlueprint)base.HeadBlueprint;
public new TestSliderCircleBlueprint TailBlueprint => (TestSliderCircleBlueprint)base.TailBlueprint;
public new PathControlPointVisualiser ControlPointVisualiser => base.ControlPointVisualiser;
public TestSliderBlueprint(DrawableSlider slider)
: base(slider)
@@ -1,26 +1,33 @@
// 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.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Lines;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Objects;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{
public class PathControlPointPiece : BlueprintPiece<Slider>
{
private readonly Slider slider;
private readonly int index;
public Action<int> RequestSelection;
public Action<Vector2[]> ControlPointsChanged;
public readonly BindableBool IsSelected = new BindableBool();
public readonly int Index;
private readonly Slider slider;
private readonly Path path;
private readonly CircularContainer marker;
private readonly Container marker;
private readonly Drawable markerRing;
[Resolved]
private OsuColour colours { get; set; }
@@ -28,7 +35,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
public PathControlPointPiece(Slider slider, int index)
{
this.slider = slider;
this.index = index;
Index = index;
Origin = Anchor.Centre;
AutoSizeAxes = Axes.Both;
@@ -40,13 +47,36 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
Anchor = Anchor.Centre,
PathRadius = 1
},
marker = new CircularContainer
marker = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(10),
Masking = true,
Child = new Box { RelativeSizeAxes = Axes.Both }
AutoSizeAxes = Axes.Both,
Children = new[]
{
new Circle
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(10),
},
markerRing = new CircularContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(14),
Masking = true,
BorderThickness = 2,
BorderColour = Color4.White,
Alpha = 0,
Child = new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0,
AlwaysPresent = true
}
}
}
}
};
}
@@ -55,30 +85,66 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{
base.Update();
Position = slider.StackedPosition + slider.Path.ControlPoints[index];
Position = slider.StackedPosition + slider.Path.ControlPoints[Index];
marker.Colour = isSegmentSeparator ? colours.Red : colours.Yellow;
updateMarkerDisplay();
updateConnectingPath();
}
/// <summary>
/// Updates the state of the circular control point marker.
/// </summary>
private void updateMarkerDisplay()
{
markerRing.Alpha = IsSelected.Value ? 1 : 0;
Color4 colour = isSegmentSeparator ? colours.Red : colours.Yellow;
if (IsHovered || IsSelected.Value)
colour = Color4.White;
marker.Colour = colour;
}
/// <summary>
/// Updates the path connecting this control point to the previous one.
/// </summary>
private void updateConnectingPath()
{
path.ClearVertices();
if (index != slider.Path.ControlPoints.Length - 1)
if (Index != slider.Path.ControlPoints.Length - 1)
{
path.AddVertex(Vector2.Zero);
path.AddVertex(slider.Path.ControlPoints[index + 1] - slider.Path.ControlPoints[index]);
path.AddVertex(slider.Path.ControlPoints[Index + 1] - slider.Path.ControlPoints[Index]);
}
path.OriginPosition = path.PositionInBoundingBox(Vector2.Zero);
}
// The connecting path is excluded from positional input
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => marker.ReceivePositionalInputAt(screenSpacePos);
protected override bool OnMouseDown(MouseDownEvent e)
{
if (RequestSelection != null)
{
RequestSelection.Invoke(Index);
return true;
}
return false;
}
protected override bool OnMouseUp(MouseUpEvent e) => RequestSelection != null;
protected override bool OnClick(ClickEvent e) => RequestSelection != null;
protected override bool OnDragStart(DragStartEvent e) => true;
protected override bool OnDrag(DragEvent e)
{
var newControlPoints = slider.Path.ControlPoints.ToArray();
if (index == 0)
if (Index == 0)
{
// Special handling for the head - only the position of the slider changes
slider.Position += e.Delta;
@@ -88,15 +154,15 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
newControlPoints[i] -= e.Delta;
}
else
newControlPoints[index] += e.Delta;
newControlPoints[Index] += e.Delta;
if (isSegmentSeparatorWithNext)
newControlPoints[index + 1] = newControlPoints[index];
newControlPoints[Index + 1] = newControlPoints[Index];
if (isSegmentSeparatorWithPrevious)
newControlPoints[index - 1] = newControlPoints[index];
newControlPoints[Index - 1] = newControlPoints[Index];
slider.Path = new SliderPath(slider.Path.Type, newControlPoints);
ControlPointsChanged?.Invoke(newControlPoints);
return true;
}
@@ -105,8 +171,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
private bool isSegmentSeparator => isSegmentSeparatorWithNext || isSegmentSeparatorWithPrevious;
private bool isSegmentSeparatorWithNext => index < slider.Path.ControlPoints.Length - 1 && slider.Path.ControlPoints[index + 1] == slider.Path.ControlPoints[index];
private bool isSegmentSeparatorWithNext => Index < slider.Path.ControlPoints.Length - 1 && slider.Path.ControlPoints[Index + 1] == slider.Path.ControlPoints[Index];
private bool isSegmentSeparatorWithPrevious => index > 0 && slider.Path.ControlPoints[index - 1] == slider.Path.ControlPoints[index];
private bool isSegmentSeparatorWithPrevious => Index > 0 && slider.Path.ControlPoints[Index - 1] == slider.Path.ControlPoints[Index];
}
}
@@ -1,33 +1,133 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit.Compose;
using osuTK;
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{
public class PathControlPointVisualiser : CompositeDrawable
public class PathControlPointVisualiser : CompositeDrawable, IKeyBindingHandler<PlatformAction>
{
public Action<Vector2[]> ControlPointsChanged;
internal readonly Container<PathControlPointPiece> Pieces;
private readonly Slider slider;
private readonly bool allowSelection;
private readonly Container<PathControlPointPiece> pieces;
private InputManager inputManager;
public PathControlPointVisualiser(Slider slider)
[Resolved(CanBeNull = true)]
private IPlacementHandler placementHandler { get; set; }
public PathControlPointVisualiser(Slider slider, bool allowSelection)
{
this.slider = slider;
this.allowSelection = allowSelection;
InternalChild = pieces = new Container<PathControlPointPiece> { RelativeSizeAxes = Axes.Both };
RelativeSizeAxes = Axes.Both;
InternalChild = Pieces = new Container<PathControlPointPiece> { RelativeSizeAxes = Axes.Both };
}
protected override void LoadComplete()
{
base.LoadComplete();
inputManager = GetContainingInputManager();
}
protected override void Update()
{
base.Update();
while (slider.Path.ControlPoints.Length > pieces.Count)
pieces.Add(new PathControlPointPiece(slider, pieces.Count));
while (slider.Path.ControlPoints.Length < pieces.Count)
pieces.Remove(pieces[pieces.Count - 1]);
while (slider.Path.ControlPoints.Length > Pieces.Count)
{
var piece = new PathControlPointPiece(slider, Pieces.Count)
{
ControlPointsChanged = c => ControlPointsChanged?.Invoke(c),
};
if (allowSelection)
piece.RequestSelection = selectPiece;
Pieces.Add(piece);
}
while (slider.Path.ControlPoints.Length < Pieces.Count)
Pieces.Remove(Pieces[Pieces.Count - 1]);
}
protected override bool OnClick(ClickEvent e)
{
foreach (var piece in Pieces)
piece.IsSelected.Value = false;
return false;
}
private void selectPiece(int index)
{
if (inputManager.CurrentState.Keyboard.ControlPressed)
Pieces[index].IsSelected.Toggle();
else
{
foreach (var piece in Pieces)
piece.IsSelected.Value = piece.Index == index;
}
}
public bool OnPressed(PlatformAction action)
{
switch (action.ActionMethod)
{
case PlatformActionMethod.Delete:
var newControlPoints = new List<Vector2>();
foreach (var piece in Pieces)
{
if (!piece.IsSelected.Value)
newControlPoints.Add(slider.Path.ControlPoints[piece.Index]);
}
// Ensure that there are any points to be deleted
if (newControlPoints.Count == slider.Path.ControlPoints.Length)
return false;
// If there are 0 remaining control points, treat the slider as being deleted
if (newControlPoints.Count == 0)
{
placementHandler?.Delete(slider);
return true;
}
// Make control points relative
Vector2 first = newControlPoints[0];
for (int i = 0; i < newControlPoints.Count; i++)
newControlPoints[i] = newControlPoints[i] - first;
// The slider's position defines the position of the first control point, and all further control points are relative to that point
slider.Position = slider.Position + first;
// Since pieces are re-used, they will not point to the deleted control points while remaining selected
foreach (var piece in Pieces)
piece.IsSelected.Value = false;
ControlPointsChanged?.Invoke(newControlPoints.ToArray());
return true;
}
return false;
}
public bool OnReleased(PlatformAction action) => action.ActionMethod == PlatformActionMethod.Delete;
}
}
@@ -43,5 +43,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
Size = body.Size;
OriginPosition = body.PathOffset;
}
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => body.ReceivePositionalInputAt(screenSpacePos);
}
}
@@ -6,6 +6,7 @@ using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Input;
using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osu.Game.Rulesets.Edit;
@@ -28,9 +29,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private readonly List<Segment> segments = new List<Segment>();
private Vector2 cursor;
private InputManager inputManager;
private PlacementState state;
[Resolved(CanBeNull = true)]
private HitObjectComposer composer { get; set; }
public SliderPlacementBlueprint()
: base(new Objects.Slider())
{
@@ -46,12 +51,18 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
bodyPiece = new SliderBodyPiece(),
headCirclePiece = new HitCirclePiece(),
tailCirclePiece = new HitCirclePiece(),
new PathControlPointVisualiser(HitObject),
new PathControlPointVisualiser(HitObject, false) { ControlPointsChanged = _ => updateSlider() },
};
setState(PlacementState.Initial);
}
protected override void LoadComplete()
{
base.LoadComplete();
inputManager = GetContainingInputManager();
}
public override void UpdatePosition(Vector2 screenSpacePosition)
{
switch (state)
@@ -61,7 +72,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
break;
case PlacementState.Body:
cursor = ToLocalSpace(screenSpacePosition) - HitObject.Position;
// The given screen-space position may have been externally snapped, but the unsnapped position from the input manager
// is used instead since snapping control points doesn't make much sense
cursor = ToLocalSpace(inputManager.CurrentState.Mouse.Position) - HitObject.Position;
break;
}
}
@@ -121,8 +134,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private void updateSlider()
{
var newControlPoints = segments.SelectMany(s => s.ControlPoints).Concat(cursor.Yield()).ToArray();
HitObject.Path = new SliderPath(newControlPoints.Length > 2 ? PathType.Bezier : PathType.Linear, newControlPoints);
Vector2[] newControlPoints = segments.SelectMany(s => s.ControlPoints).Concat(cursor.Yield()).ToArray();
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);
headCirclePiece.UpdateFrom(HitObject.HeadCircle);
@@ -1,7 +1,11 @@
// 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.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.Objects;
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 SliderCircleSelectionBlueprint HeadBlueprint;
protected readonly SliderCircleSelectionBlueprint TailBlueprint;
protected readonly PathControlPointVisualiser ControlPointVisualiser;
[Resolved(CanBeNull = true)]
private HitObjectComposer composer { get; set; }
public SliderSelectionBlueprint(DrawableSlider slider)
: base(slider)
@@ -25,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
BodyPiece = new SliderBodyPiece(),
HeadBlueprint = CreateCircleSelectionBlueprint(slider, SliderPosition.Start),
TailBlueprint = CreateCircleSelectionBlueprint(slider, SliderPosition.End),
new PathControlPointVisualiser(sliderObject),
ControlPointVisualiser = new PathControlPointVisualiser(sliderObject, true) { ControlPointsChanged = onNewControlPoints },
};
}
@@ -36,8 +44,20 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
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 bool ReceivePositionalInputAt(Vector2 screenSpacePos) => BodyPiece.ReceivePositionalInputAt(screenSpacePos);
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.
// 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.Screens.Edit.Compose.Components;
@@ -15,15 +13,5 @@ namespace osu.Game.Rulesets.Osu.Edit
{
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);
}
}
}
@@ -34,6 +34,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private readonly Slider slider;
private readonly IBindable<Vector2> positionBindable = new Bindable<Vector2>();
private readonly IBindable<int> stackHeightBindable = new Bindable<int>();
private readonly IBindable<float> scaleBindable = new Bindable<float>();
private readonly IBindable<SliderPath> pathBindable = new Bindable<SliderPath>();
@@ -72,6 +73,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
config?.BindWith(OsuRulesetSetting.SnakingOutSliders, Body.SnakingOut);
positionBindable.BindValueChanged(_ => Position = HitObject.StackedPosition);
stackHeightBindable.BindValueChanged(_ => Position = HitObject.StackedPosition);
scaleBindable.BindValueChanged(scale =>
{
updatePathRadius();
@@ -79,6 +81,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
});
positionBindable.BindTo(HitObject.PositionBindable);
stackHeightBindable.BindTo(HitObject.StackHeightBindable);
scaleBindable.BindTo(HitObject.ScaleBindable);
pathBindable.BindTo(slider.PathBindable);
@@ -7,7 +7,6 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Game.Graphics.Sprites;
using osuTK.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Skinning;
@@ -30,17 +29,15 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
Children = new Drawable[]
{
new CircularContainer
new Container
{
Masking = true,
Origin = Anchor.Centre,
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Glow,
Radius = 60,
Colour = Color4.White.Opacity(0.5f),
},
Child = new Box()
},
number = new SkinnableSpriteText(new OsuSkinComponent(OsuSkinComponents.HitCircleText), _ => new OsuSpriteText
{
@@ -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.Linq;
using osu.Framework.Bindables;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects;
@@ -98,6 +99,15 @@ namespace osu.Game.Rulesets.Osu.Objects
set => LastInComboBindable.Value = value;
}
protected OsuHitObject()
{
StackHeightBindable.BindValueChanged(height =>
{
foreach (var nested in NestedHitObjects.OfType<OsuHitObject>())
nested.StackHeight = height.NewValue;
});
}
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
{
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
+14 -6
View File
@@ -37,6 +37,8 @@ namespace osu.Game.Rulesets.Osu.Objects
{
PathBindable.Value = value;
endPositionCache.Invalidate();
updateNestedPositions();
}
}
@@ -48,14 +50,9 @@ namespace osu.Game.Rulesets.Osu.Objects
set
{
base.Position = value;
endPositionCache.Invalidate();
if (HeadCircle != null)
HeadCircle.Position = value;
if (TailCircle != null)
TailCircle.Position = EndPosition;
updateNestedPositions();
}
}
@@ -163,6 +160,7 @@ namespace osu.Game.Rulesets.Osu.Objects
{
StartTime = e.Time,
Position = Position,
StackHeight = StackHeight,
Samples = getNodeSamples(0),
SampleControlPoint = SampleControlPoint,
});
@@ -176,6 +174,7 @@ namespace osu.Game.Rulesets.Osu.Objects
{
StartTime = e.Time,
Position = EndPosition,
StackHeight = StackHeight
});
break;
@@ -195,6 +194,15 @@ namespace osu.Game.Rulesets.Osu.Objects
}
}
private void updateNestedPositions()
{
if (HeadCircle != null)
HeadCircle.Position = Position;
if (TailCircle != null)
TailCircle.Position = EndPosition;
}
private List<HitSampleInfo> getNodeSamples(int nodeIndex) =>
nodeIndex < NodeSamples.Count ? NodeSamples[nodeIndex] : Samples;
@@ -143,14 +143,14 @@
"Objects": [{
"StartTime": 34989,
"EndTime": 34989,
"X": 163,
"Y": 138
"X": 156.597382,
"Y": 131.597382
},
{
"StartTime": 35018,
"EndTime": 35018,
"X": 188,
"Y": 138
"X": 181.597382,
"Y": 131.597382
}
]
},
@@ -159,14 +159,14 @@
"Objects": [{
"StartTime": 35106,
"EndTime": 35106,
"X": 163,
"Y": 138
"X": 159.798691,
"Y": 134.798691
},
{
"StartTime": 35135,
"EndTime": 35135,
"X": 188,
"Y": 138
"X": 184.798691,
"Y": 134.798691
}
]
},
@@ -191,20 +191,20 @@
"Objects": [{
"StartTime": 35695,
"EndTime": 35695,
"X": 166,
"Y": 76
"X": 162.798691,
"Y": 72.79869
},
{
"StartTime": 35871,
"EndTime": 35871,
"X": 240.99855,
"Y": 75.53417
"X": 237.797241,
"Y": 72.33286
},
{
"StartTime": 36011,
"EndTime": 36011,
"X": 315.9971,
"Y": 75.0683441
"X": 312.795776,
"Y": 71.8670349
}
]
},
@@ -235,20 +235,20 @@
"Objects": [{
"StartTime": 36518,
"EndTime": 36518,
"X": 166,
"Y": 76
"X": 169.201309,
"Y": 79.20131
},
{
"StartTime": 36694,
"EndTime": 36694,
"X": 240.99855,
"Y": 75.53417
"X": 244.19986,
"Y": 78.73548
},
{
"StartTime": 36834,
"EndTime": 36834,
"X": 315.9971,
"Y": 75.0683441
"X": 319.198425,
"Y": 78.26965
}
]
},
@@ -257,20 +257,20 @@
"Objects": [{
"StartTime": 36929,
"EndTime": 36929,
"X": 315,
"Y": 75
"X": 324.603943,
"Y": 84.6039352
},
{
"StartTime": 37105,
"EndTime": 37105,
"X": 240.001526,
"Y": 75.47769
"X": 249.605469,
"Y": 85.08163
},
{
"StartTime": 37245,
"EndTime": 37245,
"X": 165.003052,
"Y": 75.95539
"X": 174.607,
"Y": 85.5593262
}
]
}
+7 -6
View File
@@ -38,9 +38,10 @@ namespace osu.Game.Rulesets.Osu.UI
});
}
public override void Show()
protected override void PopIn()
{
base.Show();
base.PopIn();
GameplayCursor.ActiveCursor.Hide();
cursorScaleContainer.MoveTo(GameplayCursor.ActiveCursor.Position);
clickToResumeCursor.Appear();
@@ -55,13 +56,13 @@ namespace osu.Game.Rulesets.Osu.UI
}
}
public override void Hide()
protected override void PopOut()
{
base.PopOut();
localCursorContainer?.Expire();
localCursorContainer = null;
GameplayCursor.ActiveCursor.Show();
base.Hide();
GameplayCursor?.ActiveCursor?.Show();
}
protected override bool OnHover(HoverEvent e) => true;
@@ -66,7 +66,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
AddStep("Reset height", () => changePlayfieldSize(6));
var controlPointInfo = new ControlPointInfo();
controlPointInfo.TimingPoints.Add(new TimingControlPoint());
controlPointInfo.Add(0, new TimingControlPoint());
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;
var cpi = new ControlPointInfo();
cpi.EffectPoints.Add(new EffectControlPoint { KiaiMode = kiai });
cpi.Add(0, new EffectControlPoint { KiaiMode = kiai });
Hit hit = new Hit();
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;
var cpi = new ControlPointInfo();
cpi.EffectPoints.Add(new EffectControlPoint { KiaiMode = kiai });
cpi.Add(0, new EffectControlPoint { KiaiMode = kiai });
Hit hit = new Hit();
hit.ApplyDefaults(cpi, new BeatmapDifficulty());
@@ -19,12 +19,7 @@ namespace osu.Game.Rulesets.Taiko.Audio
{
this.controlPoints = controlPoints;
IEnumerable<SampleControlPoint> samplePoints;
if (controlPoints.SamplePoints.Count == 0)
// Get the default sample point
samplePoints = new[] { controlPoints.SamplePointAt(double.MinValue) };
else
samplePoints = controlPoints.SamplePoints;
IEnumerable<SampleControlPoint> samplePoints = controlPoints.SamplePoints.Count == 0 ? new[] { controlPoints.SamplePointAt(double.MinValue) } : controlPoints.SamplePoints;
foreach (var s in samplePoints)
{
@@ -167,9 +167,9 @@ namespace osu.Game.Tests.Beatmaps.Formats
var controlPoints = beatmap.ControlPointInfo;
Assert.AreEqual(4, controlPoints.TimingPoints.Count);
Assert.AreEqual(42, controlPoints.DifficultyPoints.Count);
Assert.AreEqual(42, controlPoints.SamplePoints.Count);
Assert.AreEqual(42, controlPoints.EffectPoints.Count);
Assert.AreEqual(5, controlPoints.DifficultyPoints.Count);
Assert.AreEqual(34, controlPoints.SamplePoints.Count);
Assert.AreEqual(8, controlPoints.EffectPoints.Count);
var timingPoint = controlPoints.TimingPointAt(0);
Assert.AreEqual(956, timingPoint.Time);
@@ -191,7 +191,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.AreEqual(1.0, difficultyPoint.SpeedMultiplier);
difficultyPoint = controlPoints.DifficultyPointAt(48428);
Assert.AreEqual(48428, difficultyPoint.Time);
Assert.AreEqual(0, difficultyPoint.Time);
Assert.AreEqual(1.0, difficultyPoint.SpeedMultiplier);
difficultyPoint = controlPoints.DifficultyPointAt(116999);
@@ -224,7 +224,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.IsFalse(effectPoint.OmitFirstBarLine);
effectPoint = controlPoints.EffectPointAt(119637);
Assert.AreEqual(119637, effectPoint.Time);
Assert.AreEqual(95901, effectPoint.Time);
Assert.IsFalse(effectPoint.KiaiMode);
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]
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]
public void TestDecodeControlPointCustomSampleBank()
{
@@ -273,6 +273,96 @@ namespace osu.Game.Tests.Chat
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]
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())
{
}
}
}
}
@@ -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
@@ -1,14 +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;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Framework.MathUtils;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Objects;
@@ -27,27 +25,25 @@ namespace osu.Game.Tests.Visual.Editor
[Cached(typeof(IEditorBeatmap))]
private readonly EditorBeatmap<OsuHitObject> editorBeatmap;
private TestDistanceSnapGrid grid;
[Cached(typeof(IDistanceSnapProvider))]
private readonly SnapProvider snapProvider = new SnapProvider();
public TestSceneDistanceSnapGrid()
{
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();
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.SlateGray
},
new TestDistanceSnapGrid(new HitObject(), grid_position)
};
}
[SetUp]
public void Setup() => Schedule(() =>
{
Clear();
editorBeatmap.ControlPointInfo.TimingPoints.Clear();
editorBeatmap.ControlPointInfo.TimingPoints.Add(new TimingControlPoint { BeatLength = beat_length });
BeatDivisor.Value = 1;
});
[TestCase(1)]
[TestCase(2)]
[TestCase(3)]
@@ -56,84 +52,13 @@ namespace osu.Game.Tests.Visual.Editor
[TestCase(8)]
[TestCase(12)]
[TestCase(16)]
public void TestInitialBeatDivisor(int divisor)
public void TestBeatDivisor(int 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]
public void TestChangeBeatDivisor()
{
createGrid();
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[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.SlateGray
},
grid = new TestDistanceSnapGrid(new HitObject(), grid_position)
};
func?.Invoke(grid);
});
}
private class TestDistanceSnapGrid : DistanceSnapGrid
{
public new float Velocity = 1;
public new float DistanceSpacing => base.DistanceSpacing;
public TestDistanceSnapGrid(HitObject hitObject, Vector2 centrePosition)
@@ -203,11 +128,23 @@ namespace osu.Game.Tests.Visual.Editor
}
}
protected override float GetVelocity(double time, ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
=> Velocity;
public override (Vector2 position, double time) GetSnappedPosition(Vector2 screenSpacePosition)
=> (Vector2.Zero, 0);
}
public override Vector2 GetSnapPosition(Vector2 screenSpacePosition)
=> Vector2.Zero;
private class SnapProvider : IDistanceSnapProvider
{
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) => 0;
public double DistanceToDuration(double referenceTime, float distance) => 0;
public double GetSnappedDurationFromDistance(double referenceTime, float distance) => 0;
public float GetSnappedDistanceFromDistance(double referenceTime, float distance) => 0;
}
}
}
@@ -28,18 +28,7 @@ namespace osu.Game.Tests.Visual.Editor
{
var testBeatmap = new Beatmap
{
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 }
}
},
ControlPointInfo = new ControlPointInfo(),
HitObjects =
{
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);
Child = new TimingPointVisualiser(testBeatmap, 5000) { Clock = Clock };
@@ -22,7 +22,7 @@ using osuTK;
namespace osu.Game.Tests.Visual.Editor
{
[TestFixture]
public class TestSceneHitObjectComposer : OsuTestScene
public class TestSceneHitObjectComposer : EditorClockTestScene
{
public override IReadOnlyList<Type> RequiredTypes => new[]
{
@@ -47,7 +47,8 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
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);
@@ -61,10 +62,10 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestRelativeBeatLengthScaleTimingPointBeyondEndDoesNotBecomeDominant()
{
var beatmap = createBeatmap(
new TimingControlPoint { BeatLength = time_range / 2 },
new TimingControlPoint { Time = 12000, BeatLength = time_range },
new TimingControlPoint { Time = 100000, BeatLength = time_range });
var beatmap = createBeatmap();
beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = time_range / 2 });
beatmap.ControlPointInfo.Add(12000, new TimingControlPoint { BeatLength = time_range });
beatmap.ControlPointInfo.Add(100000, new TimingControlPoint { BeatLength = time_range });
createTest(beatmap, d => d.RelativeScaleBeatLengthsOverride = true);
@@ -75,9 +76,9 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestRelativeBeatLengthScaleFromSecondTimingPoint()
{
var beatmap = createBeatmap(
new TimingControlPoint { BeatLength = time_range },
new TimingControlPoint { Time = 3 * time_range, BeatLength = time_range / 2 });
var beatmap = createBeatmap();
beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = time_range });
beatmap.ControlPointInfo.Add(3 * time_range, new TimingControlPoint { BeatLength = time_range / 2 });
createTest(beatmap, d => d.RelativeScaleBeatLengthsOverride = true);
@@ -97,9 +98,9 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestNonRelativeScale()
{
var beatmap = createBeatmap(
new TimingControlPoint { BeatLength = time_range },
new TimingControlPoint { Time = 3 * time_range, BeatLength = time_range / 2 });
var beatmap = createBeatmap();
beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = time_range });
beatmap.ControlPointInfo.Add(3 * time_range, new TimingControlPoint { BeatLength = time_range / 2 });
createTest(beatmap);
@@ -119,7 +120,8 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
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;
createTest(beatmap, d => d.RelativeScaleBeatLengthsOverride = true);
@@ -132,7 +134,8 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
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;
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.
/// The hitobjects are spaced <see cref="time_range"/> milliseconds apart.
/// </summary>
/// <param name="timingControlPoints">The timing points to add to the beatmap.</param>
/// <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 } };
beatmap.ControlPointInfo.TimingPoints.AddRange(timingControlPoints);
for (int i = 0; i < 10; i++)
beatmap.HitObjects.Add(new HitObject { StartTime = i * time_range });
@@ -69,6 +69,24 @@ namespace osu.Game.Tests.Visual.Gameplay
confirmClockRunning(true);
}
[Test]
public void TestPauseWithResumeOverlay()
{
AddStep("move cursor to center", () => InputManager.MoveMouseTo(Player.ScreenSpaceDrawQuad.Centre));
AddUntilStep("wait for hitobjects", () => Player.ScoreProcessor.Health.Value < 1);
pauseAndConfirm();
resume();
confirmClockRunning(false);
confirmPauseOverlayShown(false);
pauseAndConfirm();
AddUntilStep("resume overlay is not active", () => Player.DrawableRuleset.ResumeOverlay.State.Value == Visibility.Hidden);
confirmPaused();
}
[Test]
public void TestResumeWithResumeOverlaySkipped()
{
@@ -7,11 +7,10 @@ using osu.Game.Online;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets.Osu;
using osu.Game.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Users;
using osuTK;
using System;
using System.Collections.Generic;
using osu.Game.Screens.Ranking.Pages;
namespace osu.Game.Tests.Visual.Gameplay
{
@@ -42,7 +41,6 @@ namespace osu.Game.Tests.Visual.Gameplay
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(80, 40),
};
});
}
@@ -3,6 +3,7 @@
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Play.PlayerSettings;
@@ -20,6 +21,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
State = { Value = Visibility.Visible }
});
Add(container = new ExampleContainer());
@@ -3,11 +3,16 @@
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Screens;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens;
using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking;
using osu.Game.Screens.Ranking.Pages;
@@ -22,11 +27,13 @@ namespace osu.Game.Tests.Visual.Gameplay
public override IReadOnlyList<Type> RequiredTypes => new[]
{
typeof(ScoreInfo),
typeof(Results),
typeof(ResultsPage),
typeof(ScoreResultsPage),
typeof(LocalLeaderboardPage)
typeof(RetryButton),
typeof(ReplayDownloadButton),
typeof(LocalLeaderboardPage),
typeof(TestPlayer)
};
[BackgroundDependencyLoader]
@@ -42,26 +49,82 @@ namespace osu.Game.Tests.Visual.Gameplay
var beatmapInfo = beatmaps.QueryBeatmap(b => b.RulesetID == 0);
if (beatmapInfo != null)
Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmapInfo);
}
LoadScreen(new SoloResults(new ScoreInfo
private TestSoloResults createResultsScreen() => new TestSoloResults(new ScoreInfo
{
TotalScore = 2845370,
Accuracy = 0.98,
MaxCombo = 123,
Rank = ScoreRank.A,
Date = DateTimeOffset.Now,
Statistics = new Dictionary<HitResult, int>
{
TotalScore = 2845370,
Accuracy = 0.98,
MaxCombo = 123,
Rank = ScoreRank.A,
Date = DateTimeOffset.Now,
Statistics = new Dictionary<HitResult, int>
{ HitResult.Great, 50 },
{ HitResult.Good, 20 },
{ HitResult.Meh, 50 },
{ HitResult.Miss, 1 }
},
User = new User
{
Username = "peppy",
}
});
[Test]
public void ResultsWithoutPlayer()
{
TestSoloResults screen = null;
AddStep("load results", () => Child = new OsuScreenStack(screen = createResultsScreen())
{
RelativeSizeAxes = Axes.Both
});
AddUntilStep("wait for loaded", () => screen.IsLoaded);
AddAssert("retry overlay not present", () => screen.RetryOverlay == null);
}
[Test]
public void ResultsWithPlayer()
{
TestSoloResults screen = null;
AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen()));
AddUntilStep("wait for loaded", () => screen.IsLoaded);
AddAssert("retry overlay present", () => screen.RetryOverlay != null);
}
private class TestResultsContainer : Container
{
[Cached(typeof(Player))]
private readonly Player player = new TestPlayer();
public TestResultsContainer(IScreen screen)
{
RelativeSizeAxes = Axes.Both;
InternalChild = new OsuScreenStack(screen)
{
{ HitResult.Great, 50 },
{ HitResult.Good, 20 },
{ HitResult.Meh, 50 },
{ HitResult.Miss, 1 }
},
User = new User
{
Username = "peppy",
}
}));
RelativeSizeAxes = Axes.Both,
};
}
}
private class TestSoloResults : SoloResults
{
public HotkeyRetryOverlay RetryOverlay;
public TestSoloResults(ScoreInfo score)
: base(score)
{
}
protected override void LoadComplete()
{
base.LoadComplete();
RetryOverlay = InternalChildren.OfType<HotkeyRetryOverlay>().SingleOrDefault();
}
}
}
}
@@ -33,23 +33,15 @@ namespace osu.Game.Tests.Visual.Menus
[Test]
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.AllowLoad.Set();
LoadScreen(loader);
});
AddUntilStep("loaded", () =>
{
logoVisible = loader.Logo?.Alpha > 0;
return loader.Logo != null && loader.ScreenLoaded;
});
AddAssert("logo was not visible", () => !logoVisible);
}
[Test]
@@ -58,7 +50,7 @@ namespace osu.Game.Tests.Visual.Menus
AddStep("begin loading", () => LoadScreen(loader = new TestLoader()));
AddUntilStep("wait for logo visible", () => loader.Logo?.Alpha > 0);
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);
}
@@ -0,0 +1,102 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Beatmaps;
using osu.Game.Overlays.BeatmapSet;
using osu.Game.Rulesets;
using System;
using System.Collections.Generic;
using System.Linq;
namespace osu.Game.Tests.Visual.Online
{
public class TestSceneBeatmapRulesetSelector : OsuTestScene
{
public override IReadOnlyList<Type> RequiredTypes => new[]
{
typeof(BeatmapRulesetSelector),
typeof(BeatmapRulesetTabItem),
};
private readonly TestRulesetSelector selector;
public TestSceneBeatmapRulesetSelector()
{
Add(selector = new TestRulesetSelector());
}
[Resolved]
private RulesetStore rulesets { get; set; }
[Test]
public void TestMultipleRulesetsBeatmapSet()
{
var enabledRulesets = rulesets.AvailableRulesets.Skip(1).Take(2);
AddStep("load multiple rulesets beatmapset", () =>
{
selector.BeatmapSet = new BeatmapSetInfo
{
Beatmaps = enabledRulesets.Select(r => new BeatmapInfo { Ruleset = r }).ToList()
};
});
var tabItems = selector.TabContainer.TabItems;
AddAssert("other rulesets disabled", () => tabItems.Except(tabItems.Where(t => enabledRulesets.Any(r => r.Equals(t.Value)))).All(t => !t.Enabled.Value));
AddAssert("left-most ruleset selected", () => tabItems.First(t => t.Enabled.Value).Active.Value);
}
[Test]
public void TestSingleRulesetBeatmapSet()
{
var enabledRuleset = rulesets.AvailableRulesets.Last();
AddStep("load single ruleset beatmapset", () =>
{
selector.BeatmapSet = new BeatmapSetInfo
{
Beatmaps = new List<BeatmapInfo>
{
new BeatmapInfo
{
Ruleset = enabledRuleset
}
}
};
});
AddAssert("single ruleset selected", () => selector.SelectedTab.Value.Equals(enabledRuleset));
}
[Test]
public void TestEmptyBeatmapSet()
{
AddStep("load empty beatmapset", () => selector.BeatmapSet = new BeatmapSetInfo
{
Beatmaps = new List<BeatmapInfo>()
});
AddAssert("no ruleset selected", () => selector.SelectedTab == null);
AddAssert("all rulesets disabled", () => selector.TabContainer.TabItems.All(t => !t.Enabled.Value));
}
[Test]
public void TestNullBeatmapSet()
{
AddStep("load null beatmapset", () => selector.BeatmapSet = null);
AddAssert("no ruleset selected", () => selector.SelectedTab == null);
AddAssert("all rulesets disabled", () => selector.TabContainer.TabItems.All(t => !t.Enabled.Value));
}
private class TestRulesetSelector : BeatmapRulesetSelector
{
public new TabItem<RulesetInfo> SelectedTab => base.SelectedTab;
public new TabFillFlowContainer TabContainer => base.TabContainer;
}
}
}
@@ -40,24 +40,19 @@ namespace osu.Game.Tests.Visual.Online
typeof(PreviewButton),
typeof(SuccessRate),
typeof(BeatmapAvailability),
typeof(BeatmapRulesetSelector),
typeof(BeatmapRulesetTabItem),
};
protected override bool UseOnlineAPI => true;
private RulesetInfo taikoRuleset;
private RulesetInfo maniaRuleset;
public TestSceneBeatmapSetOverlay()
{
Add(overlay = new TestBeatmapSetOverlay());
}
[BackgroundDependencyLoader]
private void load(RulesetStore rulesets)
{
taikoRuleset = rulesets.GetRuleset(1);
maniaRuleset = rulesets.GetRuleset(3);
}
[Resolved]
private RulesetStore rulesets { get; set; }
[Test]
public void TestLoading()
@@ -111,7 +106,7 @@ namespace osu.Game.Tests.Visual.Online
StarDifficulty = 9.99,
Version = @"TEST",
Length = 456000,
Ruleset = maniaRuleset,
Ruleset = rulesets.GetRuleset(3),
BaseDifficulty = new BeatmapDifficulty
{
CircleSize = 1,
@@ -189,7 +184,7 @@ namespace osu.Game.Tests.Visual.Online
StarDifficulty = 5.67,
Version = @"ANOTHER TEST",
Length = 123000,
Ruleset = taikoRuleset,
Ruleset = rulesets.GetRuleset(1),
BaseDifficulty = new BeatmapDifficulty
{
CircleSize = 9,
@@ -217,6 +212,54 @@ namespace osu.Game.Tests.Visual.Online
downloadAssert(false);
}
[Test]
public void TestMultipleRulesets()
{
AddStep("show multiple rulesets beatmap", () =>
{
var beatmaps = new List<BeatmapInfo>();
foreach (var ruleset in rulesets.AvailableRulesets.Skip(1))
{
beatmaps.Add(new BeatmapInfo
{
Version = ruleset.Name,
Ruleset = ruleset,
BaseDifficulty = new BeatmapDifficulty(),
OnlineInfo = new BeatmapOnlineInfo(),
Metrics = new BeatmapMetrics
{
Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6).ToArray(),
Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6).ToArray(),
},
});
}
overlay.ShowBeatmapSet(new BeatmapSetInfo
{
Metadata = new BeatmapMetadata
{
Title = @"multiple rulesets beatmap",
Artist = @"none",
Author = new User
{
Username = "BanchoBot",
Id = 3,
}
},
OnlineInfo = new BeatmapSetOnlineInfo
{
Covers = new BeatmapSetOnlineCovers(),
},
Metrics = new BeatmapSetMetrics { Ratings = Enumerable.Range(0, 11).ToArray() },
Beatmaps = beatmaps
});
});
AddAssert("shown beatmaps of current ruleset", () => overlay.Header.Picker.Difficulties.All(b => b.Beatmap.Ruleset.Equals(overlay.Header.RulesetSelector.Current.Value)));
AddAssert("left-most beatmap selected", () => overlay.Header.Picker.Difficulties.First().State == BeatmapPicker.DifficultySelectorState.Selected);
}
[Test]
public void TestHide()
{
@@ -281,12 +324,12 @@ namespace osu.Game.Tests.Visual.Online
private void downloadAssert(bool shown)
{
AddAssert($"is download button {(shown ? "shown" : "hidden")}", () => overlay.DownloadButtonsVisible == shown);
AddAssert($"is download button {(shown ? "shown" : "hidden")}", () => overlay.Header.DownloadButtonsVisible == shown);
}
private class TestBeatmapSetOverlay : BeatmapSetOverlay
{
public bool DownloadButtonsVisible => Header.DownloadButtonsVisible;
public new Header Header => base.Header;
}
}
}
@@ -22,7 +22,8 @@ namespace osu.Game.Tests.Visual.Online
typeof(HeaderButton),
typeof(SortTabControl),
typeof(ShowChildrenButton),
typeof(DeletedChildrenPlaceholder)
typeof(DeletedChildrenPlaceholder),
typeof(VotePill)
};
protected override bool UseOnlineAPI => true;
@@ -6,6 +6,11 @@ using osu.Framework.Graphics;
using osu.Game.Online.Chat;
using osu.Game.Users;
using osuTK;
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
{
@@ -41,14 +46,14 @@ namespace osu.Game.Tests.Visual.Online
[Cached]
private ChannelManager channelManager = new ChannelManager();
private readonly StandAloneChatDisplay chatDisplay;
private readonly StandAloneChatDisplay chatDisplay2;
private readonly TestStandAloneChatDisplay chatDisplay;
private readonly TestStandAloneChatDisplay chatDisplay2;
public TestSceneStandAloneChatDisplay()
{
Add(channelManager);
Add(chatDisplay = new StandAloneChatDisplay
Add(chatDisplay = new TestStandAloneChatDisplay
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
@@ -56,7 +61,7 @@ namespace osu.Game.Tests.Visual.Online
Size = new Vector2(400, 80)
});
Add(chatDisplay2 = new StandAloneChatDisplay(true)
Add(chatDisplay2 = new TestStandAloneChatDisplay(true)
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
@@ -111,6 +116,56 @@ namespace osu.Game.Tests.Visual.Online
Sender = longUsernameUser,
Content = "Hi guys, my new username is lit!"
}));
AddStep("message with new date", () => testChannel.AddNewMessages(new Message(sequence++)
{
Sender = longUsernameUser,
Content = "Message from the future!",
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}!"));
}
[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]
public void TestSortingWithFiltered()
{
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio.Track;
@@ -10,7 +11,6 @@ using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Lists;
using osu.Framework.Timing;
using osu.Game.Beatmaps.ControlPoints;
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)
{
@@ -11,7 +11,7 @@ using osuTK.Graphics;
namespace osu.Game.Tests.Visual.UserInterface
{
public class TestSceneLabelledComponent : OsuTestScene
public class TestSceneLabelledDrawable : OsuTestScene
{
[TestCase(false)]
[TestCase(true)]
@@ -25,7 +25,7 @@ namespace osu.Game.Tests.Visual.UserInterface
{
AddStep("create component", () =>
{
LabelledComponent<Drawable> component;
LabelledDrawable<Drawable> component;
Child = new Container
{
@@ -33,7 +33,7 @@ namespace osu.Game.Tests.Visual.UserInterface
Origin = Anchor.Centre,
Width = 500,
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";
@@ -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)
{
}
@@ -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)
{
}
@@ -7,7 +7,6 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
namespace osu.Game.Tests.Visual.UserInterface
@@ -28,7 +27,7 @@ namespace osu.Game.Tests.Visual.UserInterface
{
AddStep("create component", () =>
{
LabelledComponent<OsuTextBox> component;
LabelledTextBox component;
Child = new Container
{
+1 -1
View File
@@ -89,7 +89,7 @@ namespace osu.Game.Tournament.Screens
};
}
private class ActionableInfo : LabelledComponent<Drawable>
private class ActionableInfo : LabelledDrawable<Drawable>
{
private OsuButton button;
+9 -2
View File
@@ -392,8 +392,15 @@ namespace osu.Game.Beatmaps
req.Failure += e => { LogForModel(set, $"Online retrieval failed for {beatmap} ({e.Message})"); };
// intentionally blocking to limit web request concurrency
req.Perform(api);
try
{
// intentionally blocking to limit web request concurrency
req.Perform(api);
}
catch (Exception e)
{
LogForModel(set, $"Online retrieval failed for {beatmap} ({e.Message})");
}
}
}
}
@@ -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.
using System;
namespace osu.Game.Beatmaps.ControlPoints
{
public class ControlPoint : IComparable<ControlPoint>, IEquatable<ControlPoint>
public abstract class ControlPoint : IComparable<ControlPoint>, IEquatable<ControlPoint>
{
/// <summary>
/// The time at which the control point takes effect.
/// </summary>
public double Time;
public double Time => controlPointGroup?.Time ?? 0;
/// <summary>
/// Whether this timing point was generated internally, as opposed to parsed from the underlying beatmap.
/// </summary>
internal bool AutoGenerated;
private ControlPointGroup controlPointGroup;
public void AttachGroup(ControlPointGroup pointGroup) => this.controlPointGroup = pointGroup;
public int CompareTo(ControlPoint other) => Time.CompareTo(other.Time);
public bool Equals(ControlPoint other)
=> Time.Equals(other?.Time);
/// <summary>
/// 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);
}
}
@@ -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.Linq;
using Newtonsoft.Json;
using osu.Framework.Bindables;
using osu.Framework.Lists;
namespace osu.Game.Beatmaps.ControlPoints
@@ -12,57 +13,78 @@ namespace osu.Game.Beatmaps.ControlPoints
[Serializable]
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>
/// All timing points.
/// </summary>
[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>
/// All difficulty points.
/// </summary>
[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>
/// All sound points.
/// </summary>
[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>
/// All effect points.
/// </summary>
[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>
/// Finds the difficulty control point that is active at <paramref name="time"/>.
/// </summary>
/// <param name="time">The time to find the difficulty control point at.</param>
/// <returns>The difficulty control point.</returns>
public DifficultyControlPoint DifficultyPointAt(double time) => binarySearch(DifficultyPoints, time);
public DifficultyControlPoint DifficultyPointAt(double time) => binarySearchWithFallback(DifficultyPoints, time);
/// <summary>
/// Finds the effect control point that is active at <paramref name="time"/>.
/// </summary>
/// <param name="time">The time to find the effect control point at.</param>
/// <returns>The effect control point.</returns>
public EffectControlPoint EffectPointAt(double time) => binarySearch(EffectPoints, time);
public EffectControlPoint EffectPointAt(double time) => binarySearchWithFallback(EffectPoints, time);
/// <summary>
/// Finds the sound control point that is active at <paramref name="time"/>.
/// </summary>
/// <param name="time">The time to find the sound control point at.</param>
/// <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>
/// Finds the timing control point that is active at <paramref name="time"/>.
/// </summary>
/// <param name="time">The time to find the timing control point at.</param>
/// <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>
/// Finds the maximum BPM represented by any timing control point.
@@ -85,24 +107,93 @@ namespace osu.Game.Beatmaps.ControlPoints
public double BPMMode =>
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>
/// 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>
/// <param name="list">The list to search.</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>
/// <returns>The active control point at <paramref name="time"/>.</returns>
private T binarySearch<T>(SortedList<T> list, double time, T prePoint = null)
/// <returns>The active control point at <paramref name="time"/>, or a fallback <see cref="ControlPoint"/> if none found.</returns>
private T binarySearchWithFallback<T>(IReadOnlyList<T> list, double time, T prePoint = null)
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)
throw new ArgumentNullException(nameof(list));
if (list.Count == 0)
return new T();
return null;
if (time < list[0].Time)
return prePoint ?? new T();
return null;
if (time >= list[list.Count - 1].Time)
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
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.
// See the LICENCE file in the repository root for full licence text.
using System;
using osuTK;
using osu.Framework.Bindables;
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>
/// The speed multiplier at this control point.
/// </summary>
public double SpeedMultiplier
{
get => speedMultiplier;
set => speedMultiplier = MathHelper.Clamp(value, 0.1, 10);
get => SpeedMultiplierBindable.Value;
set => SpeedMultiplierBindable.Value = value;
}
private double speedMultiplier = 1;
public bool Equals(DifficultyControlPoint other)
=> base.Equals(other)
&& SpeedMultiplier.Equals(other?.SpeedMultiplier);
public override bool EquivalentTo(ControlPoint other) =>
other is DifficultyControlPoint otherTyped && otherTyped.SpeedMultiplier.Equals(SpeedMultiplier);
}
}
@@ -1,24 +1,42 @@
// 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.Bindables;
namespace osu.Game.Beatmaps.ControlPoints
{
public class EffectControlPoint : ControlPoint, IEquatable<EffectControlPoint>
public class EffectControlPoint : ControlPoint
{
/// <summary>
/// Whether this control point enables Kiai mode.
/// Whether the first bar line of this control point is ignored.
/// </summary>
public bool KiaiMode;
public readonly BindableBool OmitFirstBarLineBindable = new BindableBool();
/// <summary>
/// Whether the first bar line of this control point is ignored.
/// </summary>
public bool OmitFirstBarLine;
public bool OmitFirstBarLine
{
get => OmitFirstBarLineBindable.Value;
set => OmitFirstBarLineBindable.Value = value;
}
public bool Equals(EffectControlPoint other)
=> base.Equals(other)
&& KiaiMode == other?.KiaiMode && OmitFirstBarLine == other.OmitFirstBarLine;
/// <summary>
/// Whether this control point enables Kiai mode.
/// </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.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Bindables;
using osu.Game.Audio;
namespace osu.Game.Beatmaps.ControlPoints
{
public class SampleControlPoint : ControlPoint, IEquatable<SampleControlPoint>
public class SampleControlPoint : ControlPoint
{
public const string DEFAULT_BANK = "normal";
/// <summary>
/// The default sample bank at this control point.
/// </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>
/// The default sample volume at this control point.
/// </summary>
public int SampleVolume = 100;
public int SampleVolume
{
get => SampleVolumeBindable.Value;
set => SampleVolumeBindable.Value = value;
}
/// <summary>
/// Create a SampleInfo based on the sample settings in this control point.
@@ -45,8 +68,8 @@ namespace osu.Game.Beatmaps.ControlPoints
return newSampleInfo;
}
public bool Equals(SampleControlPoint other)
=> base.Equals(other)
&& string.Equals(SampleBank, other?.SampleBank) && SampleVolume == other?.SampleVolume;
public override bool EquivalentTo(ControlPoint other) =>
other is SampleControlPoint otherTyped &&
string.Equals(SampleBank, otherTyped.SampleBank) && SampleVolume == otherTyped.SampleVolume;
}
}
@@ -1,34 +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 osuTK;
using osu.Framework.Bindables;
using osu.Game.Beatmaps.Timing;
namespace osu.Game.Beatmaps.ControlPoints
{
public class TimingControlPoint : ControlPoint, IEquatable<TimingControlPoint>
public class TimingControlPoint : ControlPoint
{
/// <summary>
/// The time signature at this control point.
/// </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;
/// <summary>
/// The beat length at this control point.
/// </summary>
public virtual double BeatLength
public readonly BindableDouble BeatLengthBindable = new BindableDouble(DEFAULT_BEAT_LENGTH)
{
get => beatLength;
set => beatLength = MathHelper.Clamp(value, 6, 60000);
Default = DEFAULT_BEAT_LENGTH,
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;
public bool Equals(TimingControlPoint other)
=> base.Equals(other)
&& TimeSignature == other?.TimeSignature && beatLength.Equals(other.beatLength);
public override bool EquivalentTo(ControlPoint other) =>
other is TimingControlPoint otherTyped
&& TimeSignature == otherTyped.TimeSignature && BeatLength.Equals(otherTyped.BeatLength);
}
}
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using osu.Framework.IO.File;
@@ -50,6 +51,8 @@ namespace osu.Game.Beatmaps.Formats
base.ParseStreamInto(stream, beatmap);
flushPendingPoints();
// 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).
// 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)
{
var controlPoint = CreateTimingControlPoint();
controlPoint.Time = time;
controlPoint.BeatLength = beatLength;
controlPoint.TimeSignature = timeSignature;
handleTimingControlPoint(controlPoint);
addControlPoint(time, controlPoint, true);
}
handleDifficultyControlPoint(new DifficultyControlPoint
addControlPoint(time, new LegacyDifficultyControlPoint
{
Time = time,
SpeedMultiplier = speedMultiplier,
AutoGenerated = timingChange
});
}, timingChange);
handleEffectControlPoint(new EffectControlPoint
addControlPoint(time, new EffectControlPoint
{
Time = time,
KiaiMode = kiaiMode,
OmitFirstBarLine = omitFirstBarSignature,
AutoGenerated = timingChange
});
}, timingChange);
handleSampleControlPoint(new LegacySampleControlPoint
addControlPoint(time, new LegacySampleControlPoint
{
Time = time,
SampleBank = stringSampleSet,
SampleVolume = sampleVolume,
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.
// this allows for incorrectly ordered timing points to still be correctly handled.
if (newPoint.AutoGenerated && !existing.AutoGenerated)
return;
beatmap.ControlPointInfo.TimingPoints.Remove(existing);
beatmap.ControlPointInfo.Add(time, point);
return;
}
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)
{
// 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);
pendingControlPoints.Clear();
}
private void handleHitObject(string line)
+12 -4
View File
@@ -189,7 +189,15 @@ namespace osu.Game.Beatmaps.Formats
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;
@@ -203,9 +211,9 @@ namespace osu.Game.Beatmaps.Formats
return baseInfo;
}
public bool Equals(LegacySampleControlPoint other)
=> base.Equals(other)
&& CustomSampleBank == other?.CustomSampleBank;
public override bool EquivalentTo(ControlPoint other) =>
base.EquivalentTo(other) && other is LegacySampleControlPoint otherTyped &&
CustomSampleBank == otherTyped.CustomSampleBank;
}
}
}
@@ -28,11 +28,15 @@ namespace osu.Game.Beatmaps.Formats
}
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;
}
}
}
}
+3 -1
View File
@@ -108,7 +108,7 @@ namespace osu.Game.Database
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.Text = $"{HumanisedModelName.Humanize(LetterCasing.Title)} import is initialising...";
@@ -168,6 +168,8 @@ namespace osu.Game.Database
notification.State = ProgressNotificationState.Completed;
}
return imported;
}
/// <summary>
@@ -76,21 +76,17 @@ namespace osu.Game.Database
Task.Factory.StartNew(async () =>
{
// 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);
}, TaskCreationOptions.LongRunning);
};
request.Failure += error =>
{
DownloadFailed?.Invoke(request);
if (error is OperationCanceledException) return;
notification.State = ProgressNotificationState.Cancelled;
Logger.Error(error, $"{HumanisedModelName.Titleize()} download failed!");
currentDownloads.Remove(request);
};
request.Failure += triggerFailure;
notification.CancelRequested += () =>
{
@@ -103,11 +99,31 @@ namespace osu.Game.Database
currentDownloads.Add(request);
PostNotification?.Invoke(notification);
Task.Factory.StartNew(() => request.Perform(api), TaskCreationOptions.LongRunning);
Task.Factory.StartNew(() =>
{
try
{
request.Perform(api);
}
catch (Exception error)
{
triggerFailure(error);
}
}, TaskCreationOptions.LongRunning);
DownloadBegan?.Invoke(request);
return true;
void triggerFailure(Exception error)
{
DownloadFailed?.Invoke(request);
if (error is OperationCanceledException) return;
notification.State = ProgressNotificationState.Cancelled;
Logger.Error(error, $"{HumanisedModelName.Titleize()} download failed!");
currentDownloads.Remove(request);
}
}
public bool IsAvailableLocally(TModel model) => CheckLocalAvailability(model, modelStore.ConsumableItems.Where(m => !m.DeletePending));
-13
View File
@@ -166,19 +166,6 @@ namespace osu.Game.Database
// 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
{
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
{
BeatLength = default_beat_length,
AutoGenerated = true,
Time = 0
};
defaultEffect = new EffectControlPoint
{
Time = 0,
AutoGenerated = true,
KiaiMode = false,
OmitFirstBarLine = false
};
@@ -8,9 +8,6 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics.Sprites;
using System.Collections.Generic;
using osu.Framework.Graphics;
using osu.Framework.Logging;
using osu.Game.Overlays;
using osu.Game.Overlays.Notifications;
using osu.Game.Users;
namespace osu.Game.Graphics.Containers
@@ -23,21 +20,12 @@ namespace osu.Game.Graphics.Containers
}
private OsuGame game;
private ChannelManager channelManager;
private Action showNotImplementedError;
[BackgroundDependencyLoader(true)]
private void load(OsuGame game, NotificationOverlay notifications, ChannelManager channelManager)
private void load(OsuGame game)
{
// will be null in tests
this.game = game;
this.channelManager = channelManager;
showNotImplementedError = () => notifications?.Post(new SimpleNotification
{
Text = @"This link type is not yet supported!",
Icon = FontAwesome.Solid.LifeRing,
});
}
public void AddLinks(string text, List<Link> links)
@@ -56,85 +44,47 @@ namespace osu.Game.Graphics.Containers
foreach (var link in links)
{
AddText(text.Substring(previousLinkEnd, link.Index - previousLinkEnd));
AddLink(text.Substring(link.Index, link.Length), link.Url, link.Action, link.Argument);
AddLink(text.Substring(link.Index, link.Length), link.Action, link.Argument ?? link.Url);
previousLinkEnd = link.Index + link.Length;
}
AddText(text.Substring(previousLinkEnd));
}
public IEnumerable<Drawable> AddLink(string text, string url, LinkAction linkType = LinkAction.External, string linkArgument = null, string tooltipText = null, Action<SpriteText> creationParameters = null)
=> createLink(AddText(text, creationParameters), text, url, linkType, linkArgument, tooltipText);
public void AddLink(string text, string url, Action<SpriteText> creationParameters = null) =>
createLink(AddText(text, creationParameters), new LinkDetails(LinkAction.External, url), url);
public IEnumerable<Drawable> AddLink(string text, Action action, string tooltipText = null, Action<SpriteText> creationParameters = null)
=> createLink(AddText(text, creationParameters), text, tooltipText: tooltipText, action: action);
public void AddLink(string text, Action action, string tooltipText = null, Action<SpriteText> creationParameters = null)
=> createLink(AddText(text, creationParameters), new LinkDetails(LinkAction.Custom, null), tooltipText, action);
public IEnumerable<Drawable> AddLink(IEnumerable<SpriteText> text, string url, LinkAction linkType = LinkAction.External, string linkArgument = null, string tooltipText = null)
public void AddLink(string text, LinkAction action, string argument, string tooltipText = null, Action<SpriteText> creationParameters = null)
=> createLink(AddText(text, creationParameters), new LinkDetails(action, argument), null);
public void AddLink(IEnumerable<SpriteText> text, LinkAction action = LinkAction.External, string linkArgument = null, string tooltipText = null)
{
foreach (var t in text)
AddArbitraryDrawable(t);
return createLink(text, null, url, linkType, linkArgument, tooltipText);
createLink(text, new LinkDetails(action, linkArgument), tooltipText);
}
public IEnumerable<Drawable> AddUserLink(User user, Action<SpriteText> creationParameters = null)
=> createLink(AddText(user.Username, creationParameters), user.Username, null, LinkAction.OpenUserProfile, user.Id.ToString(), "View profile");
public void AddUserLink(User user, Action<SpriteText> creationParameters = null)
=> createLink(AddText(user.Username, creationParameters), new LinkDetails(LinkAction.OpenUserProfile, user.Id.ToString()), "View Profile");
private IEnumerable<Drawable> createLink(IEnumerable<Drawable> drawables, string text, string url = null, LinkAction linkType = LinkAction.External, string linkArgument = null, string tooltipText = null, Action action = null)
private void createLink(IEnumerable<Drawable> drawables, LinkDetails link, string tooltipText, Action action = null)
{
AddInternal(new DrawableLinkCompiler(drawables.OfType<SpriteText>().ToList())
{
RelativeSizeAxes = Axes.Both,
TooltipText = tooltipText ?? (url != text ? url : string.Empty),
Action = action ?? (() =>
TooltipText = tooltipText,
Action = () =>
{
switch (linkType)
{
case LinkAction.OpenBeatmap:
// TODO: proper query params handling
if (linkArgument != null && int.TryParse(linkArgument.Contains('?') ? linkArgument.Split('?')[0] : linkArgument, out int beatmapId))
game?.ShowBeatmap(beatmapId);
break;
case LinkAction.OpenBeatmapSet:
if (int.TryParse(linkArgument, out int setId))
game?.ShowBeatmapSet(setId);
break;
case LinkAction.OpenChannel:
try
{
channelManager?.OpenChannel(linkArgument);
}
catch (ChannelNotFoundException)
{
Logger.Log($"The requested channel \"{linkArgument}\" does not exist");
}
break;
case LinkAction.OpenEditorTimestamp:
case LinkAction.JoinMultiplayerMatch:
case LinkAction.Spectate:
showNotImplementedError?.Invoke();
break;
case LinkAction.External:
game?.OpenUrlExternally(url);
break;
case LinkAction.OpenUserProfile:
if (long.TryParse(linkArgument, out long userId))
game?.ShowUser(userId);
break;
default:
throw new NotImplementedException($"This {nameof(LinkAction)} ({linkType.ToString()}) is missing an associated action.");
}
}),
if (action != null)
action();
else
game.HandleLink(link);
},
});
return drawables;
}
// We want the compilers to always be visible no matter where they are, so RelativeSizeAxes is used.
@@ -159,8 +159,15 @@ namespace osu.Game.Graphics.Containers
Height = Parent.Parent.DrawSize.Y * 1.5f;
}
protected override void PopIn() => this.MoveToY(FinalPosition, APPEAR_DURATION, easing_show);
protected override void PopOut() => this.MoveToY(Parent.Parent.DrawSize.Y, DISAPPEAR_DURATION, easing_hide);
protected override void PopIn() => Schedule(() => this.MoveToY(FinalPosition, APPEAR_DURATION, easing_show));
protected override void PopOut()
{
double duration = IsLoaded ? DISAPPEAR_DURATION : 0;
// scheduling is required as parent may not be present at the time this is called.
Schedule(() => this.MoveToY(Parent.Parent.DrawSize.Y, duration, easing_hide));
}
}
}
}
@@ -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 -1
View File
@@ -59,7 +59,7 @@ namespace osu.Game.Graphics.UserInterface
protected override bool OnHover(HoverEvent e)
{
hover.FadeIn(200);
return base.OnHover(e);
return true;
}
protected override void OnHoverLost(HoverLostEvent e)
@@ -17,7 +17,7 @@ using osu.Framework.Input.Events;
namespace osu.Game.Graphics.UserInterface
{
public class OsuSliderBar<T> : SliderBar<T>, IHasTooltip, IHasAccentColour
where T : struct, IEquatable<T>, IComparable, IConvertible
where T : struct, IEquatable<T>, IComparable<T>, IConvertible
{
/// <summary>
/// Maximum number of decimal digits to be displayed in the tooltip.
@@ -14,8 +14,6 @@ namespace osu.Game.Graphics.UserInterface
{
protected virtual bool AllowCommit => false;
public override bool HandleLeftRightArrows => false;
public SearchTextBox()
{
Height = 35;
@@ -0,0 +1,13 @@
// 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.
namespace osu.Game.Graphics.UserInterface
{
/// <summary>
/// A <see cref="SearchTextBox"/> which does not handle left/right arrow keys for seeking.
/// </summary>
public class SeekLimitedSearchTextBox : SearchTextBox
{
public override bool HandleLeftRightArrows => false;
}
}
@@ -5,8 +5,6 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osuTK;
using osuTK.Graphics;
@@ -14,9 +12,9 @@ using System.Collections.Generic;
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;
@@ -32,100 +30,55 @@ namespace osu.Game.Graphics.UserInterface
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 };
private ChevronIcon leftChevron;
private ChevronIcon rightChevron;
private SpriteText text;
private Box background;
private FillFlowContainer textContainer;
public ShowMoreButton()
{
AutoSizeAxes = Axes.Both;
}
protected override Drawable CreateContent() => new CircularContainer
{
Masking = true,
Size = new Vector2(140, 30),
Children = new Drawable[]
{
new CircularContainer
background = new Box
{
Masking = true,
Size = new Vector2(140, 30),
RelativeSizeAxes = Axes.Both,
},
textContainer = new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(7),
Children = new Drawable[]
{
background = new Box
{
RelativeSizeAxes = Axes.Both,
},
content = new FillFlowContainer
leftChevron = new ChevronIcon(),
text = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
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)
Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold),
Text = "show more".ToUpper(),
},
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.
IsLoading = true;
}
}
};
protected override void OnLoadStarted() => textContainer.FadeOut(duration, Easing.OutQuint);
protected override void OnLoadFinished() => textContainer.FadeIn(duration, Easing.OutQuint);
private class ChevronIcon : SpriteIcon
{
@@ -1,132 +1,24 @@
// 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.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics.Containers;
using osuTK;
using osu.Framework.Graphics.UserInterface;
namespace osu.Game.Graphics.UserInterfaceV2
{
public abstract class LabelledComponent<T> : CompositeDrawable
where T : Drawable
public abstract class LabelledComponent<T, U> : LabelledDrawable<T>, IHasCurrentValue<U>
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)
: 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]
private void load(OsuColour osuColour)
public Bindable<U> Current
{
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();
}
}
@@ -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
{
public class LabelledSwitchButton : LabelledComponent<SwitchButton>
public class LabelledSwitchButton : LabelledComponent<SwitchButton, bool>
{
public LabelledSwitchButton()
: base(true)
@@ -8,7 +8,7 @@ using osu.Game.Graphics.UserInterface;
namespace osu.Game.Graphics.UserInterfaceV2
{
public class LabelledTextBox : LabelledComponent<OsuTextBox>
public class LabelledTextBox : LabelledComponent<OsuTextBox, string>
{
public event TextBox.OnCommitHandler OnCommit;
@@ -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 IsVoted { get; set; }
public string GetMessage => HasMessage ? WebUtility.HtmlDecode(Regex.Replace(MessageHtml, @"<(.|\n)*?>", string.Empty)) : string.Empty;
public int DeletedChildrenCount => ChildComments.Count(c => c.IsDeleted);
@@ -47,6 +47,22 @@ namespace osu.Game.Online.API.Requests.Responses
[JsonProperty(@"included_comments")]
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;
[JsonProperty(@"users")]
+3 -5
View File
@@ -14,7 +14,7 @@ namespace osu.Game.Online.Chat
{
public class Channel
{
public readonly int MaxHistory = 300;
public const int MAX_HISTORY = 300;
/// <summary>
/// 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>
public Bindable<bool> Joined = new Bindable<bool>();
public const int MAX_HISTORY = 300;
[JsonConstructor]
public Channel()
{
@@ -162,8 +160,8 @@ namespace osu.Game.Online.Chat
{
// never purge local echos
int messageCount = Messages.Count - pendingMessages.Count;
if (messageCount > MaxHistory)
Messages.RemoveRange(0, messageCount - MaxHistory);
if (messageCount > MAX_HISTORY)
Messages.RemoveRange(0, messageCount - MAX_HISTORY);
}
}
}
+25 -16
View File
@@ -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)(?!)))\]");
// [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
// 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.
result.Links.ForEach(l => l.Index -= l.Index > index ? m.Length - displayText.Length : 0);
var details = getLinkDetails(linkText);
var details = GetLinkDetails(linkText);
result.Links.Add(new Link(linkText, index, displayText.Length, linkActionOverride ?? details.Action, details.Argument));
//adjust the offset for processing the current matches group.
@@ -95,15 +95,21 @@ namespace osu.Game.Online.Chat
foreach (Match m in regex.Matches(result.Text, startIndex))
{
var index = m.Index;
var link = m.Groups["link"].Value;
var indexLength = link.Length;
var linkText = m.Groups["link"].Value;
var indexLength = linkText.Length;
var details = getLinkDetails(link);
result.Links.Add(new Link(link, index, indexLength, details.Action, details.Argument));
var details = GetLinkDetails(linkText);
var link = new Link(linkText, index, indexLength, details.Action, details.Argument);
// sometimes an already-processed formatted link can reduce to a simple URL, too
// (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);
args[0] = args[0].TrimEnd(':');
@@ -249,17 +255,17 @@ namespace osu.Game.Online.Chat
OriginalText = Text = text;
}
}
}
public class LinkDetails
public class LinkDetails
{
public LinkAction Action;
public string Argument;
public LinkDetails(LinkAction action, string argument)
{
public LinkAction Action;
public string Argument;
public LinkDetails(LinkAction action, string argument)
{
Action = action;
Argument = argument;
}
Action = action;
Argument = argument;
}
}
@@ -273,6 +279,7 @@ namespace osu.Game.Online.Chat
JoinMultiplayerMatch,
Spectate,
OpenUserProfile,
Custom
}
public class Link : IComparable<Link>
@@ -292,6 +299,8 @@ namespace osu.Game.Online.Chat
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;
}
}
@@ -8,6 +8,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.Chat;
using osuTK.Graphics;
@@ -124,6 +125,8 @@ namespace osu.Game.Online.Chat
protected override ChatLine CreateChatLine(Message m) => CreateChatLineAction(m);
protected override DaySeparator CreateDaySeparator(DateTimeOffset time) => new CustomDaySeparator(time);
public StandAloneDrawableChannel(Channel channel)
: base(channel)
{
@@ -134,6 +137,24 @@ namespace osu.Game.Online.Chat
{
ChatLineFlow.Padding = new MarginPadding { Horizontal = 0 };
}
private class CustomDaySeparator : DaySeparator
{
public CustomDaySeparator(DateTimeOffset time)
: base(time)
{
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
Colour = colours.Yellow;
TextSize = 14;
LineHeight = 1;
Padding = new MarginPadding { Horizontal = 10 };
Margin = new MarginPadding { Vertical = 5 };
}
}
}
protected class StandAloneMessage : ChatLine
@@ -21,6 +21,7 @@ using osu.Game.Users.Drawables;
using osuTK;
using osuTK.Graphics;
using Humanizer;
using osu.Game.Online.API;
namespace osu.Game.Online.Leaderboards
{
@@ -37,6 +38,7 @@ namespace osu.Game.Online.Leaderboards
private readonly ScoreInfo score;
private readonly int rank;
private readonly bool allowHighlight;
private Box background;
private Container content;
@@ -49,17 +51,18 @@ namespace osu.Game.Online.Leaderboards
private List<ScoreComponentLabel> statisticsLabels;
public LeaderboardScore(ScoreInfo score, int rank)
public LeaderboardScore(ScoreInfo score, int rank, bool allowHighlight = true)
{
this.score = score;
this.rank = rank;
this.allowHighlight = allowHighlight;
RelativeSizeAxes = Axes.X;
Height = HEIGHT;
}
[BackgroundDependencyLoader]
private void load()
private void load(IAPIProvider api, OsuColour colour)
{
var user = score.User;
@@ -100,7 +103,7 @@ namespace osu.Game.Online.Leaderboards
background = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.Black,
Colour = user.Id == api.LocalUser.Value.Id && allowHighlight ? colour.Green : Color4.Black,
Alpha = background_alpha,
},
},
+93 -5
View File
@@ -215,31 +215,102 @@ namespace osu.Game
private ExternalLinkOpener externalLinkOpener;
public void OpenUrlExternally(string url)
/// <summary>
/// Handle an arbitrary URL. Displays via in-game overlays where possible.
/// This can be called from a non-thread-safe non-game-loaded state.
/// </summary>
/// <param name="url">The URL to load.</param>
public void HandleLink(string url) => HandleLink(MessageFormatter.GetLinkDetails(url));
/// <summary>
/// Handle a specific <see cref="LinkDetails"/>.
/// This can be called from a non-thread-safe non-game-loaded state.
/// </summary>
/// <param name="link">The link to load.</param>
public void HandleLink(LinkDetails link) => Schedule(() =>
{
switch (link.Action)
{
case LinkAction.OpenBeatmap:
// TODO: proper query params handling
if (link.Argument != null && int.TryParse(link.Argument.Contains('?') ? link.Argument.Split('?')[0] : link.Argument, out int beatmapId))
ShowBeatmap(beatmapId);
break;
case LinkAction.OpenBeatmapSet:
if (int.TryParse(link.Argument, out int setId))
ShowBeatmapSet(setId);
break;
case LinkAction.OpenChannel:
ShowChannel(link.Argument);
break;
case LinkAction.OpenEditorTimestamp:
case LinkAction.JoinMultiplayerMatch:
case LinkAction.Spectate:
waitForReady(() => notifications, _ => notifications?.Post(new SimpleNotification
{
Text = @"This link type is not yet supported!",
Icon = FontAwesome.Solid.LifeRing,
}));
break;
case LinkAction.External:
OpenUrlExternally(link.Argument);
break;
case LinkAction.OpenUserProfile:
if (long.TryParse(link.Argument, out long userId))
ShowUser(userId);
break;
default:
throw new NotImplementedException($"This {nameof(LinkAction)} ({link.Action.ToString()}) is missing an associated action.");
}
});
public void OpenUrlExternally(string url) => waitForReady(() => externalLinkOpener, _ =>
{
if (url.StartsWith("/"))
url = $"{API.Endpoint}{url}";
externalLinkOpener.OpenUrlExternally(url);
}
});
/// <summary>
/// Open a specific channel in chat.
/// </summary>
/// <param name="channel">The channel to display.</param>
public void ShowChannel(string channel) => waitForReady(() => channelManager, _ =>
{
try
{
channelManager.OpenChannel(channel);
}
catch (ChannelNotFoundException)
{
Logger.Log($"The requested channel \"{channel}\" does not exist");
}
});
/// <summary>
/// Show a beatmap set as an overlay.
/// </summary>
/// <param name="setId">The set to display.</param>
public void ShowBeatmapSet(int setId) => beatmapSetOverlay.FetchAndShowBeatmapSet(setId);
public void ShowBeatmapSet(int setId) => waitForReady(() => beatmapSetOverlay, _ => beatmapSetOverlay.FetchAndShowBeatmapSet(setId));
/// <summary>
/// Show a user's profile as an overlay.
/// </summary>
/// <param name="userId">The user to display.</param>
public void ShowUser(long userId) => userProfile.ShowUser(userId);
public void ShowUser(long userId) => waitForReady(() => userProfile, _ => userProfile.ShowUser(userId));
/// <summary>
/// Show a beatmap's set as an overlay, displaying the given beatmap.
/// </summary>
/// <param name="beatmapId">The beatmap to show.</param>
public void ShowBeatmap(int beatmapId) => beatmapSetOverlay.FetchAndShowBeatmap(beatmapId);
public void ShowBeatmap(int beatmapId) => waitForReady(() => beatmapSetOverlay, _ => beatmapSetOverlay.FetchAndShowBeatmap(beatmapId));
/// <summary>
/// Present a beatmap at song select immediately.
@@ -397,6 +468,23 @@ namespace osu.Game
performFromMainMenuTask = Schedule(() => performFromMainMenu(action, taskName));
}
/// <summary>
/// Wait for the game (and target component) to become loaded and then run an action.
/// </summary>
/// <param name="retrieveInstance">A function to retrieve a (potentially not-yet-constructed) target instance.</param>
/// <param name="action">The action to perform on the instance when load is confirmed.</param>
/// <typeparam name="T">The type of the target instance.</typeparam>
private void waitForReady<T>(Func<T> retrieveInstance, Action<T> action)
where T : Drawable
{
var instance = retrieveInstance();
if (ScreenStack == null || ScreenStack.CurrentScreen is StartupScreen || instance?.IsLoaded != true)
Schedule(() => waitForReady(retrieveInstance, action));
else
action(instance);
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
@@ -138,18 +138,13 @@ namespace osu.Game.Overlays.AccountCreation
passwordTextBox.Current.ValueChanged += password => { characterCheckText.ForEach(s => s.Colour = password.NewValue.Length == 0 ? Color4.White : Interpolation.ValueAt(password.NewValue.Length, Color4.OrangeRed, Color4.YellowGreen, 0, 8, Easing.In)); };
}
protected override void Update()
{
base.Update();
if (host?.OnScreenKeyboardOverlapsGameWindow != true && !textboxes.Any(t => t.HasFocus))
focusNextTextbox();
}
public override void OnEntering(IScreen last)
{
base.OnEntering(last);
processingOverlay.Hide();
if (host?.OnScreenKeyboardOverlapsGameWindow != true)
focusNextTextbox();
}
private void performRegistration()
+40 -34
View File
@@ -17,6 +17,7 @@ using osu.Game.Beatmaps.Drawables;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets;
using osuTK;
using osuTK.Graphics;
@@ -27,10 +28,11 @@ namespace osu.Game.Overlays.BeatmapSet
private const float tile_icon_padding = 7;
private const float tile_spacing = 2;
private readonly DifficultiesContainer difficulties;
private readonly OsuSpriteText version, starRating;
private readonly Statistic plays, favourites;
public readonly DifficultiesContainer Difficulties;
public readonly Bindable<BeatmapInfo> Beatmap = new Bindable<BeatmapInfo>();
private BeatmapSetInfo beatmapSet;
@@ -43,38 +45,10 @@ namespace osu.Game.Overlays.BeatmapSet
if (value == beatmapSet) return;
beatmapSet = value;
updateDisplay();
}
}
private void updateDisplay()
{
difficulties.Clear();
if (BeatmapSet != null)
{
difficulties.ChildrenEnumerable = BeatmapSet.Beatmaps.OrderBy(beatmap => beatmap.StarDifficulty).Select(b => new DifficultySelectorButton(b)
{
State = DifficultySelectorState.NotSelected,
OnHovered = beatmap =>
{
showBeatmap(beatmap);
starRating.Text = beatmap.StarDifficulty.ToString("Star Difficulty 0.##");
starRating.FadeIn(100);
},
OnClicked = beatmap => { Beatmap.Value = beatmap; },
});
}
starRating.FadeOut(100);
Beatmap.Value = BeatmapSet?.Beatmaps.FirstOrDefault();
plays.Value = BeatmapSet?.OnlineInfo.PlayCount ?? 0;
favourites.Value = BeatmapSet?.OnlineInfo.FavouriteCount ?? 0;
updateDifficultyButtons();
}
public BeatmapPicker()
{
RelativeSizeAxes = Axes.X;
@@ -89,7 +63,7 @@ namespace osu.Game.Overlays.BeatmapSet
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
difficulties = new DifficultiesContainer
Difficulties = new DifficultiesContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
@@ -147,6 +121,9 @@ namespace osu.Game.Overlays.BeatmapSet
};
}
[Resolved]
private IBindable<RulesetInfo> ruleset { get; set; }
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
@@ -158,10 +135,39 @@ namespace osu.Game.Overlays.BeatmapSet
{
base.LoadComplete();
ruleset.ValueChanged += r => updateDisplay();
// done here so everything can bind in intialization and get the first trigger
Beatmap.TriggerChange();
}
private void updateDisplay()
{
Difficulties.Clear();
if (BeatmapSet != null)
{
Difficulties.ChildrenEnumerable = BeatmapSet.Beatmaps.Where(b => b.Ruleset.Equals(ruleset.Value)).OrderBy(b => b.StarDifficulty).Select(b => new DifficultySelectorButton(b)
{
State = DifficultySelectorState.NotSelected,
OnHovered = beatmap =>
{
showBeatmap(beatmap);
starRating.Text = beatmap.StarDifficulty.ToString("Star Difficulty 0.##");
starRating.FadeIn(100);
},
OnClicked = beatmap => { Beatmap.Value = beatmap; },
});
}
starRating.FadeOut(100);
Beatmap.Value = Difficulties.FirstOrDefault()?.Beatmap;
plays.Value = BeatmapSet?.OnlineInfo.PlayCount ?? 0;
favourites.Value = BeatmapSet?.OnlineInfo.FavouriteCount ?? 0;
updateDifficultyButtons();
}
private void showBeatmap(BeatmapInfo beatmap)
{
version.Text = beatmap?.Version;
@@ -169,10 +175,10 @@ namespace osu.Game.Overlays.BeatmapSet
private void updateDifficultyButtons()
{
difficulties.Children.ToList().ForEach(diff => diff.State = diff.Beatmap == Beatmap.Value ? DifficultySelectorState.Selected : DifficultySelectorState.NotSelected);
Difficulties.Children.ToList().ForEach(diff => diff.State = diff.Beatmap == Beatmap.Value ? DifficultySelectorState.Selected : DifficultySelectorState.NotSelected);
}
private class DifficultiesContainer : FillFlowContainer<DifficultySelectorButton>
public class DifficultiesContainer : FillFlowContainer<DifficultySelectorButton>
{
public Action OnLostHover;
@@ -183,7 +189,7 @@ namespace osu.Game.Overlays.BeatmapSet
}
}
private class DifficultySelectorButton : OsuClickableContainer, IStateful<DifficultySelectorState>
public class DifficultySelectorButton : OsuClickableContainer, IStateful<DifficultySelectorState>
{
private const float transition_duration = 100;
private const float size = 52;
@@ -320,7 +326,7 @@ namespace osu.Game.Overlays.BeatmapSet
}
}
private enum DifficultySelectorState
public enum DifficultySelectorState
{
Selected,
NotSelected,
@@ -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),
};
}
}
@@ -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
}
}
+14 -2
View File
@@ -3,6 +3,7 @@
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
@@ -16,6 +17,7 @@ using osu.Game.Graphics.UserInterface;
using osu.Game.Online;
using osu.Game.Overlays.BeatmapSet.Buttons;
using osu.Game.Overlays.Direct;
using osu.Game.Rulesets;
using osuTK;
using osuTK.Graphics;
@@ -39,6 +41,7 @@ namespace osu.Game.Overlays.BeatmapSet
public bool DownloadButtonsVisible => downloadButtonsContainer.Any();
public readonly BeatmapRulesetSelector RulesetSelector;
public readonly BeatmapPicker Picker;
private readonly FavouriteButton favouriteButton;
@@ -47,6 +50,9 @@ namespace osu.Game.Overlays.BeatmapSet
private readonly LoadingAnimation loading;
[Cached(typeof(IBindable<RulesetInfo>))]
private readonly Bindable<RulesetInfo> ruleset = new Bindable<RulesetInfo>();
public Header()
{
ExternalLinkButton externalLink;
@@ -69,12 +75,18 @@ namespace osu.Game.Overlays.BeatmapSet
{
RelativeSizeAxes = Axes.X,
Height = tabs_height,
Children = new[]
Children = new Drawable[]
{
tabsBg = new Box
{
RelativeSizeAxes = Axes.Both,
},
RulesetSelector = new BeatmapRulesetSelector
{
Current = ruleset,
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
}
},
},
new Container
@@ -223,7 +235,7 @@ namespace osu.Game.Overlays.BeatmapSet
BeatmapSet.BindValueChanged(setInfo =>
{
Picker.BeatmapSet = author.BeatmapSet = beatmapAvailability.BeatmapSet = Details.BeatmapSet = setInfo.NewValue;
Picker.BeatmapSet = RulesetSelector.BeatmapSet = author.BeatmapSet = beatmapAvailability.BeatmapSet = Details.BeatmapSet = setInfo.NewValue;
cover.BeatmapSet = setInfo.NewValue;
if (setInfo.NewValue == null)
@@ -110,7 +110,7 @@ namespace osu.Game.Overlays.Changelog
t.Font = fontLarge;
t.Colour = entryColour;
});
title.AddLink($"{entry.Repository.Replace("ppy/", "")}#{entry.GithubPullRequestId}", entry.GithubUrl, Online.Chat.LinkAction.External,
title.AddLink($"{entry.Repository.Replace("ppy/", "")}#{entry.GithubPullRequestId}", entry.GithubUrl,
creationParameters: t =>
{
t.Font = fontLarge;
@@ -140,7 +140,7 @@ namespace osu.Game.Overlays.Changelog
t.Colour = entryColour;
});
else if (entry.GithubUser.GithubUrl != null)
title.AddLink(entry.GithubUser.DisplayName, entry.GithubUser.GithubUrl, Online.Chat.LinkAction.External, null, null, t =>
title.AddLink(entry.GithubUser.DisplayName, entry.GithubUser.GithubUrl, t =>
{
t.Font = fontMedium;
t.Colour = entryColour;
@@ -44,7 +44,17 @@ namespace osu.Game.Overlays.Changelog
req.Failure += _ => complete = true;
// This is done on a separate thread to support cancellation below
Task.Run(() => req.Perform(api));
Task.Run(() =>
{
try
{
req.Perform(api);
}
catch
{
complete = true;
}
});
while (!complete)
{
+17 -2
View File
@@ -170,6 +170,7 @@ namespace osu.Game.Overlays
var tcs = new TaskCompletionSource<bool>();
var req = new GetChangelogRequest();
req.Success += res => Schedule(() =>
{
// remap streams to builds to ensure model equality
@@ -183,8 +184,22 @@ namespace osu.Game.Overlays
tcs.SetResult(true);
});
req.Failure += _ => initialFetchTask = null;
req.Perform(API);
req.Failure += _ =>
{
initialFetchTask = null;
tcs.SetResult(false);
};
try
{
req.Perform(API);
}
catch
{
initialFetchTask = null;
tcs.SetResult(false);
}
await tcs.Task;
});
+115 -23
View File
@@ -1,4 +1,4 @@
// 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.
using System;
@@ -12,15 +12,22 @@ using osu.Framework.Graphics.Containers;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Cursor;
using osu.Game.Online.Chat;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics.Sprites;
namespace osu.Game.Overlays.Chat
{
public class DrawableChannel : Container
{
public readonly Channel Channel;
protected ChatLineContainer ChatLineFlow;
protected FillFlowContainer ChatLineFlow;
private OsuScrollContainer scroll;
[Resolved]
private OsuColour colours { get; set; }
public DrawableChannel(Channel channel)
{
Channel = channel;
@@ -40,7 +47,7 @@ namespace osu.Game.Overlays.Chat
// Some chat lines have effects that slightly protrude to the bottom,
// which we do not want to mask away, hence the padding.
Padding = new MarginPadding { Bottom = 5 },
Child = ChatLineFlow = new ChatLineContainer
Child = ChatLineFlow = new FillFlowContainer
{
Padding = new MarginPadding { Left = 20, Right = 20 },
RelativeSizeAxes = Axes.X,
@@ -74,31 +81,61 @@ namespace osu.Game.Overlays.Chat
protected virtual ChatLine CreateChatLine(Message m) => new ChatLine(m);
protected virtual DaySeparator CreateDaySeparator(DateTimeOffset time) => new DaySeparator(time)
{
Margin = new MarginPadding { Vertical = 10 },
Colour = colours.ChatBlue.Lighten(0.7f),
};
private void newMessagesArrived(IEnumerable<Message> newMessages)
{
bool shouldScrollToEnd = scroll.IsScrolledToEnd(10) || !chatLines.Any() || newMessages.Any(m => m is LocalMessage);
// Add up to last Channel.MAX_HISTORY messages
var displayMessages = newMessages.Skip(Math.Max(0, newMessages.Count() - Channel.MaxHistory));
var displayMessages = newMessages.Skip(Math.Max(0, newMessages.Count() - Channel.MAX_HISTORY));
ChatLineFlow.AddRange(displayMessages.Select(CreateChatLine));
Message lastMessage = chatLines.LastOrDefault()?.Message;
if (scroll.IsScrolledToEnd(10) || !ChatLineFlow.Children.Any() || newMessages.Any(m => m is LocalMessage))
scrollToEnd();
var staleMessages = ChatLineFlow.Children.Where(c => c.LifetimeEnd == double.MaxValue).ToArray();
int count = staleMessages.Length - Channel.MaxHistory;
for (int i = 0; i < count; i++)
foreach (var message in displayMessages)
{
var d = staleMessages[i];
if (!scroll.IsScrolledToEnd(10))
scroll.OffsetScrollPosition(-d.DrawHeight);
d.Expire();
if (lastMessage == null || lastMessage.Timestamp.ToLocalTime().Date != message.Timestamp.ToLocalTime().Date)
ChatLineFlow.Add(CreateDaySeparator(message.Timestamp));
ChatLineFlow.Add(CreateChatLine(message));
lastMessage = message;
}
var staleMessages = chatLines.Where(c => c.LifetimeEnd == double.MaxValue).ToArray();
int count = staleMessages.Length - Channel.MAX_HISTORY;
if (count > 0)
{
void expireAndAdjustScroll(Drawable d)
{
scroll.OffsetScrollPosition(-d.DrawHeight);
d.Expire();
}
for (int i = 0; i < count; i++)
expireAndAdjustScroll(staleMessages[i]);
// remove all adjacent day separators after stale message removal
for (int i = 0; i < ChatLineFlow.Count - 1; i++)
{
if (!(ChatLineFlow[i] is DaySeparator)) break;
if (!(ChatLineFlow[i + 1] is DaySeparator)) break;
expireAndAdjustScroll(ChatLineFlow[i]);
}
}
if (shouldScrollToEnd)
scrollToEnd();
}
private void pendingMessageResolved(Message existing, Message updated)
{
var found = ChatLineFlow.Children.LastOrDefault(c => c.Message == existing);
var found = chatLines.LastOrDefault(c => c.Message == existing);
if (found != null)
{
@@ -112,19 +149,74 @@ namespace osu.Game.Overlays.Chat
private void messageRemoved(Message removed)
{
ChatLineFlow.Children.FirstOrDefault(c => c.Message == removed)?.FadeColour(Color4.Red, 400).FadeOut(600).Expire();
chatLines.FirstOrDefault(c => c.Message == removed)?.FadeColour(Color4.Red, 400).FadeOut(600).Expire();
}
private IEnumerable<ChatLine> chatLines => ChatLineFlow.Children.OfType<ChatLine>();
private void scrollToEnd() => ScheduleAfterChildren(() => scroll.ScrollToEnd());
protected class ChatLineContainer : FillFlowContainer<ChatLine>
public class DaySeparator : Container
{
protected override int Compare(Drawable x, Drawable y)
public float TextSize
{
var xC = (ChatLine)x;
var yC = (ChatLine)y;
get => text.Font.Size;
set => text.Font = text.Font.With(size: value);
}
return xC.Message.CompareTo(yC.Message);
private float lineHeight = 2;
public float LineHeight
{
get => lineHeight;
set => lineHeight = leftBox.Height = rightBox.Height = value;
}
private readonly SpriteText text;
private readonly Box leftBox;
private readonly Box rightBox;
public DaySeparator(DateTimeOffset time)
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
Child = new GridContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
ColumnDimensions = new[]
{
new Dimension(),
new Dimension(GridSizeMode.AutoSize),
new Dimension(),
},
RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize), },
Content = new[]
{
new Drawable[]
{
leftBox = new Box
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.X,
Height = lineHeight,
},
text = new SpriteText
{
Margin = new MarginPadding { Horizontal = 10 },
Text = time.ToLocalTime().ToString("dd MMM yyyy"),
},
rightBox = new Box
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.X,
Height = lineHeight,
},
}
}
};
}
}
}
+9 -29
View File
@@ -55,7 +55,7 @@ namespace osu.Game.Overlays.Comments
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding(margin),
Padding = new MarginPadding(margin) { Left = margin + 5 },
Child = content = new GridContainer
{
RelativeSizeAxes = Axes.X,
@@ -81,11 +81,17 @@ namespace osu.Game.Overlays.Comments
Spacing = new Vector2(5, 0),
Children = new Drawable[]
{
votePill = new VotePill(comment.VotesCount)
new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AlwaysPresent = true,
Width = 40,
AutoSizeAxes = Axes.Y,
Child = votePill = new VotePill(comment)
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
}
},
new UpdateableAvatar(comment.User)
{
@@ -333,31 +339,5 @@ namespace osu.Game.Overlays.Comments
return parentComment.HasMessage ? parentComment.GetMessage : parentComment.IsDeleted ? @"deleted" : string.Empty;
}
}
private class VotePill : CircularContainer
{
public VotePill(int count)
{
AutoSizeAxes = Axes.X;
Height = 20;
Masking = true;
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = OsuColour.Gray(0.05f)
},
new SpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Margin = new MarginPadding { Horizontal = margin },
Font = OsuFont.GetFont(size: 14),
Text = $"+{count}"
}
};
}
}
}
}
+183
View File
@@ -0,0 +1,183 @@
// 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.Framework.Graphics;
using osu.Game.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Game.Online.API.Requests.Responses;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Allocation;
using osu.Game.Graphics.Sprites;
using osuTK.Graphics;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Input.Events;
using osu.Game.Graphics.UserInterface;
using System.Collections.Generic;
using osuTK;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Framework.Bindables;
using System.Linq;
namespace osu.Game.Overlays.Comments
{
public class VotePill : LoadingButton, IHasAccentColour
{
private const int duration = 200;
public Color4 AccentColour { get; set; }
protected override IEnumerable<Drawable> EffectTargets => null;
[Resolved]
private IAPIProvider api { get; set; }
private readonly Comment comment;
private Box background;
private Box hoverLayer;
private CircularContainer borderContainer;
private SpriteText sideNumber;
private OsuSpriteText votesCounter;
private CommentVoteRequest request;
private readonly BindableBool isVoted = new BindableBool();
private readonly BindableInt votesCount = new BindableInt();
public VotePill(Comment comment)
{
this.comment = comment;
Action = onAction;
AutoSizeAxes = Axes.X;
Height = 20;
LoadingAnimationSize = new Vector2(10);
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
AccentColour = borderContainer.BorderColour = sideNumber.Colour = colours.GreenLight;
hoverLayer.Colour = Color4.Black.Opacity(0.5f);
}
protected override void LoadComplete()
{
base.LoadComplete();
isVoted.Value = comment.IsVoted;
votesCount.Value = comment.VotesCount;
isVoted.BindValueChanged(voted => background.Colour = voted.NewValue ? AccentColour : OsuColour.Gray(0.05f), true);
votesCount.BindValueChanged(count => votesCounter.Text = $"+{count.NewValue}", true);
}
private void onAction()
{
request = new CommentVoteRequest(comment.Id, isVoted.Value ? CommentVoteAction.UnVote : CommentVoteAction.Vote);
request.Success += onSuccess;
api.Queue(request);
}
private void onSuccess(CommentBundle response)
{
var receivedComment = response.Comments.Single();
isVoted.Value = receivedComment.IsVoted;
votesCount.Value = receivedComment.VotesCount;
IsLoading = false;
}
protected override Drawable CreateContent() => new Container
{
RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X,
Children = new Drawable[]
{
borderContainer = new CircularContainer
{
RelativeSizeAxes = Axes.Both,
Masking = true,
Children = new Drawable[]
{
background = new Box
{
RelativeSizeAxes = Axes.Both
},
hoverLayer = new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0
}
}
},
sideNumber = new SpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreRight,
Text = "+1",
Font = OsuFont.GetFont(size: 14),
Margin = new MarginPadding { Right = 3 },
Alpha = 0,
},
votesCounter = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Margin = new MarginPadding { Horizontal = 10 },
Font = OsuFont.GetFont(size: 14),
AlwaysPresent = true,
}
},
};
protected override void OnLoadStarted()
{
votesCounter.FadeOut(duration, Easing.OutQuint);
updateDisplay();
}
protected override void OnLoadFinished()
{
votesCounter.FadeIn(duration, Easing.OutQuint);
if (IsHovered)
onHoverAction();
}
protected override bool OnHover(HoverEvent e)
{
onHoverAction();
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
updateDisplay();
base.OnHoverLost(e);
}
private void updateDisplay()
{
if (isVoted.Value)
{
hoverLayer.FadeTo(IsHovered ? 1 : 0);
sideNumber.Hide();
}
else
sideNumber.FadeTo(IsHovered ? 1 : 0);
borderContainer.BorderThickness = IsHovered ? 3 : 0;
}
private void onHoverAction()
{
if (!IsLoading)
updateDisplay();
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
request?.Cancel();
}
}
}
@@ -200,6 +200,7 @@ namespace osu.Game.Overlays.Settings.Sections.General
{
private TextBox username;
private TextBox password;
private ShakeContainer shakeSignIn;
private IAPIProvider api;
public Action RequestHide;
@@ -208,6 +209,8 @@ namespace osu.Game.Overlays.Settings.Sections.General
{
if (!string.IsNullOrEmpty(username.Text) && !string.IsNullOrEmpty(password.Text))
api.Login(username.Text, password.Text);
else
shakeSignIn.Shake();
}
[BackgroundDependencyLoader(permitNulls: true)]
@@ -244,10 +247,23 @@ namespace osu.Game.Overlays.Settings.Sections.General
LabelText = "Stay signed in",
Bindable = config.GetBindable<bool>(OsuSetting.SavePassword),
},
new SettingsButton
new Container
{
Text = "Sign in",
Action = performLogin
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Children = new Drawable[]
{
shakeSignIn = new ShakeContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Child = new SettingsButton
{
Text = "Sign in",
Action = performLogin
},
}
}
},
new SettingsButton
{
+2 -2
View File
@@ -8,12 +8,12 @@ using osu.Game.Graphics.UserInterface;
namespace osu.Game.Overlays.Settings
{
public class SettingsSlider<T> : SettingsSlider<T, OsuSliderBar<T>>
where T : struct, IEquatable<T>, IComparable, IConvertible
where T : struct, IEquatable<T>, IComparable<T>, IConvertible
{
}
public class SettingsSlider<T, U> : SettingsItem<T>
where T : struct, IEquatable<T>, IComparable, IConvertible
where T : struct, IEquatable<T>, IComparable<T>, IConvertible
where U : OsuSliderBar<T>, new()
{
protected override Drawable CreateControl() => new U
+2 -2
View File
@@ -37,7 +37,7 @@ namespace osu.Game.Overlays
protected SettingsSectionsContainer SectionsContainer;
private SearchTextBox searchTextBox;
private SeekLimitedSearchTextBox searchTextBox;
/// <summary>
/// Provide a source for the toolbar height.
@@ -80,7 +80,7 @@ namespace osu.Game.Overlays
Masking = true,
RelativeSizeAxes = Axes.Both,
ExpandableHeader = CreateHeader(),
FixedHeader = searchTextBox = new SearchTextBox
FixedHeader = searchTextBox = new SeekLimitedSearchTextBox
{
RelativeSizeAxes = Axes.X,
Origin = Anchor.TopCentre,
+72 -13
View File
@@ -14,6 +14,7 @@ using osu.Framework.Logging;
using osu.Framework.Threading;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Configuration;
using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Mods;
@@ -39,6 +40,9 @@ namespace osu.Game.Rulesets.Edit
[Resolved]
protected IFrameBasedClock EditorClock { get; private set; }
[Resolved]
private BindableBeatDivisor beatDivisor { get; set; }
private IWorkingBeatmap workingBeatmap;
private Beatmap<TObject> playableBeatmap;
private IBeatmapProcessor beatmapProcessor;
@@ -144,7 +148,7 @@ namespace osu.Game.Rulesets.Edit
EditorBeatmap = new EditorBeatmap<TObject>(playableBeatmap);
EditorBeatmap.HitObjectAdded += addHitObject;
EditorBeatmap.HitObjectRemoved += removeHitObject;
EditorBeatmap.StartTimeChanged += updateHitObject;
EditorBeatmap.StartTimeChanged += UpdateHitObject;
var dependencies = new DependencyContainer(parent);
dependencies.CacheAs<IEditorBeatmap>(EditorBeatmap);
@@ -162,11 +166,13 @@ namespace osu.Game.Rulesets.Edit
inputManager = GetContainingInputManager();
}
private double lastGridUpdateTime;
protected override void Update()
{
base.Update();
if (EditorClock.ElapsedFrameTime != 0 && blueprintContainer.CurrentTool != null)
if (EditorClock.CurrentTime != lastGridUpdateTime && blueprintContainer.CurrentTool != null)
showGridFor(Enumerable.Empty<HitObject>());
}
@@ -213,15 +219,13 @@ namespace osu.Game.Rulesets.Edit
distanceSnapGridContainer.Child = distanceSnapGrid;
distanceSnapGridContainer.Show();
}
lastGridUpdateTime = EditorClock.CurrentTime;
}
private ScheduledDelegate scheduledUpdate;
private void addHitObject(HitObject hitObject) => updateHitObject(hitObject);
private void removeHitObject(HitObject hitObject) => updateHitObject(null);
private void updateHitObject([CanBeNull] HitObject hitObject)
public override void UpdateHitObject(HitObject hitObject)
{
scheduledUpdate?.Cancel();
scheduledUpdate = Schedule(() =>
@@ -232,6 +236,10 @@ namespace osu.Game.Rulesets.Edit
});
}
private void addHitObject(HitObject hitObject) => UpdateHitObject(hitObject);
private void removeHitObject(HitObject hitObject) => UpdateHitObject(null);
public override IEnumerable<DrawableHitObject> HitObjects => drawableRulesetWrapper.Playfield.AllHitObjects;
public override bool CursorInPlacementArea => drawableRulesetWrapper.Playfield.ReceivePositionalInputAt(inputManager.CurrentState.Mouse.Position);
@@ -242,7 +250,7 @@ namespace osu.Game.Rulesets.Edit
public void BeginPlacement(HitObject hitObject)
{
if (distanceSnapGrid != null)
hitObject.StartTime = GetSnappedTime(hitObject.StartTime, distanceSnapGrid.ToLocalSpace(inputManager.CurrentState.Mouse.Position));
hitObject.StartTime = GetSnappedPosition(distanceSnapGrid.ToLocalSpace(inputManager.CurrentState.Mouse.Position), hitObject.StartTime).time;
}
public void EndPlacement(HitObject hitObject)
@@ -253,9 +261,45 @@ namespace osu.Game.Rulesets.Edit
public void Delete(HitObject hitObject) => EditorBeatmap.Remove(hitObject);
public override Vector2 GetSnappedPosition(Vector2 position) => distanceSnapGrid?.GetSnapPosition(position) ?? position;
public override (Vector2 position, double time) GetSnappedPosition(Vector2 position, double time) => distanceSnapGrid?.GetSnappedPosition(position) ?? (position, time);
public override double GetSnappedTime(double startTime, Vector2 position) => distanceSnapGrid?.GetSnapTime(position) ?? startTime;
public override float GetBeatSnapDistanceAt(double referenceTime)
{
DifficultyControlPoint difficultyPoint = EditorBeatmap.ControlPointInfo.DifficultyPointAt(referenceTime);
return (float)(100 * EditorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier / beatDivisor.Value);
}
public override float DurationToDistance(double referenceTime, double duration)
{
double beatLength = EditorBeatmap.ControlPointInfo.TimingPointAt(referenceTime).BeatLength / beatDivisor.Value;
return (float)(duration / beatLength * GetBeatSnapDistanceAt(referenceTime));
}
public override double DistanceToDuration(double referenceTime, float distance)
{
double beatLength = EditorBeatmap.ControlPointInfo.TimingPointAt(referenceTime).BeatLength / beatDivisor.Value;
return distance / GetBeatSnapDistanceAt(referenceTime) * beatLength;
}
public override double GetSnappedDurationFromDistance(double referenceTime, float distance)
=> beatSnap(referenceTime, DistanceToDuration(referenceTime, distance));
public override float GetSnappedDistanceFromDistance(double referenceTime, float distance)
=> DurationToDistance(referenceTime, beatSnap(referenceTime, DistanceToDuration(referenceTime, distance)));
/// <summary>
/// Snaps a duration to the closest beat of a timing point applicable at the reference time.
/// </summary>
/// <param name="referenceTime">The time of the timing point which <paramref name="duration"/> resides in.</param>
/// <param name="duration">The duration to snap.</param>
/// <returns>A value that represents <paramref name="duration"/> snapped to the closest beat of the timing point.</returns>
private double beatSnap(double referenceTime, double duration)
{
double beatLength = EditorBeatmap.ControlPointInfo.TimingPointAt(referenceTime).BeatLength / beatDivisor.Value;
// A 1ms offset prevents rounding errors due to minute variations in duration
return (int)((duration + 1) / beatLength) * beatLength;
}
protected override void Dispose(bool isDisposing)
{
@@ -270,7 +314,8 @@ namespace osu.Game.Rulesets.Edit
}
[Cached(typeof(HitObjectComposer))]
public abstract class HitObjectComposer : CompositeDrawable
[Cached(typeof(IDistanceSnapProvider))]
public abstract class HitObjectComposer : CompositeDrawable, IDistanceSnapProvider
{
internal HitObjectComposer()
{
@@ -306,8 +351,22 @@ namespace osu.Game.Rulesets.Edit
[CanBeNull]
protected virtual DistanceSnapGrid CreateDistanceSnapGrid([NotNull] IEnumerable<HitObject> selectedHitObjects) => null;
public abstract Vector2 GetSnappedPosition(Vector2 position);
/// <summary>
/// Updates a <see cref="HitObject"/>, invoking <see cref="HitObject.ApplyDefaults"/> and re-processing the beatmap.
/// </summary>
/// <param name="hitObject">The <see cref="HitObject"/> to update.</param>
public abstract void UpdateHitObject([CanBeNull] HitObject hitObject);
public abstract double GetSnappedTime(double startTime, Vector2 screenSpacePosition);
public abstract (Vector2 position, double time) GetSnappedPosition(Vector2 position, double time);
public abstract float GetBeatSnapDistanceAt(double referenceTime);
public abstract float DurationToDistance(double referenceTime, double duration);
public abstract double DistanceToDuration(double referenceTime, float distance);
public abstract double GetSnappedDurationFromDistance(double referenceTime, float distance);
public abstract float GetSnappedDistanceFromDistance(double referenceTime, float distance);
}
}

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