1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-14 15:33:05 +08:00

Merge branch 'master' into customized-mods

This commit is contained in:
Dean Herbert 2019-12-10 19:13:32 +09:00 committed by GitHub
commit 138b83c9ea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
86 changed files with 1107 additions and 550 deletions

View File

@ -54,6 +54,6 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2019.1010.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2019.1010.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2019.1205.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2019.1210.1" />
</ItemGroup> </ItemGroup>
</Project> </Project>

20
osu.Desktop/app.manifest Normal file
View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<asmv1:assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv1="urn:schemas-microsoft-com:asm.v1" xmlns:asmv2="urn:schemas-microsoft-com:asm.v2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
<assemblyIdentity version="1.0.0.0" name="osu!" />
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
<security>
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
</requestedPrivileges>
<applicationRequestMinimum>
<defaultAssemblyRequest permissionSetReference="Custom" />
<PermissionSet class="System.Security.PermissionSet" version="1" Unrestricted="true" ID="Custom" SameSite="site" />
</applicationRequestMinimum>
</security>
</trustInfo>
<asmv3:application>
<asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">
<dpiAware>true</dpiAware>
</asmv3:windowsSettings>
</asmv3:application>
</asmv1:assembly>

View File

@ -8,6 +8,7 @@
<Title>osu!lazer</Title> <Title>osu!lazer</Title>
<Product>osu!lazer</Product> <Product>osu!lazer</Product>
<ApplicationIcon>lazer.ico</ApplicationIcon> <ApplicationIcon>lazer.ico</ApplicationIcon>
<ApplicationManifest>app.manifest</ApplicationManifest>
<Version>0.0.0</Version> <Version>0.0.0</Version>
<FileVersion>0.0.0</FileVersion> <FileVersion>0.0.0</FileVersion>
</PropertyGroup> </PropertyGroup>
@ -23,11 +24,11 @@
<ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" /> <ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="System.IO.Packaging" Version="4.6.0" /> <PackageReference Include="System.IO.Packaging" Version="4.7.0" />
<PackageReference Include="ppy.squirrel.windows" Version="1.9.0.4" /> <PackageReference Include="ppy.squirrel.windows" Version="1.9.0.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="2.2.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="2.2.6" />
<PackageReference Include="Microsoft.Win32.Registry" Version="4.6.0" /> <PackageReference Include="Microsoft.Win32.Registry" Version="4.7.0" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Resources"> <ItemGroup Label="Resources">
<EmbeddedResource Include="lazer.ico" /> <EmbeddedResource Include="lazer.ico" />

View File

@ -116,7 +116,23 @@ namespace osu.Game.Rulesets.Catch.Objects
public double Duration => EndTime - StartTime; public double Duration => EndTime - StartTime;
public SliderPath Path { get; set; } private readonly SliderPath path = new SliderPath();
public SliderPath Path
{
get => path;
set
{
path.ControlPoints.Clear();
path.ExpectedDistance.Value = null;
if (value != null)
{
path.ControlPoints.AddRange(value.ControlPoints);
path.ExpectedDistance.Value = value.ExpectedDistance.Value;
}
}
}
public double Distance => Path.Distance; public double Distance => Path.Distance;

View File

@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Catch.UI
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
InternalChild = new SkinnableSprite("Gameplay/catch/fruit-catcher-idle") InternalChild = new SkinnableSprite("Gameplay/catch/fruit-catcher-idle", confineMode: ConfineMode.ScaleDownToFit)
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Anchor = Anchor.TopCentre, Anchor = Anchor.TopCentre,

View File

@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
protected override string ResourceAssembly => "osu.Game.Rulesets.Osu"; protected override string ResourceAssembly => "osu.Game.Rulesets.Osu";
[TestCase(6.931145117263422, "diffcalc-test")] [TestCase(6.9311451172608853d, "diffcalc-test")]
[TestCase(1.0736587013228804d, "zero-length-sliders")] [TestCase(1.0736587013228804d, "zero-length-sliders")]
public void Test(double expected, string name) public void Test(double expected, string name)
=> base.Test(expected, name); => base.Test(expected, name);

View File

@ -196,7 +196,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
AddStep($"move mouse to control point {index}", () => AddStep($"move mouse to control point {index}", () =>
{ {
Vector2 position = slider.Position + slider.Path.ControlPoints[index]; Vector2 position = slider.Position + slider.Path.ControlPoints[index].Position.Value;
InputManager.MoveMouseTo(drawableObject.Parent.ToScreenSpace(position)); InputManager.MoveMouseTo(drawableObject.Parent.ToScreenSpace(position));
}); });
} }

View File

@ -21,7 +21,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
public class PathControlPointPiece : BlueprintPiece<Slider> public class PathControlPointPiece : BlueprintPiece<Slider>
{ {
public Action<int, MouseButtonEvent> RequestSelection; public Action<int, MouseButtonEvent> RequestSelection;
public Action<Vector2[]> ControlPointsChanged;
public readonly BindableBool IsSelected = new BindableBool(); public readonly BindableBool IsSelected = new BindableBool();
public readonly int Index; public readonly int Index;
@ -90,7 +89,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{ {
base.Update(); base.Update();
Position = slider.StackedPosition + slider.Path.ControlPoints[Index]; Position = slider.StackedPosition + slider.Path.ControlPoints[Index].Position.Value;
updateMarkerDisplay(); updateMarkerDisplay();
updateConnectingPath(); updateConnectingPath();
@ -103,7 +102,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{ {
markerRing.Alpha = IsSelected.Value ? 1 : 0; markerRing.Alpha = IsSelected.Value ? 1 : 0;
Color4 colour = isSegmentSeparator ? colours.Red : colours.Yellow; Color4 colour = slider.Path.ControlPoints[Index].Type.Value.HasValue ? colours.Red : colours.Yellow;
if (IsHovered || IsSelected.Value) if (IsHovered || IsSelected.Value)
colour = Color4.White; colour = Color4.White;
marker.Colour = colour; marker.Colour = colour;
@ -116,10 +115,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{ {
path.ClearVertices(); path.ClearVertices();
if (Index != slider.Path.ControlPoints.Length - 1) if (Index != slider.Path.ControlPoints.Count - 1)
{ {
path.AddVertex(Vector2.Zero); path.AddVertex(Vector2.Zero);
path.AddVertex(slider.Path.ControlPoints[Index + 1] - slider.Path.ControlPoints[Index]); path.AddVertex(slider.Path.ControlPoints[Index + 1].Position.Value - slider.Path.ControlPoints[Index].Position.Value);
} }
path.OriginPosition = path.PositionInBoundingBox(Vector2.Zero); path.OriginPosition = path.PositionInBoundingBox(Vector2.Zero);
@ -156,8 +155,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
protected override bool OnDrag(DragEvent e) protected override bool OnDrag(DragEvent e)
{ {
var newControlPoints = slider.Path.ControlPoints.ToArray();
if (Index == 0) if (Index == 0)
{ {
// Special handling for the head control point - the position of the slider changes which means the snapped position and time have to be taken into account // Special handling for the head control point - the position of the slider changes which means the snapped position and time have to be taken into account
@ -168,29 +165,15 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
slider.StartTime = snappedTime; slider.StartTime = snappedTime;
// Since control points are relative to the position of the slider, they all need to be offset backwards by the delta // Since control points are relative to the position of the slider, they all need to be offset backwards by the delta
for (int i = 1; i < newControlPoints.Length; i++) for (int i = 1; i < slider.Path.ControlPoints.Count; i++)
newControlPoints[i] -= movementDelta; slider.Path.ControlPoints[i].Position.Value -= movementDelta;
} }
else else
newControlPoints[Index] += e.Delta; slider.Path.ControlPoints[Index].Position.Value += e.Delta;
if (isSegmentSeparatorWithNext)
newControlPoints[Index + 1] = newControlPoints[Index];
if (isSegmentSeparatorWithPrevious)
newControlPoints[Index - 1] = newControlPoints[Index];
ControlPointsChanged?.Invoke(newControlPoints);
return true; return true;
} }
protected override bool OnDragEnd(DragEndEvent e) => true; protected override bool OnDragEnd(DragEndEvent e) => true;
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 isSegmentSeparatorWithPrevious => Index > 0 && slider.Path.ControlPoints[Index - 1] == slider.Path.ControlPoints[Index];
} }
} }

View File

@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Humanizer; using Humanizer;
@ -14,6 +13,7 @@ using osu.Framework.Input;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit.Compose; using osu.Game.Screens.Edit.Compose;
using osuTK; using osuTK;
@ -23,8 +23,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{ {
public class PathControlPointVisualiser : CompositeDrawable, IKeyBindingHandler<PlatformAction>, IHasContextMenu public class PathControlPointVisualiser : CompositeDrawable, IKeyBindingHandler<PlatformAction>, IHasContextMenu
{ {
public Action<Vector2[]> ControlPointsChanged;
internal readonly Container<PathControlPointPiece> Pieces; internal readonly Container<PathControlPointPiece> Pieces;
private readonly Slider slider; private readonly Slider slider;
private readonly bool allowSelection; private readonly bool allowSelection;
@ -55,12 +53,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{ {
base.Update(); base.Update();
while (slider.Path.ControlPoints.Length > Pieces.Count) while (slider.Path.ControlPoints.Count > Pieces.Count)
{ {
var piece = new PathControlPointPiece(slider, Pieces.Count) var piece = new PathControlPointPiece(slider, Pieces.Count);
{
ControlPointsChanged = c => ControlPointsChanged?.Invoke(c),
};
if (allowSelection) if (allowSelection)
piece.RequestSelection = selectPiece; piece.RequestSelection = selectPiece;
@ -68,7 +63,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
Pieces.Add(piece); Pieces.Add(piece);
} }
while (slider.Path.ControlPoints.Length < Pieces.Count) while (slider.Path.ControlPoints.Count < Pieces.Count)
Pieces.Remove(Pieces[Pieces.Count - 1]); Pieces.Remove(Pieces[Pieces.Count - 1]);
} }
@ -105,38 +100,40 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
private bool deleteSelected() private bool deleteSelected()
{ {
var newControlPoints = new List<Vector2>(); List<PathControlPoint> toRemove = Pieces.Where(p => p.IsSelected.Value).Select(p => slider.Path.ControlPoints[p.Index]).ToList();
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 // Ensure that there are any points to be deleted
if (newControlPoints.Count == slider.Path.ControlPoints.Length) if (toRemove.Count == 0)
return false; return false;
// If there are 0 remaining control points, treat the slider as being deleted foreach (var c in toRemove)
if (newControlPoints.Count == 0) {
// The first control point in the slider must have a type, so take it from the previous "first" one
// Todo: Should be handled within SliderPath itself
if (c == slider.Path.ControlPoints[0] && slider.Path.ControlPoints.Count > 1 && slider.Path.ControlPoints[1].Type.Value == null)
slider.Path.ControlPoints[1].Type.Value = slider.Path.ControlPoints[0].Type.Value;
slider.Path.ControlPoints.Remove(c);
}
// If there are 0 or 1 remaining control points, the slider is in a degenerate (single point) form and should be deleted
if (slider.Path.ControlPoints.Count <= 1)
{ {
placementHandler?.Delete(slider); placementHandler?.Delete(slider);
return true; return true;
} }
// Make control points relative // The path will have a non-zero offset if the head is removed, but sliders don't support this behaviour since the head is positioned at the slider's position
Vector2 first = newControlPoints[0]; // So the slider needs to be offset by this amount instead, and all control points offset backwards such that the path is re-positioned at (0, 0)
for (int i = 0; i < newControlPoints.Count; i++) Vector2 first = slider.Path.ControlPoints[0].Position.Value;
newControlPoints[i] = newControlPoints[i] - first; foreach (var c in slider.Path.ControlPoints)
c.Position.Value -= 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 += first; slider.Position += first;
// Since pieces are re-used, they will not point to the deleted control points while remaining selected // Since pieces are re-used, they will not point to the deleted control points while remaining selected
foreach (var piece in Pieces) foreach (var piece in Pieces)
piece.IsSelected.Value = false; piece.IsSelected.Value = false;
ControlPointsChanged?.Invoke(newControlPoints.ToArray());
return true; return true;
} }
@ -154,7 +151,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
return new MenuItem[] return new MenuItem[]
{ {
new OsuMenuItem($"Delete {"control point".ToQuantity(selectedPoints)}", MenuItemType.Destructive, () => deleteSelected()) new OsuMenuItem($"Delete {"control point".ToQuantity(selectedPoints, selectedPoints > 1 ? ShowQuantityAs.Numeric : ShowQuantityAs.None)}", MenuItemType.Destructive, () => deleteSelected())
}; };
} }
} }

View File

@ -1,10 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Input; using osu.Framework.Input;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
@ -27,11 +24,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private HitCirclePiece headCirclePiece; private HitCirclePiece headCirclePiece;
private HitCirclePiece tailCirclePiece; private HitCirclePiece tailCirclePiece;
private readonly List<Segment> segments = new List<Segment>();
private Vector2 cursor;
private InputManager inputManager; private InputManager inputManager;
private PlacementState state; private PlacementState state;
private PathControlPoint segmentStart;
private PathControlPoint cursor;
private int currentSegmentLength;
[Resolved(CanBeNull = true)] [Resolved(CanBeNull = true)]
private HitObjectComposer composer { get; set; } private HitObjectComposer composer { get; set; }
@ -40,7 +38,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
: base(new Objects.Slider()) : base(new Objects.Slider())
{ {
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
segments.Add(new Segment(Vector2.Zero));
HitObject.Path.ControlPoints.Add(segmentStart = new PathControlPoint(Vector2.Zero, PathType.Linear));
currentSegmentLength = 1;
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
@ -51,7 +51,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
bodyPiece = new SliderBodyPiece(), bodyPiece = new SliderBodyPiece(),
headCirclePiece = new HitCirclePiece(), headCirclePiece = new HitCirclePiece(),
tailCirclePiece = new HitCirclePiece(), tailCirclePiece = new HitCirclePiece(),
new PathControlPointVisualiser(HitObject, false) { ControlPointsChanged = _ => updateSlider() }, new PathControlPointVisualiser(HitObject, false)
}; };
setState(PlacementState.Initial); setState(PlacementState.Initial);
@ -72,9 +72,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
break; break;
case PlacementState.Body: case PlacementState.Body:
ensureCursor();
// The given screen-space position may have been externally snapped, but the unsnapped position from the input manager // 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 // is used instead since snapping control points doesn't make much sense
cursor = ToLocalSpace(inputManager.CurrentState.Mouse.Position) - HitObject.Position; cursor.Position.Value = ToLocalSpace(inputManager.CurrentState.Mouse.Position) - HitObject.Position;
break; break;
} }
} }
@ -91,7 +93,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
switch (e.Button) switch (e.Button)
{ {
case MouseButton.Left: case MouseButton.Left:
segments.Last().ControlPoints.Add(cursor); ensureCursor();
// Detatch the cursor
cursor = null;
break; break;
} }
@ -110,7 +115,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
protected override bool OnDoubleClick(DoubleClickEvent e) protected override bool OnDoubleClick(DoubleClickEvent e)
{ {
segments.Add(new Segment(segments[segments.Count - 1].ControlPoints.Last())); // Todo: This should all not occur on double click, but rather if the previous control point is hovered.
segmentStart = HitObject.Path.ControlPoints[HitObject.Path.ControlPoints.Count - 1];
segmentStart.Type.Value = PathType.Linear;
currentSegmentLength = 1;
return true; return true;
} }
@ -132,14 +141,39 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
updateSlider(); updateSlider();
} }
private void updatePathType()
{
switch (currentSegmentLength)
{
case 1:
case 2:
segmentStart.Type.Value = PathType.Linear;
break;
case 3:
segmentStart.Type.Value = PathType.PerfectCurve;
break;
default:
segmentStart.Type.Value = PathType.Bezier;
break;
}
}
private void ensureCursor()
{
if (cursor == null)
{
HitObject.Path.ControlPoints.Add(cursor = new PathControlPoint { Position = { Value = Vector2.Zero } });
currentSegmentLength++;
updatePathType();
}
}
private void updateSlider() private void updateSlider()
{ {
Vector2[] newControlPoints = segments.SelectMany(s => s.ControlPoints).Concat(cursor.Yield()).ToArray(); HitObject.Path.ExpectedDistance.Value = composer?.GetSnappedDistanceFromDistance(HitObject.StartTime, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance;
var unsnappedPath = new SliderPath(newControlPoints.Length > 2 ? PathType.Bezier : PathType.Linear, newControlPoints);
var snappedDistance = composer?.GetSnappedDistanceFromDistance(HitObject.StartTime, (float)unsnappedPath.Distance) ?? (float)unsnappedPath.Distance;
HitObject.Path = new SliderPath(unsnappedPath.Type, newControlPoints, snappedDistance);
bodyPiece.UpdateFrom(HitObject); bodyPiece.UpdateFrom(HitObject);
headCirclePiece.UpdateFrom(HitObject.HeadCircle); headCirclePiece.UpdateFrom(HitObject.HeadCircle);
@ -156,15 +190,5 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
Initial, Initial,
Body, Body,
} }
private class Segment
{
public readonly List<Vector2> ControlPoints = new List<Vector2>();
public Segment(Vector2 offset)
{
ControlPoints.Add(offset);
}
}
} }
} }

View File

@ -1,9 +1,9 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Diagnostics; using System.Diagnostics;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
@ -11,7 +11,6 @@ using osu.Framework.Input.Events;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components; using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables;
@ -40,10 +39,20 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
BodyPiece = new SliderBodyPiece(), BodyPiece = new SliderBodyPiece(),
HeadBlueprint = CreateCircleSelectionBlueprint(slider, SliderPosition.Start), HeadBlueprint = CreateCircleSelectionBlueprint(slider, SliderPosition.Start),
TailBlueprint = CreateCircleSelectionBlueprint(slider, SliderPosition.End), TailBlueprint = CreateCircleSelectionBlueprint(slider, SliderPosition.End),
ControlPointVisualiser = new PathControlPointVisualiser(sliderObject, true) { ControlPointsChanged = onNewControlPoints }, ControlPointVisualiser = new PathControlPointVisualiser(sliderObject, true)
}; };
} }
private IBindable<int> pathVersion;
protected override void LoadComplete()
{
base.LoadComplete();
pathVersion = HitObject.Path.Version.GetBoundCopy();
pathVersion.BindValueChanged(_ => updatePath());
}
protected override void Update() protected override void Update()
{ {
base.Update(); base.Update();
@ -77,12 +86,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
{ {
Debug.Assert(placementControlPointIndex != null); Debug.Assert(placementControlPointIndex != null);
Vector2 position = e.MousePosition - HitObject.Position; HitObject.Path.ControlPoints[placementControlPointIndex.Value].Position.Value = e.MousePosition - HitObject.Position;
var controlPoints = HitObject.Path.ControlPoints.ToArray();
controlPoints[placementControlPointIndex.Value] = position;
onNewControlPoints(controlPoints);
return true; return true;
} }
@ -97,15 +101,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
{ {
position -= HitObject.Position; position -= HitObject.Position;
var controlPoints = new Vector2[HitObject.Path.ControlPoints.Length + 1];
HitObject.Path.ControlPoints.CopyTo(controlPoints);
int insertionIndex = 0; int insertionIndex = 0;
float minDistance = float.MaxValue; float minDistance = float.MaxValue;
for (int i = 0; i < controlPoints.Length - 2; i++) for (int i = 0; i < HitObject.Path.ControlPoints.Count - 1; i++)
{ {
float dist = new Line(controlPoints[i], controlPoints[i + 1]).DistanceToPoint(position); float dist = new Line(HitObject.Path.ControlPoints[i].Position.Value, HitObject.Path.ControlPoints[i + 1].Position.Value).DistanceToPoint(position);
if (dist < minDistance) if (dist < minDistance)
{ {
@ -115,21 +116,14 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
} }
// Move the control points from the insertion index onwards to make room for the insertion // Move the control points from the insertion index onwards to make room for the insertion
Array.Copy(controlPoints, insertionIndex, controlPoints, insertionIndex + 1, controlPoints.Length - insertionIndex - 1); HitObject.Path.ControlPoints.Insert(insertionIndex, new PathControlPoint { Position = { Value = position } });
controlPoints[insertionIndex] = position;
onNewControlPoints(controlPoints);
return insertionIndex; return insertionIndex;
} }
private void onNewControlPoints(Vector2[] controlPoints) private void updatePath()
{ {
var unsnappedPath = new SliderPath(controlPoints.Length > 2 ? PathType.Bezier : PathType.Linear, controlPoints); HitObject.Path.ExpectedDistance.Value = composer?.GetSnappedDistanceFromDistance(HitObject.StartTime, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance;
var snappedDistance = composer?.GetSnappedDistanceFromDistance(HitObject.StartTime, (float)unsnappedPath.Distance) ?? (float)unsnappedPath.Distance;
HitObject.Path = new SliderPath(unsnappedPath.Type, controlPoints, snappedDistance);
UpdateHitObject(); UpdateHitObject();
} }

View File

@ -28,11 +28,8 @@ namespace osu.Game.Rulesets.Osu.Mods
slider.NestedHitObjects.OfType<SliderTick>().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y)); slider.NestedHitObjects.OfType<SliderTick>().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y));
slider.NestedHitObjects.OfType<RepeatPoint>().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y)); slider.NestedHitObjects.OfType<RepeatPoint>().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y));
var newControlPoints = new Vector2[slider.Path.ControlPoints.Length]; foreach (var point in slider.Path.ControlPoints)
for (int i = 0; i < slider.Path.ControlPoints.Length; i++) point.Position.Value = new Vector2(point.Position.Value.X, -point.Position.Value.Y);
newControlPoints[i] = new Vector2(slider.Path.ControlPoints[i].X, -slider.Path.ControlPoints[i].Y);
slider.Path = new SliderPath(slider.Path.Type, newControlPoints, slider.Path.ExpectedDistance);
} }
} }
} }

View File

@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private readonly IBindable<Vector2> positionBindable = new Bindable<Vector2>(); private readonly IBindable<Vector2> positionBindable = new Bindable<Vector2>();
private readonly IBindable<int> stackHeightBindable = new Bindable<int>(); private readonly IBindable<int> stackHeightBindable = new Bindable<int>();
private readonly IBindable<float> scaleBindable = new Bindable<float>(); private readonly IBindable<float> scaleBindable = new Bindable<float>();
private readonly IBindable<SliderPath> pathBindable = new Bindable<SliderPath>(); private readonly IBindable<int> pathVersion = new Bindable<int>();
[Resolved(CanBeNull = true)] [Resolved(CanBeNull = true)]
private OsuRulesetConfigManager config { get; set; } private OsuRulesetConfigManager config { get; set; }
@ -84,9 +84,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
positionBindable.BindTo(HitObject.PositionBindable); positionBindable.BindTo(HitObject.PositionBindable);
stackHeightBindable.BindTo(HitObject.StackHeightBindable); stackHeightBindable.BindTo(HitObject.StackHeightBindable);
scaleBindable.BindTo(HitObject.ScaleBindable); scaleBindable.BindTo(HitObject.ScaleBindable);
pathBindable.BindTo(slider.PathBindable); pathVersion.BindTo(slider.Path.Version);
pathBindable.BindValueChanged(_ => Body.Refresh()); pathVersion.BindValueChanged(_ => Body.Refresh());
AccentColour.BindValueChanged(colour => AccentColour.BindValueChanged(colour =>
{ {

View File

@ -4,7 +4,6 @@
using System; using System;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osuTK; using osuTK;
@ -13,7 +12,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public class DrawableSliderHead : DrawableHitCircle public class DrawableSliderHead : DrawableHitCircle
{ {
private readonly IBindable<Vector2> positionBindable = new Bindable<Vector2>(); private readonly IBindable<Vector2> positionBindable = new Bindable<Vector2>();
private readonly IBindable<SliderPath> pathBindable = new Bindable<SliderPath>(); private readonly IBindable<int> pathVersion = new Bindable<int>();
private readonly Slider slider; private readonly Slider slider;
@ -27,10 +26,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private void load() private void load()
{ {
positionBindable.BindTo(HitObject.PositionBindable); positionBindable.BindTo(HitObject.PositionBindable);
pathBindable.BindTo(slider.PathBindable); pathVersion.BindTo(slider.Path.Version);
positionBindable.BindValueChanged(_ => updatePosition()); positionBindable.BindValueChanged(_ => updatePosition());
pathBindable.BindValueChanged(_ => updatePosition(), true); pathVersion.BindValueChanged(_ => updatePosition(), true);
} }
protected override void Update() protected override void Update()

View File

@ -3,7 +3,6 @@
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osuTK; using osuTK;
@ -21,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public bool Tracking { get; set; } public bool Tracking { get; set; }
private readonly IBindable<Vector2> positionBindable = new Bindable<Vector2>(); private readonly IBindable<Vector2> positionBindable = new Bindable<Vector2>();
private readonly IBindable<SliderPath> pathBindable = new Bindable<SliderPath>(); private readonly IBindable<int> pathVersion = new Bindable<int>();
public DrawableSliderTail(Slider slider, SliderTailCircle hitCircle) public DrawableSliderTail(Slider slider, SliderTailCircle hitCircle)
: base(hitCircle) : base(hitCircle)
@ -36,10 +35,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
AlwaysPresent = true; AlwaysPresent = true;
positionBindable.BindTo(hitCircle.PositionBindable); positionBindable.BindTo(hitCircle.PositionBindable);
pathBindable.BindTo(slider.PathBindable); pathVersion.BindTo(slider.Path.Version);
positionBindable.BindValueChanged(_ => updatePosition()); positionBindable.BindValueChanged(_ => updatePosition());
pathBindable.BindValueChanged(_ => updatePosition(), true); pathVersion.BindValueChanged(_ => updatePosition(), true);
// TODO: This has no drawable content. Support for skins should be added. // TODO: This has no drawable content. Support for skins should be added.
} }

View File

@ -6,7 +6,6 @@ using osu.Game.Rulesets.Objects.Types;
using System.Collections.Generic; using System.Collections.Generic;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using System.Linq; using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Caching; using osu.Framework.Caching;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
@ -28,17 +27,21 @@ namespace osu.Game.Rulesets.Osu.Objects
public Vector2 StackedPositionAt(double t) => StackedPosition + this.CurvePositionAt(t); public Vector2 StackedPositionAt(double t) => StackedPosition + this.CurvePositionAt(t);
public readonly Bindable<SliderPath> PathBindable = new Bindable<SliderPath>(); private readonly SliderPath path = new SliderPath();
public SliderPath Path public SliderPath Path
{ {
get => PathBindable.Value; get => path;
set set
{ {
PathBindable.Value = value; path.ControlPoints.Clear();
endPositionCache.Invalidate(); path.ExpectedDistance.Value = null;
updateNestedPositions(); if (value != null)
{
path.ControlPoints.AddRange(value.ControlPoints);
path.ExpectedDistance.Value = value.ExpectedDistance.Value;
}
} }
} }
@ -50,8 +53,6 @@ namespace osu.Game.Rulesets.Osu.Objects
set set
{ {
base.Position = value; base.Position = value;
endPositionCache.Invalidate();
updateNestedPositions(); updateNestedPositions();
} }
} }
@ -112,6 +113,7 @@ namespace osu.Game.Rulesets.Osu.Objects
{ {
SamplesBindable.ItemsAdded += _ => updateNestedSamples(); SamplesBindable.ItemsAdded += _ => updateNestedSamples();
SamplesBindable.ItemsRemoved += _ => updateNestedSamples(); SamplesBindable.ItemsRemoved += _ => updateNestedSamples();
Path.Version.ValueChanged += _ => updateNestedPositions();
} }
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
@ -189,6 +191,8 @@ namespace osu.Game.Rulesets.Osu.Objects
private void updateNestedPositions() private void updateNestedPositions()
{ {
endPositionCache.Invalidate();
if (HeadCircle != null) if (HeadCircle != null)
HeadCircle.Position = Position; HeadCircle.Position = Position;

View File

@ -15,12 +15,12 @@ namespace osu.Game.Rulesets.Osu.Objects
/// </summary> /// </summary>
public class SliderTailCircle : SliderCircle public class SliderTailCircle : SliderCircle
{ {
private readonly IBindable<SliderPath> pathBindable = new Bindable<SliderPath>(); private readonly IBindable<int> pathVersion = new Bindable<int>();
public SliderTailCircle(Slider slider) public SliderTailCircle(Slider slider)
{ {
pathBindable.BindTo(slider.PathBindable); pathVersion.BindTo(slider.Path.Version);
pathBindable.BindValueChanged(_ => Position = slider.EndPosition); pathVersion.BindValueChanged(_ => Position = slider.EndPosition);
} }
public override Judgement CreateJudgement() => new OsuSliderTailJudgement(); public override Judgement CreateJudgement() => new OsuSliderTailJudgement();

View File

@ -11,6 +11,9 @@ namespace osu.Game.Rulesets.Osu.Skinning
{ {
public class LegacyCursor : CompositeDrawable public class LegacyCursor : CompositeDrawable
{ {
private NonPlayfieldSprite cursor;
private bool spin;
public LegacyCursor() public LegacyCursor()
{ {
Size = new Vector2(50); Size = new Vector2(50);
@ -22,6 +25,8 @@ namespace osu.Game.Rulesets.Osu.Skinning
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(ISkinSource skin) private void load(ISkinSource skin)
{ {
spin = skin.GetConfig<OsuSkinConfiguration, bool>(OsuSkinConfiguration.CursorRotate)?.Value ?? true;
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
new NonPlayfieldSprite new NonPlayfieldSprite
@ -30,7 +35,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
}, },
new NonPlayfieldSprite cursor = new NonPlayfieldSprite
{ {
Texture = skin.GetTexture("cursor"), Texture = skin.GetTexture("cursor"),
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
@ -38,5 +43,11 @@ namespace osu.Game.Rulesets.Osu.Skinning
} }
}; };
} }
protected override void LoadComplete()
{
if (spin)
cursor.Spin(10000, RotationDirection.Clockwise);
}
} }
} }

View File

@ -49,7 +49,11 @@ namespace osu.Game.Rulesets.Osu.Skinning
return this.GetAnimation(component.LookupName, true, false); return this.GetAnimation(component.LookupName, true, false);
case OsuSkinComponents.SliderFollowCircle: case OsuSkinComponents.SliderFollowCircle:
return this.GetAnimation("sliderfollowcircle", true, true); var followCircle = this.GetAnimation("sliderfollowcircle", true, true);
if (followCircle != null)
// follow circles are 2x the hitcircle resolution in legacy skins (since they are scaled down from >1x
followCircle.Scale *= 0.5f;
return followCircle;
case OsuSkinComponents.SliderBall: case OsuSkinComponents.SliderBall:
var sliderBallContent = this.GetAnimation("sliderb", true, true, ""); var sliderBallContent = this.GetAnimation("sliderb", true, true, "");

View File

@ -11,5 +11,6 @@ namespace osu.Game.Rulesets.Osu.Skinning
SliderPathRadius, SliderPathRadius,
AllowSliderBallTint, AllowSliderBallTint,
CursorExpand, CursorExpand,
CursorRotate
} }
} }

View File

@ -13,6 +13,8 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Timing; using osu.Framework.Timing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Objects;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Compose.Components.Timeline; using osu.Game.Screens.Edit.Compose.Components.Timeline;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
@ -25,6 +27,7 @@ namespace osu.Game.Tests.Visual.Editor
public override IReadOnlyList<Type> RequiredTypes => new[] public override IReadOnlyList<Type> RequiredTypes => new[]
{ {
typeof(TimelineArea), typeof(TimelineArea),
typeof(TimelineHitObjectDisplay),
typeof(Timeline), typeof(Timeline),
typeof(TimelineButton), typeof(TimelineButton),
typeof(CentreMarker) typeof(CentreMarker)
@ -35,6 +38,8 @@ namespace osu.Game.Tests.Visual.Editor
{ {
Beatmap.Value = new WaveformTestBeatmap(audio); Beatmap.Value = new WaveformTestBeatmap(audio);
var editorBeatmap = new EditorBeatmap<HitObject>((Beatmap<HitObject>)Beatmap.Value.Beatmap);
Children = new Drawable[] Children = new Drawable[]
{ {
new FillFlowContainer new FillFlowContainer
@ -50,6 +55,7 @@ namespace osu.Game.Tests.Visual.Editor
}, },
new TimelineArea new TimelineArea
{ {
Child = new TimelineHitObjectDisplay(editorBeatmap),
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio; using osu.Framework.Audio;
@ -19,6 +20,7 @@ using osu.Game.Overlays;
using osu.Game.Overlays.Notifications; using osu.Game.Overlays.Notifications;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Screens; using osu.Game.Screens;
@ -55,6 +57,9 @@ namespace osu.Game.Tests.Visual.Gameplay
beforeLoadAction?.Invoke(); beforeLoadAction?.Invoke();
Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
foreach (var mod in Mods.Value.OfType<IApplicableToTrack>())
mod.ApplyToTrack(Beatmap.Value.Track);
InputManager.Child = container = new TestPlayerLoaderContainer( InputManager.Child = container = new TestPlayerLoaderContainer(
loader = new TestPlayerLoader(() => loader = new TestPlayerLoader(() =>
{ {
@ -63,6 +68,24 @@ namespace osu.Game.Tests.Visual.Gameplay
})); }));
} }
/// <summary>
/// When <see cref="PlayerLoader"/> exits early, it has to wait for the player load task
/// to complete before running disposal on player. This previously caused an issue where mod
/// speed adjustments were undone too late, causing cross-screen pollution.
/// </summary>
[Test]
public void TestEarlyExit()
{
AddStep("load dummy beatmap", () => ResetPlayer(false, () => Mods.Value = new[] { new OsuModNightcore() }));
AddUntilStep("wait for current", () => loader.IsCurrentScreen());
AddAssert("mod rate applied", () => Beatmap.Value.Track.Rate != 1);
AddStep("exit loader", () => loader.Exit());
AddUntilStep("wait for not current", () => !loader.IsCurrentScreen());
AddAssert("player did not load", () => !player.IsLoaded);
AddUntilStep("player disposed", () => loader.DisposalTask?.IsCompleted == true);
AddAssert("mod rate still applied", () => Beatmap.Value.Track.Rate != 1);
}
[Test] [Test]
public void TestBlockLoadViaMouseMovement() public void TestBlockLoadViaMouseMovement()
{ {
@ -196,6 +219,8 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
public new VisualSettings VisualSettings => base.VisualSettings; public new VisualSettings VisualSettings => base.VisualSettings;
public new Task DisposalTask => base.DisposalTask;
public TestPlayerLoader(Func<Player> createPlayer) public TestPlayerLoader(Func<Player> createPlayer)
: base(createPlayer) : base(createPlayer)
{ {

View File

@ -0,0 +1,193 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Lines;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osuTK;
namespace osu.Game.Tests.Visual.Gameplay
{
public class TestSceneSliderPath : OsuTestScene
{
private readonly SmoothPath drawablePath;
private SliderPath path;
public TestSceneSliderPath()
{
Child = drawablePath = new SmoothPath
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre
};
}
[SetUp]
public void Setup() => Schedule(() =>
{
path = new SliderPath();
});
protected override void Update()
{
base.Update();
if (path != null)
{
List<Vector2> vertices = new List<Vector2>();
path.GetPathToProgress(vertices, 0, 1);
drawablePath.Vertices = vertices;
}
}
[Test]
public void TestEmptyPath()
{
}
[TestCase(PathType.Linear)]
[TestCase(PathType.Bezier)]
[TestCase(PathType.Catmull)]
[TestCase(PathType.PerfectCurve)]
public void TestSingleSegment(PathType type)
=> AddStep("create path", () => path.ControlPoints.AddRange(createSegment(type, Vector2.Zero, new Vector2(0, 100), new Vector2(100))));
[TestCase(PathType.Linear)]
[TestCase(PathType.Bezier)]
[TestCase(PathType.Catmull)]
[TestCase(PathType.PerfectCurve)]
public void TestMultipleSegment(PathType type)
{
AddStep("create path", () =>
{
path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero));
path.ControlPoints.AddRange(createSegment(type, new Vector2(0, 100), new Vector2(100), Vector2.Zero));
});
}
[Test]
public void TestAddControlPoint()
{
AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(0, 100))));
AddStep("add point", () => path.ControlPoints.Add(new PathControlPoint { Position = { Value = new Vector2(100) } }));
}
[Test]
public void TestInsertControlPoint()
{
AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(100))));
AddStep("insert point", () => path.ControlPoints.Insert(1, new PathControlPoint { Position = { Value = new Vector2(0, 100) } }));
}
[Test]
public void TestRemoveControlPoint()
{
AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(0, 100), new Vector2(100))));
AddStep("remove second point", () => path.ControlPoints.RemoveAt(1));
}
[Test]
public void TestChangePathType()
{
AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(0, 100), new Vector2(100))));
AddStep("change type to bezier", () => path.ControlPoints[0].Type.Value = PathType.Bezier);
}
[Test]
public void TestAddSegmentByChangingType()
{
AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(0, 100), new Vector2(100), new Vector2(100, 0))));
AddStep("change second point type to bezier", () => path.ControlPoints[1].Type.Value = PathType.Bezier);
}
[Test]
public void TestRemoveSegmentByChangingType()
{
AddStep("create path", () =>
{
path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(0, 100), new Vector2(100), new Vector2(100, 0)));
path.ControlPoints[1].Type.Value = PathType.Bezier;
});
AddStep("change second point type to null", () => path.ControlPoints[1].Type.Value = null);
}
[Test]
public void TestRemoveSegmentByRemovingControlPoint()
{
AddStep("create path", () =>
{
path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(0, 100), new Vector2(100), new Vector2(100, 0)));
path.ControlPoints[1].Type.Value = PathType.Bezier;
});
AddStep("remove second point", () => path.ControlPoints.RemoveAt(1));
}
[TestCase(2)]
[TestCase(4)]
public void TestPerfectCurveFallbackScenarios(int points)
{
AddStep("create path", () =>
{
switch (points)
{
case 2:
path.ControlPoints.AddRange(createSegment(PathType.PerfectCurve, Vector2.Zero, new Vector2(0, 100)));
break;
case 4:
path.ControlPoints.AddRange(createSegment(PathType.PerfectCurve, Vector2.Zero, new Vector2(0, 100), new Vector2(100), new Vector2(100, 0)));
break;
}
});
}
[Test]
public void TestLengthenLastSegment()
{
AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(0, 100), new Vector2(100))));
AddStep("lengthen last segment", () => path.ExpectedDistance.Value = 300);
}
[Test]
public void TestShortenLastSegment()
{
AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(0, 100), new Vector2(100))));
AddStep("shorten last segment", () => path.ExpectedDistance.Value = 150);
}
[Test]
public void TestShortenFirstSegment()
{
AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(0, 100), new Vector2(100))));
AddStep("shorten first segment", () => path.ExpectedDistance.Value = 50);
}
[Test]
public void TestShortenToZeroLength()
{
AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(0, 100), new Vector2(100))));
AddStep("shorten to 0 length", () => path.ExpectedDistance.Value = 0);
}
[Test]
public void TestShortenToNegativeLength()
{
AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(0, 100), new Vector2(100))));
AddStep("shorten to -10 length", () => path.ExpectedDistance.Value = -10);
}
private List<PathControlPoint> createSegment(PathType type, params Vector2[] controlPoints)
{
var points = controlPoints.Select(p => new PathControlPoint { Position = { Value = p } }).ToList();
points[0].Type.Value = type;
return points;
}
}
}

View File

@ -95,6 +95,42 @@ namespace osu.Game.Tests.Visual.SongSelect
AddAssert("filter count is 1", () => songSelect.FilterCount == 1); AddAssert("filter count is 1", () => songSelect.FilterCount == 1);
} }
[Test]
public void TestNoFilterOnSimpleResume()
{
addRulesetImportStep(0);
addRulesetImportStep(0);
createSongSelect();
AddStep("push child screen", () => Stack.Push(new TestSceneOsuScreenStack.TestScreen("test child")));
AddUntilStep("wait for not current", () => !songSelect.IsCurrentScreen());
AddStep("return", () => songSelect.MakeCurrent());
AddUntilStep("wait for current", () => songSelect.IsCurrentScreen());
AddAssert("filter count is 1", () => songSelect.FilterCount == 1);
}
[Test]
public void TestFilterOnResumeAfterChange()
{
addRulesetImportStep(0);
addRulesetImportStep(0);
AddStep("change convert setting", () => config.Set(OsuSetting.ShowConvertedBeatmaps, false));
createSongSelect();
AddStep("push child screen", () => Stack.Push(new TestSceneOsuScreenStack.TestScreen("test child")));
AddUntilStep("wait for not current", () => !songSelect.IsCurrentScreen());
AddStep("change convert setting", () => config.Set(OsuSetting.ShowConvertedBeatmaps, true));
AddStep("return", () => songSelect.MakeCurrent());
AddUntilStep("wait for current", () => songSelect.IsCurrentScreen());
AddAssert("filter count is 2", () => songSelect.FilterCount == 2);
}
[Test] [Test]
public void TestAudioResuming() public void TestAudioResuming()
{ {

View File

@ -42,7 +42,7 @@ namespace osu.Game.Tests.Visual
AddAssert("Parallax is off", () => stack.ParallaxAmount == 0); AddAssert("Parallax is off", () => stack.ParallaxAmount == 0);
} }
private class TestScreen : ScreenWithBeatmapBackground public class TestScreen : ScreenWithBeatmapBackground
{ {
private readonly string screenText; private readonly string screenText;

View File

@ -1,9 +1,12 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.Dialog; using osu.Game.Overlays.Dialog;
namespace osu.Game.Tests.Visual.UserInterface namespace osu.Game.Tests.Visual.UserInterface
@ -11,13 +14,22 @@ namespace osu.Game.Tests.Visual.UserInterface
[TestFixture] [TestFixture]
public class TestScenePopupDialog : OsuTestScene public class TestScenePopupDialog : OsuTestScene
{ {
public override IReadOnlyList<Type> RequiredTypes => new[]
{
typeof(PopupDialogOkButton),
typeof(PopupDialogCancelButton),
typeof(PopupDialogButton),
typeof(DialogButton),
};
public TestScenePopupDialog() public TestScenePopupDialog()
{ {
AddStep("new popup", () =>
Add(new TestPopupDialog Add(new TestPopupDialog
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
State = { Value = Framework.Graphics.Containers.Visibility.Visible }, State = { Value = Framework.Graphics.Containers.Visibility.Visible },
}); }));
} }
private class TestPopupDialog : PopupDialog private class TestPopupDialog : PopupDialog

View File

@ -9,6 +9,6 @@
<ProjectReference Include="..\osu.Game\osu.Game.csproj" /> <ProjectReference Include="..\osu.Game\osu.Game.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Microsoft.Win32.Registry" Version="4.6.0" /> <PackageReference Include="Microsoft.Win32.Registry" Version="4.7.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Bindables; using osu.Framework.Bindables;
@ -9,8 +10,8 @@ using osu.Game.Rulesets;
namespace osu.Game.Configuration namespace osu.Game.Configuration
{ {
public abstract class DatabasedConfigManager<T> : ConfigManager<T> public abstract class DatabasedConfigManager<TLookup> : ConfigManager<TLookup>
where T : struct where TLookup : struct, Enum
{ {
private readonly SettingsStore settings; private readonly SettingsStore settings;
@ -53,7 +54,7 @@ namespace osu.Game.Configuration
private readonly List<DatabasedSetting> dirtySettings = new List<DatabasedSetting>(); private readonly List<DatabasedSetting> dirtySettings = new List<DatabasedSetting>();
protected override void AddBindable<TBindable>(T lookup, Bindable<TBindable> bindable) protected override void AddBindable<TBindable>(TLookup lookup, Bindable<TBindable> bindable)
{ {
base.AddBindable(lookup, bindable); base.AddBindable(lookup, bindable);

View File

@ -1,12 +1,13 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Configuration; using osu.Framework.Configuration;
namespace osu.Game.Configuration namespace osu.Game.Configuration
{ {
public class InMemoryConfigManager<T> : ConfigManager<T> public class InMemoryConfigManager<TLookup> : ConfigManager<TLookup>
where T : struct where TLookup : struct, Enum
{ {
public InMemoryConfigManager() public InMemoryConfigManager()
{ {

View File

@ -3,7 +3,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Reflection; using System.Reflection;
using JetBrains.Annotations; using JetBrains.Annotations;
using osu.Framework.Bindables; using osu.Framework.Bindables;
@ -36,11 +35,13 @@ namespace osu.Game.Configuration
{ {
public static IEnumerable<Drawable> CreateSettingsControls(this object obj) public static IEnumerable<Drawable> CreateSettingsControls(this object obj)
{ {
var configProperties = obj.GetType().GetProperties(BindingFlags.GetProperty | BindingFlags.Public | BindingFlags.Instance).Where(p => p.GetCustomAttribute<SettingSourceAttribute>(true) != null); foreach (var property in obj.GetType().GetProperties(BindingFlags.GetProperty | BindingFlags.Public | BindingFlags.Instance))
foreach (var property in configProperties)
{ {
var attr = property.GetCustomAttribute<SettingSourceAttribute>(true); var attr = property.GetCustomAttribute<SettingSourceAttribute>(true);
if (attr == null)
continue;
var prop = property.GetValue(obj); var prop = property.GetValue(obj);
switch (prop) switch (prop)
@ -91,9 +92,7 @@ namespace osu.Game.Configuration
break; break;
case IBindable bindable: case IBindable bindable:
var dropdownType = typeof(SettingsEnumDropdown<>).MakeGenericType(bindable.GetType().GetGenericArguments()[0]); var dropdownType = typeof(SettingsEnumDropdown<>).MakeGenericType(bindable.GetType().GetGenericArguments()[0]);
var dropdown = (Drawable)Activator.CreateInstance(dropdownType); var dropdown = (Drawable)Activator.CreateInstance(dropdownType);
dropdown.GetType().GetProperty(nameof(IHasCurrentValue<object>.Current))?.SetValue(dropdown, obj); dropdown.GetType().GetProperty(nameof(IHasCurrentValue<object>.Current))?.SetValue(dropdown, obj);

View File

@ -24,7 +24,7 @@ namespace osu.Game.Graphics
/// </summary> /// </summary>
/// <returns>A <see cref="TransformSequence{T}"/> to which further transforms can be added.</returns> /// <returns>A <see cref="TransformSequence{T}"/> to which further transforms can be added.</returns>
public static TransformSequence<T> FadeAccent<T>(this T accentedDrawable, Color4 newColour, double duration = 0, Easing easing = Easing.None) public static TransformSequence<T> FadeAccent<T>(this T accentedDrawable, Color4 newColour, double duration = 0, Easing easing = Easing.None)
where T : IHasAccentColour where T : class, IHasAccentColour
=> accentedDrawable.TransformTo(nameof(accentedDrawable.AccentColour), newColour, duration, easing); => accentedDrawable.TransformTo(nameof(accentedDrawable.AccentColour), newColour, duration, easing);
/// <summary> /// <summary>

View File

@ -20,9 +20,10 @@ namespace osu.Game.Graphics.UserInterface
{ {
public class DialogButton : OsuClickableContainer public class DialogButton : OsuClickableContainer
{ {
private const float idle_width = 0.8f;
private const float hover_width = 0.9f; private const float hover_width = 0.9f;
private const float hover_duration = 500; private const float hover_duration = 500;
private const float glow_fade_duration = 250;
private const float click_duration = 200; private const float click_duration = 200;
public readonly BindableBool Selected = new BindableBool(); public readonly BindableBool Selected = new BindableBool();
@ -99,7 +100,7 @@ namespace osu.Game.Graphics.UserInterface
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Width = 0.8f, Width = idle_width,
Masking = true, Masking = true,
MaskingSmoothness = 2, MaskingSmoothness = 2,
EdgeEffect = new EdgeEffectParameters EdgeEffect = new EdgeEffectParameters
@ -199,26 +200,50 @@ namespace osu.Game.Graphics.UserInterface
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => backgroundContainer.ReceivePositionalInputAt(screenSpacePos); public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => backgroundContainer.ReceivePositionalInputAt(screenSpacePos);
private bool clickAnimating;
protected override bool OnClick(ClickEvent e) protected override bool OnClick(ClickEvent e)
{ {
colourContainer.ResizeTo(new Vector2(1.5f, 1f), click_duration, Easing.In); var flash = new Box
flash();
this.Delay(click_duration).Schedule(delegate
{ {
colourContainer.ResizeTo(new Vector2(0.8f, 1f)); RelativeSizeAxes = Axes.Both,
spriteText.Spacing = Vector2.Zero; Colour = ButtonColour,
glowContainer.FadeOut(); Blending = BlendingParameters.Additive,
Alpha = 0.05f
};
colourContainer.Add(flash);
flash.FadeOutFromOne(100).Expire();
clickAnimating = true;
colourContainer.ResizeWidthTo(colourContainer.Width * 1.05f, 100, Easing.OutQuint)
.OnComplete(_ =>
{
clickAnimating = false;
Selected.TriggerChange();
}); });
return base.OnClick(e); return base.OnClick(e);
} }
protected override bool OnMouseDown(MouseDownEvent e)
{
colourContainer.ResizeWidthTo(hover_width * 0.98f, click_duration * 4, Easing.OutQuad);
return base.OnMouseDown(e);
}
protected override bool OnMouseUp(MouseUpEvent e)
{
if (Selected.Value)
colourContainer.ResizeWidthTo(hover_width, click_duration, Easing.In);
return base.OnMouseUp(e);
}
protected override bool OnHover(HoverEvent e) protected override bool OnHover(HoverEvent e)
{ {
base.OnHover(e); base.OnHover(e);
Selected.Value = true; Selected.Value = true;
return true; return true;
} }
@ -230,36 +255,23 @@ namespace osu.Game.Graphics.UserInterface
private void selectionChanged(ValueChangedEvent<bool> args) private void selectionChanged(ValueChangedEvent<bool> args)
{ {
if (clickAnimating)
return;
if (args.NewValue) if (args.NewValue)
{ {
spriteText.TransformSpacingTo(hoverSpacing, hover_duration, Easing.OutElastic); spriteText.TransformSpacingTo(hoverSpacing, hover_duration, Easing.OutElastic);
colourContainer.ResizeTo(new Vector2(hover_width, 1f), hover_duration, Easing.OutElastic); colourContainer.ResizeWidthTo(hover_width, hover_duration, Easing.OutElastic);
glowContainer.FadeIn(glow_fade_duration, Easing.Out); glowContainer.FadeIn(hover_duration, Easing.OutQuint);
} }
else else
{ {
colourContainer.ResizeTo(new Vector2(0.8f, 1f), hover_duration, Easing.OutElastic); colourContainer.ResizeWidthTo(idle_width, hover_duration, Easing.OutElastic);
spriteText.TransformSpacingTo(Vector2.Zero, hover_duration, Easing.OutElastic); spriteText.TransformSpacingTo(Vector2.Zero, hover_duration, Easing.OutElastic);
glowContainer.FadeOut(glow_fade_duration, Easing.Out); glowContainer.FadeOut(hover_duration, Easing.OutQuint);
} }
} }
private void flash()
{
var flash = new Box
{
RelativeSizeAxes = Axes.Both
};
colourContainer.Add(flash);
flash.Colour = ButtonColour;
flash.Blending = BlendingParameters.Additive;
flash.Alpha = 0.3f;
flash.FadeOutFromOne(click_duration);
flash.Expire();
}
private void updateGlow() private void updateGlow()
{ {
leftGlow.Colour = ColourInfo.GradientHorizontal(new Color4(ButtonColour.R, ButtonColour.G, ButtonColour.B, 0f), ButtonColour); leftGlow.Colour = ColourInfo.GradientHorizontal(new Color4(ButtonColour.R, ButtonColour.G, ButtonColour.B, 0f), ButtonColour);

View File

@ -6,12 +6,10 @@ using System;
namespace osu.Game.Graphics.UserInterface namespace osu.Game.Graphics.UserInterface
{ {
public class OsuEnumDropdown<T> : OsuDropdown<T> public class OsuEnumDropdown<T> : OsuDropdown<T>
where T : struct, Enum
{ {
public OsuEnumDropdown() public OsuEnumDropdown()
{ {
if (!typeof(T).IsEnum)
throw new InvalidOperationException("OsuEnumDropdown only supports enums as the generic type argument");
Items = (T[])Enum.GetValues(typeof(T)); Items = (T[])Enum.GetValues(typeof(T));
} }
} }

View File

@ -34,7 +34,7 @@ namespace osu.Game.Online.API.Requests.Responses
PP = PP, PP = PP,
Beatmap = Beatmap, Beatmap = Beatmap,
RulesetID = OnlineRulesetID, RulesetID = OnlineRulesetID,
Hash = "online", // todo: temporary? Hash = Replay ? "online" : string.Empty, // todo: temporary?
Rank = Rank, Rank = Rank,
Ruleset = ruleset, Ruleset = ruleset,
Mods = mods, Mods = mods,

View File

@ -26,7 +26,7 @@ namespace osu.Game.Online
/// </summary> /// </summary>
protected readonly Bindable<DownloadState> State = new Bindable<DownloadState>(); protected readonly Bindable<DownloadState> State = new Bindable<DownloadState>();
protected readonly Bindable<double> Progress = new Bindable<double>(); protected readonly BindableNumber<double> Progress = new BindableNumber<double> { MinValue = 0, MaxValue = 1 };
protected DownloadTrackingComposite(TModel model = null) protected DownloadTrackingComposite(TModel model = null)
{ {

View File

@ -63,7 +63,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
return; return;
for (int i = 0; i < value.Count; i++) for (int i = 0; i < value.Count; i++)
backgroundFlow.Add(new ScoreTableRowBackground(i)); backgroundFlow.Add(new ScoreTableRowBackground(i, value[i]));
Columns = createHeaders(value[0]); Columns = createHeaders(value[0]);
Content = value.Select((s, i) => createContent(i, s)).ToArray().ToRectangular(); Content = value.Select((s, i) => createContent(i, s)).ToArray().ToRectangular();

View File

@ -7,6 +7,8 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Online.API;
using osu.Game.Scoring;
namespace osu.Game.Overlays.BeatmapSet.Scores namespace osu.Game.Overlays.BeatmapSet.Scores
{ {
@ -17,8 +19,14 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
private readonly Box hoveredBackground; private readonly Box hoveredBackground;
private readonly Box background; private readonly Box background;
public ScoreTableRowBackground(int index) private readonly int index;
private readonly ScoreInfo score;
public ScoreTableRowBackground(int index, ScoreInfo score)
{ {
this.index = index;
this.score = score;
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
Height = 25; Height = 25;
@ -37,16 +45,21 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
Alpha = 0, Alpha = 0,
}, },
}; };
if (index % 2 != 0)
background.Alpha = 0;
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colours) private void load(OsuColour colours, IAPIProvider api)
{ {
hoveredBackground.Colour = colours.Gray4; var isOwnScore = api.LocalUser.Value.Id == score.UserID;
if (isOwnScore)
background.Colour = colours.GreenDarker;
else if (index % 2 == 0)
background.Colour = colours.Gray3; background.Colour = colours.Gray3;
else
background.Alpha = 0;
hoveredBackground.Colour = isOwnScore ? colours.GreenDark : colours.Gray4;
} }
protected override bool OnHover(HoverEvent e) protected override bool OnHover(HoverEvent e)

View File

@ -167,10 +167,6 @@ namespace osu.Game.Overlays.Mods
{ {
switch (e.Button) switch (e.Button)
{ {
case MouseButton.Left:
SelectNext(1);
break;
case MouseButton.Right: case MouseButton.Right:
SelectNext(-1); SelectNext(-1);
break; break;
@ -180,6 +176,13 @@ namespace osu.Game.Overlays.Mods
return true; return true;
} }
protected override bool OnClick(ClickEvent e)
{
SelectNext(1);
return true;
}
/// <summary> /// <summary>
/// Select the next available mod in a specified direction. /// Select the next available mod in a specified direction.
/// </summary> /// </summary>

View File

@ -261,8 +261,8 @@ namespace osu.Game.Overlays
if (allowRateAdjustments) if (allowRateAdjustments)
{ {
foreach (var mod in mods.Value.OfType<IApplicableToClock>()) foreach (var mod in mods.Value.OfType<IApplicableToTrack>())
mod.ApplyToClock(track); mod.ApplyToTrack(track);
} }
} }

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osuTK; using osuTK;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -11,6 +12,7 @@ using osu.Game.Graphics.Containers;
namespace osu.Game.Overlays.SearchableList namespace osu.Game.Overlays.SearchableList
{ {
public class DisplayStyleControl<T> : Container public class DisplayStyleControl<T> : Container
where T : struct, Enum
{ {
public readonly SlimEnumDropdown<T> Dropdown; public readonly SlimEnumDropdown<T> Dropdown;
public readonly Bindable<PanelDisplayStyle> DisplayStyle = new Bindable<PanelDisplayStyle>(); public readonly Bindable<PanelDisplayStyle> DisplayStyle = new Bindable<PanelDisplayStyle>();

View File

@ -13,7 +13,9 @@ using osu.Framework.Graphics.Shapes;
namespace osu.Game.Overlays.SearchableList namespace osu.Game.Overlays.SearchableList
{ {
public abstract class SearchableListFilterControl<T, U> : Container public abstract class SearchableListFilterControl<TTab, TCategory> : Container
where TTab : struct, Enum
where TCategory : struct, Enum
{ {
private const float padding = 10; private const float padding = 10;
@ -21,12 +23,12 @@ namespace osu.Game.Overlays.SearchableList
private readonly Box tabStrip; private readonly Box tabStrip;
public readonly SearchTextBox Search; public readonly SearchTextBox Search;
public readonly PageTabControl<T> Tabs; public readonly PageTabControl<TTab> Tabs;
public readonly DisplayStyleControl<U> DisplayStyleControl; public readonly DisplayStyleControl<TCategory> DisplayStyleControl;
protected abstract Color4 BackgroundColour { get; } protected abstract Color4 BackgroundColour { get; }
protected abstract T DefaultTab { get; } protected abstract TTab DefaultTab { get; }
protected abstract U DefaultCategory { get; } protected abstract TCategory DefaultCategory { get; }
protected virtual Drawable CreateSupplementaryControls() => null; protected virtual Drawable CreateSupplementaryControls() => null;
/// <summary> /// <summary>
@ -36,9 +38,6 @@ namespace osu.Game.Overlays.SearchableList
protected SearchableListFilterControl() protected SearchableListFilterControl()
{ {
if (!typeof(T).IsEnum)
throw new InvalidOperationException("SearchableListFilterControl's sort tabs only support enums as the generic type argument");
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
var controls = CreateSupplementaryControls(); var controls = CreateSupplementaryControls();
@ -90,7 +89,7 @@ namespace osu.Game.Overlays.SearchableList
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
Padding = new MarginPadding { Right = 225 }, Padding = new MarginPadding { Right = 225 },
Child = Tabs = new PageTabControl<T> Child = Tabs = new PageTabControl<TTab>
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
}, },
@ -105,7 +104,7 @@ namespace osu.Game.Overlays.SearchableList
}, },
}, },
}, },
DisplayStyleControl = new DisplayStyleControl<U> DisplayStyleControl = new DisplayStyleControl<TCategory>
{ {
Anchor = Anchor.TopRight, Anchor = Anchor.TopRight,
Origin = Anchor.TopRight, Origin = Anchor.TopRight,

View File

@ -14,6 +14,7 @@ using osu.Framework.Graphics.Sprites;
namespace osu.Game.Overlays.SearchableList namespace osu.Game.Overlays.SearchableList
{ {
public abstract class SearchableListHeader<T> : Container public abstract class SearchableListHeader<T> : Container
where T : struct, Enum
{ {
public readonly HeaderTabControl<T> Tabs; public readonly HeaderTabControl<T> Tabs;
@ -24,9 +25,6 @@ namespace osu.Game.Overlays.SearchableList
protected SearchableListHeader() protected SearchableListHeader()
{ {
if (!typeof(T).IsEnum)
throw new InvalidOperationException("BrowseHeader only supports enums as the generic type argument");
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
Height = 90; Height = 90;

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using osuTK.Graphics; using osuTK.Graphics;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
@ -16,19 +17,22 @@ namespace osu.Game.Overlays.SearchableList
public const float WIDTH_PADDING = 80; public const float WIDTH_PADDING = 80;
} }
public abstract class SearchableListOverlay<T, U, S> : SearchableListOverlay public abstract class SearchableListOverlay<THeader, TTab, TCategory> : SearchableListOverlay
where THeader : struct, Enum
where TTab : struct, Enum
where TCategory : struct, Enum
{ {
private readonly Container scrollContainer; private readonly Container scrollContainer;
protected readonly SearchableListHeader<T> Header; protected readonly SearchableListHeader<THeader> Header;
protected readonly SearchableListFilterControl<U, S> Filter; protected readonly SearchableListFilterControl<TTab, TCategory> Filter;
protected readonly FillFlowContainer ScrollFlow; protected readonly FillFlowContainer ScrollFlow;
protected abstract Color4 BackgroundColour { get; } protected abstract Color4 BackgroundColour { get; }
protected abstract Color4 TrianglesColourLight { get; } protected abstract Color4 TrianglesColourLight { get; }
protected abstract Color4 TrianglesColourDark { get; } protected abstract Color4 TrianglesColourDark { get; }
protected abstract SearchableListHeader<T> CreateHeader(); protected abstract SearchableListHeader<THeader> CreateHeader();
protected abstract SearchableListFilterControl<U, S> CreateFilterControl(); protected abstract SearchableListFilterControl<TTab, TCategory> CreateFilterControl();
protected SearchableListOverlay() protected SearchableListOverlay()
{ {

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using osuTK.Graphics; using osuTK.Graphics;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -11,6 +12,7 @@ using osuTK;
namespace osu.Game.Overlays.SearchableList namespace osu.Game.Overlays.SearchableList
{ {
public class SlimEnumDropdown<T> : OsuEnumDropdown<T> public class SlimEnumDropdown<T> : OsuEnumDropdown<T>
where T : struct, Enum
{ {
protected override DropdownHeader CreateHeader() => new SlimDropdownHeader(); protected override DropdownHeader CreateHeader() => new SlimDropdownHeader();

View File

@ -1,12 +1,14 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
namespace osu.Game.Overlays.Settings namespace osu.Game.Overlays.Settings
{ {
public class SettingsEnumDropdown<T> : SettingsDropdown<T> public class SettingsEnumDropdown<T> : SettingsDropdown<T>
where T : struct, Enum
{ {
protected override OsuDropdown<T> CreateDropdown() => new DropdownControl(); protected override OsuDropdown<T> CreateDropdown() => new DropdownControl();

View File

@ -3,16 +3,14 @@
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Input.Bindings; using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.Settings; using osu.Game.Overlays.Settings;
using osu.Game.Screens.Ranking;
using osuTK; using osuTK;
using osuTK.Graphics;
namespace osu.Game.Overlays namespace osu.Game.Overlays
{ {
@ -36,21 +34,21 @@ namespace osu.Game.Overlays
protected override bool DimMainContent => false; // dimming is handled by main overlay protected override bool DimMainContent => false; // dimming is handled by main overlay
private class BackButton : OsuClickableContainer, IKeyBindingHandler<GlobalAction> private class BackButton : OsuButton
{ {
private AspectContainer aspect;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
Size = new Vector2(Sidebar.DEFAULT_WIDTH); Size = new Vector2(Sidebar.DEFAULT_WIDTH);
Children = new Drawable[]
BackgroundColour = Color4.Black;
AddRange(new Drawable[]
{ {
aspect = new AspectContainer new Container
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Y,
Children = new Drawable[] Children = new Drawable[]
{ {
new SpriteIcon new SpriteIcon
@ -71,34 +69,8 @@ namespace osu.Game.Overlays
}, },
} }
} }
}; });
} }
protected override bool OnMouseDown(MouseDownEvent e)
{
aspect.ScaleTo(0.75f, 2000, Easing.OutQuint);
return base.OnMouseDown(e);
}
protected override bool OnMouseUp(MouseUpEvent e)
{
aspect.ScaleTo(1, 1000, Easing.OutElastic);
return base.OnMouseUp(e);
}
public bool OnPressed(GlobalAction action)
{
switch (action)
{
case GlobalAction.Back:
Click();
return true;
}
return false;
}
public bool OnReleased(GlobalAction action) => false;
} }
} }
} }

View File

@ -1,12 +1,13 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using osu.Game.Configuration; using osu.Game.Configuration;
namespace osu.Game.Rulesets.Configuration namespace osu.Game.Rulesets.Configuration
{ {
public abstract class RulesetConfigManager<T> : DatabasedConfigManager<T>, IRulesetConfigManager public abstract class RulesetConfigManager<TLookup> : DatabasedConfigManager<TLookup>, IRulesetConfigManager
where T : struct where TLookup : struct, Enum
{ {
protected RulesetConfigManager(SettingsStore settings, RulesetInfo ruleset, int? variant = null) protected RulesetConfigManager(SettingsStore settings, RulesetInfo ruleset, int? variant = null)
: base(settings, ruleset, variant) : base(settings, ruleset, variant)

View File

@ -4,8 +4,8 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Audio.Track;
using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Timing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Skills; using osu.Game.Rulesets.Difficulty.Skills;
@ -41,10 +41,10 @@ namespace osu.Game.Rulesets.Difficulty
IBeatmap playableBeatmap = beatmap.GetPlayableBeatmap(ruleset.RulesetInfo, mods); IBeatmap playableBeatmap = beatmap.GetPlayableBeatmap(ruleset.RulesetInfo, mods);
var clock = new StopwatchClock(); var track = new TrackVirtual(10000);
mods.OfType<IApplicableToClock>().ForEach(m => m.ApplyToClock(clock)); mods.OfType<IApplicableToTrack>().ForEach(m => m.ApplyToTrack(track));
return calculate(playableBeatmap, mods, clock.Rate); return calculate(playableBeatmap, mods, track.Rate);
} }
/// <summary> /// <summary>

View File

@ -3,8 +3,8 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Audio.Track;
using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Timing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Scoring; using osu.Game.Scoring;
@ -35,9 +35,9 @@ namespace osu.Game.Rulesets.Difficulty
protected virtual void ApplyMods(Mod[] mods) protected virtual void ApplyMods(Mod[] mods)
{ {
var clock = new StopwatchClock(); var track = new TrackVirtual(10000);
mods.OfType<IApplicableToClock>().ForEach(m => m.ApplyToClock(clock)); mods.OfType<IApplicableToTrack>().ForEach(m => m.ApplyToTrack(track));
TimeRate = clock.Rate; TimeRate = track.Rate;
} }
public abstract double Calculate(Dictionary<string, double> categoryDifficulty = null); public abstract double Calculate(Dictionary<string, double> categoryDifficulty = null);

View File

@ -34,7 +34,9 @@ namespace osu.Game.Rulesets.Edit
where TObject : HitObject where TObject : HitObject
{ {
protected IRulesetConfigManager Config { get; private set; } protected IRulesetConfigManager Config { get; private set; }
protected EditorBeatmap<TObject> EditorBeatmap { get; private set; }
protected new EditorBeatmap<TObject> EditorBeatmap { get; private set; }
protected readonly Ruleset Ruleset; protected readonly Ruleset Ruleset;
[Resolved] [Resolved]
@ -148,7 +150,7 @@ namespace osu.Game.Rulesets.Edit
beatmapProcessor = Ruleset.CreateBeatmapProcessor(playableBeatmap); beatmapProcessor = Ruleset.CreateBeatmapProcessor(playableBeatmap);
EditorBeatmap = new EditorBeatmap<TObject>(playableBeatmap); base.EditorBeatmap = EditorBeatmap = new EditorBeatmap<TObject>(playableBeatmap);
EditorBeatmap.HitObjectAdded += addHitObject; EditorBeatmap.HitObjectAdded += addHitObject;
EditorBeatmap.HitObjectRemoved += removeHitObject; EditorBeatmap.HitObjectRemoved += removeHitObject;
EditorBeatmap.StartTimeChanged += UpdateHitObject; EditorBeatmap.StartTimeChanged += UpdateHitObject;
@ -333,6 +335,11 @@ namespace osu.Game.Rulesets.Edit
/// </summary> /// </summary>
public abstract IEnumerable<DrawableHitObject> HitObjects { get; } public abstract IEnumerable<DrawableHitObject> HitObjects { get; }
/// <summary>
/// An editor-specific beatmap, exposing mutation events.
/// </summary>
public IEditorBeatmap EditorBeatmap { get; protected set; }
/// <summary> /// <summary>
/// Whether the user's cursor is currently in an area of the <see cref="HitObjectComposer"/> that is valid for placement. /// Whether the user's cursor is currently in an area of the <see cref="HitObjectComposer"/> that is valid for placement.
/// </summary> /// </summary>

View File

@ -1,15 +1,15 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Timing; using osu.Framework.Audio.Track;
namespace osu.Game.Rulesets.Mods namespace osu.Game.Rulesets.Mods
{ {
/// <summary> /// <summary>
/// An interface for mods that make adjustments to the track. /// An interface for mods that make adjustments to the track.
/// </summary> /// </summary>
public interface IApplicableToClock : IApplicableMod public interface IApplicableToTrack : IApplicableMod
{ {
void ApplyToClock(IAdjustableClock clock); void ApplyToTrack(Track track);
} }
} }

View File

@ -1,9 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Audio; using osu.Framework.Audio.Track;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Timing;
namespace osu.Game.Rulesets.Mods namespace osu.Game.Rulesets.Mods
{ {
@ -14,12 +13,9 @@ namespace osu.Game.Rulesets.Mods
public override IconUsage Icon => FontAwesome.Solid.Question; public override IconUsage Icon => FontAwesome.Solid.Question;
public override string Description => "Whoaaaaa..."; public override string Description => "Whoaaaaa...";
public override void ApplyToClock(IAdjustableClock clock) public override void ApplyToTrack(Track track)
{ {
if (clock is IHasPitchAdjust pitchAdjust) track.Frequency.Value *= RateAdjust;
pitchAdjust.PitchAdjust *= RateAdjust;
else
base.ApplyToClock(clock);
} }
} }
} }

View File

@ -8,7 +8,7 @@ using osu.Game.Graphics;
namespace osu.Game.Rulesets.Mods namespace osu.Game.Rulesets.Mods
{ {
public abstract class ModDoubleTime : ModTimeAdjust, IApplicableToClock public abstract class ModDoubleTime : ModTimeAdjust
{ {
public override string Name => "Double Time"; public override string Name => "Double Time";
public override string Acronym => "DT"; public override string Acronym => "DT";

View File

@ -8,7 +8,7 @@ using osu.Game.Graphics;
namespace osu.Game.Rulesets.Mods namespace osu.Game.Rulesets.Mods
{ {
public abstract class ModHalfTime : ModTimeAdjust, IApplicableToClock public abstract class ModHalfTime : ModTimeAdjust
{ {
public override string Name => "Half Time"; public override string Name => "Half Time";
public override string Acronym => "HT"; public override string Acronym => "HT";

View File

@ -1,9 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Audio; using osu.Framework.Audio.Track;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Timing;
using osu.Game.Graphics; using osu.Game.Graphics;
namespace osu.Game.Rulesets.Mods namespace osu.Game.Rulesets.Mods
@ -15,12 +14,9 @@ namespace osu.Game.Rulesets.Mods
public override IconUsage Icon => OsuIcon.ModNightcore; public override IconUsage Icon => OsuIcon.ModNightcore;
public override string Description => "Uguuuuuuuu..."; public override string Description => "Uguuuuuuuu...";
public override void ApplyToClock(IAdjustableClock clock) public override void ApplyToTrack(Track track)
{ {
if (clock is IHasPitchAdjust pitchAdjust) track.Frequency.Value *= RateAdjust;
pitchAdjust.PitchAdjust *= RateAdjust;
else
base.ApplyToClock(clock);
} }
} }
} }

View File

@ -2,23 +2,19 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using osu.Framework.Audio; using osu.Framework.Audio.Track;
using osu.Framework.Timing;
namespace osu.Game.Rulesets.Mods namespace osu.Game.Rulesets.Mods
{ {
public abstract class ModTimeAdjust : Mod public abstract class ModTimeAdjust : Mod, IApplicableToTrack
{ {
public override Type[] IncompatibleMods => new[] { typeof(ModTimeRamp) }; public override Type[] IncompatibleMods => new[] { typeof(ModTimeRamp) };
protected abstract double RateAdjust { get; } protected abstract double RateAdjust { get; }
public virtual void ApplyToClock(IAdjustableClock clock) public virtual void ApplyToTrack(Track track)
{ {
if (clock is IHasTempoAdjust tempo) track.Tempo.Value *= RateAdjust;
tempo.TempoAdjust *= RateAdjust;
else
clock.Rate *= RateAdjust;
} }
} }
} }

View File

@ -3,15 +3,14 @@
using System; using System;
using System.Linq; using System.Linq;
using osu.Framework.Audio; using osu.Framework.Audio.Track;
using osu.Framework.Timing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
namespace osu.Game.Rulesets.Mods namespace osu.Game.Rulesets.Mods
{ {
public abstract class ModTimeRamp : Mod, IUpdatableByPlayfield, IApplicableToClock, IApplicableToBeatmap public abstract class ModTimeRamp : Mod, IUpdatableByPlayfield, IApplicableToTrack, IApplicableToBeatmap
{ {
/// <summary> /// <summary>
/// The point in the beatmap at which the final ramping rate should be reached. /// The point in the beatmap at which the final ramping rate should be reached.
@ -24,11 +23,11 @@ namespace osu.Game.Rulesets.Mods
private double finalRateTime; private double finalRateTime;
private double beginRampTime; private double beginRampTime;
private IAdjustableClock clock; private Track track;
public virtual void ApplyToClock(IAdjustableClock clock) public virtual void ApplyToTrack(Track track)
{ {
this.clock = clock; this.track = track;
lastAdjust = 1; lastAdjust = 1;
@ -46,7 +45,7 @@ namespace osu.Game.Rulesets.Mods
public virtual void Update(Playfield playfield) public virtual void Update(Playfield playfield)
{ {
applyAdjustment((clock.CurrentTime - beginRampTime) / finalRateTime); applyAdjustment((track.CurrentTime - beginRampTime) / finalRateTime);
} }
private double lastAdjust = 1; private double lastAdjust = 1;
@ -59,23 +58,8 @@ namespace osu.Game.Rulesets.Mods
{ {
double adjust = 1 + (Math.Sign(FinalRateAdjustment) * Math.Clamp(amount, 0, 1) * Math.Abs(FinalRateAdjustment)); double adjust = 1 + (Math.Sign(FinalRateAdjustment) * Math.Clamp(amount, 0, 1) * Math.Abs(FinalRateAdjustment));
switch (clock) track.Tempo.Value /= lastAdjust;
{ track.Tempo.Value *= adjust;
case IHasPitchAdjust pitch:
pitch.PitchAdjust /= lastAdjust;
pitch.PitchAdjust *= adjust;
break;
case IHasTempoAdjust tempo:
tempo.TempoAdjust /= lastAdjust;
tempo.TempoAdjust *= adjust;
break;
default:
clock.Rate /= lastAdjust;
clock.Rate *= adjust;
break;
}
lastAdjust = adjust; lastAdjust = adjust;
} }

View File

@ -3,7 +3,6 @@
using osuTK; using osuTK;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Rulesets.Objects.Types;
using System.Collections.Generic; using System.Collections.Generic;
namespace osu.Game.Rulesets.Objects.Legacy.Catch namespace osu.Game.Rulesets.Objects.Legacy.Catch
@ -37,7 +36,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Catch
}; };
} }
protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, Vector2[] controlPoints, double? length, PathType pathType, int repeatCount, protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, PathControlPoint[] controlPoints, double? length, int repeatCount,
List<IList<HitSampleInfo>> nodeSamples) List<IList<HitSampleInfo>> nodeSamples)
{ {
newCombo |= forceNewCombo; newCombo |= forceNewCombo;
@ -51,7 +50,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Catch
X = position.X, X = position.X,
NewCombo = FirstObject || newCombo, NewCombo = FirstObject || newCombo,
ComboOffset = comboOffset, ComboOffset = comboOffset,
Path = new SliderPath(pathType, controlPoints, length), Path = new SliderPath(controlPoints, length),
NodeSamples = nodeSamples, NodeSamples = nodeSamples,
RepeatCount = repeatCount RepeatCount = repeatCount
}; };

View File

@ -115,12 +115,6 @@ namespace osu.Game.Rulesets.Objects.Legacy
points[pointIndex++] = new Vector2((int)Parsing.ParseDouble(temp[0], Parsing.MAX_COORDINATE_VALUE), (int)Parsing.ParseDouble(temp[1], Parsing.MAX_COORDINATE_VALUE)) - pos; points[pointIndex++] = new Vector2((int)Parsing.ParseDouble(temp[0], Parsing.MAX_COORDINATE_VALUE), (int)Parsing.ParseDouble(temp[1], Parsing.MAX_COORDINATE_VALUE)) - pos;
} }
// osu-stable special-cased colinear perfect curves to a CurveType.Linear
static bool isLinear(Vector2[] p) => Precision.AlmostEquals(0, (p[1].Y - p[0].Y) * (p[2].X - p[0].X) - (p[1].X - p[0].X) * (p[2].Y - p[0].Y));
if (points.Length == 3 && pathType == PathType.PerfectCurve && isLinear(points))
pathType = PathType.Linear;
int repeatCount = Parsing.ParseInt(split[6]); int repeatCount = Parsing.ParseInt(split[6]);
if (repeatCount > 9000) if (repeatCount > 9000)
@ -187,7 +181,7 @@ namespace osu.Game.Rulesets.Objects.Legacy
for (int i = 0; i < nodes; i++) for (int i = 0; i < nodes; i++)
nodeSamples.Add(convertSoundType(nodeSoundTypes[i], nodeBankInfos[i])); nodeSamples.Add(convertSoundType(nodeSoundTypes[i], nodeBankInfos[i]));
result = CreateSlider(pos, combo, comboOffset, points, length, pathType, repeatCount, nodeSamples); result = CreateSlider(pos, combo, comboOffset, convertControlPoints(points, pathType), length, repeatCount, nodeSamples);
// The samples are played when the slider ends, which is the last node // The samples are played when the slider ends, which is the last node
result.Samples = nodeSamples[nodeSamples.Count - 1]; result.Samples = nodeSamples[nodeSamples.Count - 1];
@ -259,6 +253,44 @@ namespace osu.Game.Rulesets.Objects.Legacy
bankInfo.Filename = split.Length > 4 ? split[4] : null; bankInfo.Filename = split.Length > 4 ? split[4] : null;
} }
private PathControlPoint[] convertControlPoints(Vector2[] vertices, PathType type)
{
if (type == PathType.PerfectCurve)
{
if (vertices.Length != 3)
type = PathType.Bezier;
else if (isLinear(vertices))
{
// osu-stable special-cased colinear perfect curves to a linear path
type = PathType.Linear;
}
}
var points = new List<PathControlPoint>(vertices.Length)
{
new PathControlPoint
{
Position = { Value = vertices[0] },
Type = { Value = type }
}
};
for (int i = 1; i < vertices.Length; i++)
{
if (vertices[i] == vertices[i - 1])
{
points[points.Count - 1].Type.Value = type;
continue;
}
points.Add(new PathControlPoint { Position = { Value = vertices[i] } });
}
return points.ToArray();
static bool isLinear(Vector2[] p) => Precision.AlmostEquals(0, (p[1].Y - p[0].Y) * (p[2].X - p[0].X) - (p[1].X - p[0].X) * (p[2].Y - p[0].Y));
}
/// <summary> /// <summary>
/// Creates a legacy Hit-type hit object. /// Creates a legacy Hit-type hit object.
/// </summary> /// </summary>
@ -276,11 +308,10 @@ namespace osu.Game.Rulesets.Objects.Legacy
/// <param name="comboOffset">When starting a new combo, the offset of the new combo relative to the current one.</param> /// <param name="comboOffset">When starting a new combo, the offset of the new combo relative to the current one.</param>
/// <param name="controlPoints">The slider control points.</param> /// <param name="controlPoints">The slider control points.</param>
/// <param name="length">The slider length.</param> /// <param name="length">The slider length.</param>
/// <param name="pathType">The slider curve type.</param>
/// <param name="repeatCount">The slider repeat count.</param> /// <param name="repeatCount">The slider repeat count.</param>
/// <param name="nodeSamples">The samples to be played when the slider nodes are hit. This includes the head and tail of the slider.</param> /// <param name="nodeSamples">The samples to be played when the slider nodes are hit. This includes the head and tail of the slider.</param>
/// <returns>The hit object.</returns> /// <returns>The hit object.</returns>
protected abstract HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, Vector2[] controlPoints, double? length, PathType pathType, int repeatCount, protected abstract HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, PathControlPoint[] controlPoints, double? length, int repeatCount,
List<IList<HitSampleInfo>> nodeSamples); List<IList<HitSampleInfo>> nodeSamples);
/// <summary> /// <summary>

View File

@ -3,7 +3,6 @@
using osuTK; using osuTK;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Rulesets.Objects.Types;
using System.Collections.Generic; using System.Collections.Generic;
namespace osu.Game.Rulesets.Objects.Legacy.Mania namespace osu.Game.Rulesets.Objects.Legacy.Mania
@ -26,13 +25,13 @@ namespace osu.Game.Rulesets.Objects.Legacy.Mania
}; };
} }
protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, Vector2[] controlPoints, double? length, PathType pathType, int repeatCount, protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, PathControlPoint[] controlPoints, double? length, int repeatCount,
List<IList<HitSampleInfo>> nodeSamples) List<IList<HitSampleInfo>> nodeSamples)
{ {
return new ConvertSlider return new ConvertSlider
{ {
X = position.X, X = position.X,
Path = new SliderPath(pathType, controlPoints, length), Path = new SliderPath(controlPoints, length),
NodeSamples = nodeSamples, NodeSamples = nodeSamples,
RepeatCount = repeatCount RepeatCount = repeatCount
}; };

View File

@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osuTK; using osuTK;
using osu.Game.Rulesets.Objects.Types;
using System.Collections.Generic; using System.Collections.Generic;
using osu.Game.Audio; using osu.Game.Audio;
@ -37,7 +36,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu
}; };
} }
protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, Vector2[] controlPoints, double? length, PathType pathType, int repeatCount, protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, PathControlPoint[] controlPoints, double? length, int repeatCount,
List<IList<HitSampleInfo>> nodeSamples) List<IList<HitSampleInfo>> nodeSamples)
{ {
newCombo |= forceNewCombo; newCombo |= forceNewCombo;
@ -51,7 +50,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu
Position = position, Position = position,
NewCombo = FirstObject || newCombo, NewCombo = FirstObject || newCombo,
ComboOffset = comboOffset, ComboOffset = comboOffset,
Path = new SliderPath(pathType, controlPoints, length), Path = new SliderPath(controlPoints, length),
NodeSamples = nodeSamples, NodeSamples = nodeSamples,
RepeatCount = repeatCount RepeatCount = repeatCount
}; };

View File

@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osuTK; using osuTK;
using osu.Game.Rulesets.Objects.Types;
using System.Collections.Generic; using System.Collections.Generic;
using osu.Game.Audio; using osu.Game.Audio;
@ -23,12 +22,12 @@ namespace osu.Game.Rulesets.Objects.Legacy.Taiko
return new ConvertHit(); return new ConvertHit();
} }
protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, Vector2[] controlPoints, double? length, PathType pathType, int repeatCount, protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, PathControlPoint[] controlPoints, double? length, int repeatCount,
List<IList<HitSampleInfo>> nodeSamples) List<IList<HitSampleInfo>> nodeSamples)
{ {
return new ConvertSlider return new ConvertSlider
{ {
Path = new SliderPath(pathType, controlPoints, length), Path = new SliderPath(controlPoints, length),
NodeSamples = nodeSamples, NodeSamples = nodeSamples,
RepeatCount = repeatCount RepeatCount = repeatCount
}; };

View File

@ -0,0 +1,52 @@
// 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.Rulesets.Objects.Types;
using osuTK;
namespace osu.Game.Rulesets.Objects
{
public class PathControlPoint : IEquatable<PathControlPoint>
{
/// <summary>
/// The position of this <see cref="PathControlPoint"/>.
/// </summary>
public readonly Bindable<Vector2> Position = new Bindable<Vector2>();
/// <summary>
/// The type of path segment starting at this <see cref="PathControlPoint"/>.
/// If null, this <see cref="PathControlPoint"/> will be a part of the previous path segment.
/// </summary>
public readonly Bindable<PathType?> Type = new Bindable<PathType?>();
/// <summary>
/// Invoked when any property of this <see cref="PathControlPoint"/> is changed.
/// </summary>
internal event Action Changed;
/// <summary>
/// Creates a new <see cref="PathControlPoint"/>.
/// </summary>
public PathControlPoint()
{
Position.ValueChanged += _ => Changed?.Invoke();
Type.ValueChanged += _ => Changed?.Invoke();
}
/// <summary>
/// Creates a new <see cref="PathControlPoint"/> with a provided position and type.
/// </summary>
/// <param name="position">The initial position.</param>
/// <param name="type">The initial type.</param>
public PathControlPoint(Vector2 position, PathType? type = null)
: this()
{
Position.Value = position;
Type.Value = type;
}
public bool Equals(PathControlPoint other) => Position.Value == other?.Position.Value && Type.Value == other.Type.Value;
}
}

View File

@ -1,68 +1,86 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Newtonsoft.Json; using Newtonsoft.Json;
using osu.Framework.Bindables;
using osu.Framework.Caching;
using osu.Framework.MathUtils; using osu.Framework.MathUtils;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osuTK; using osuTK;
namespace osu.Game.Rulesets.Objects namespace osu.Game.Rulesets.Objects
{ {
public struct SliderPath : IEquatable<SliderPath> public class SliderPath
{ {
/// <summary>
/// The current version of this <see cref="SliderPath"/>. Updated when any change to the path occurs.
/// </summary>
[JsonIgnore]
public IBindable<int> Version => version;
private readonly Bindable<int> version = new Bindable<int>();
/// <summary> /// <summary>
/// The user-set distance of the path. If non-null, <see cref="Distance"/> will match this value, /// The user-set distance of the path. If non-null, <see cref="Distance"/> will match this value,
/// and the path will be shortened/lengthened to match this length. /// and the path will be shortened/lengthened to match this length.
/// </summary> /// </summary>
public readonly double? ExpectedDistance; public readonly Bindable<double?> ExpectedDistance = new Bindable<double?>();
/// <summary>
/// The type of path.
/// </summary>
public readonly PathType Type;
[JsonProperty]
private Vector2[] controlPoints;
private List<Vector2> calculatedPath;
private List<double> cumulativeLength;
private bool isInitialised;
/// <summary>
/// Creates a new <see cref="SliderPath"/>.
/// </summary>
/// <param name="type">The type of path.</param>
/// <param name="controlPoints">The control points of the path.</param>
/// <param name="expectedDistance">A user-set distance of the path that may be shorter or longer than the true distance between all
/// <paramref name="controlPoints"/>. The path will be shortened/lengthened to match this length.
/// If null, the path will use the true distance between all <paramref name="controlPoints"/>.</param>
[JsonConstructor]
public SliderPath(PathType type, Vector2[] controlPoints, double? expectedDistance = null)
{
this = default;
this.controlPoints = controlPoints;
Type = type;
ExpectedDistance = expectedDistance;
ensureInitialised();
}
/// <summary> /// <summary>
/// The control points of the path. /// The control points of the path.
/// </summary> /// </summary>
[JsonIgnore] public readonly BindableList<PathControlPoint> ControlPoints = new BindableList<PathControlPoint>();
public ReadOnlySpan<Vector2> ControlPoints
private readonly List<Vector2> calculatedPath = new List<Vector2>();
private readonly List<double> cumulativeLength = new List<double>();
private readonly Cached pathCache = new Cached();
private double calculatedLength;
/// <summary>
/// Creates a new <see cref="SliderPath"/>.
/// </summary>
public SliderPath()
{ {
get ExpectedDistance.ValueChanged += _ => invalidate();
ControlPoints.ItemsAdded += items =>
{ {
ensureInitialised(); foreach (var c in items)
return controlPoints.AsSpan(); c.Changed += invalidate;
invalidate();
};
ControlPoints.ItemsRemoved += items =>
{
foreach (var c in items)
c.Changed -= invalidate;
invalidate();
};
} }
/// <summary>
/// Creates a new <see cref="SliderPath"/> initialised with a list of control points.
/// </summary>
/// <param name="controlPoints">An optional set of <see cref="PathControlPoint"/>s to initialise the path with.</param>
/// <param name="expectedDistance">A user-set distance of the path that may be shorter or longer than the true distance between all control points.
/// The path will be shortened/lengthened to match this length. If null, the path will use the true distance between all control points.</param>
[JsonConstructor]
public SliderPath(PathControlPoint[] controlPoints, double? expectedDistance = null)
: this()
{
ControlPoints.AddRange(controlPoints);
ExpectedDistance.Value = expectedDistance;
}
public SliderPath(PathType type, Vector2[] controlPoints, double? expectedDistance = null)
: this(controlPoints.Select((c, i) => new PathControlPoint(c, i == 0 ? (PathType?)type : null)).ToArray(), expectedDistance)
{
} }
/// <summary> /// <summary>
@ -73,11 +91,23 @@ namespace osu.Game.Rulesets.Objects
{ {
get get
{ {
ensureInitialised(); ensureValid();
return cumulativeLength.Count == 0 ? 0 : cumulativeLength[cumulativeLength.Count - 1]; return cumulativeLength.Count == 0 ? 0 : cumulativeLength[cumulativeLength.Count - 1];
} }
} }
/// <summary>
/// The distance of the path prior to lengthening/shortening to account for <see cref="ExpectedDistance"/>.
/// </summary>
public double CalculatedDistance
{
get
{
ensureValid();
return calculatedLength;
}
}
/// <summary> /// <summary>
/// Computes the slider path until a given progress that ranges from 0 (beginning of the slider) /// Computes the slider path until a given progress that ranges from 0 (beginning of the slider)
/// to 1 (end of the slider) and stores the generated path in the given list. /// to 1 (end of the slider) and stores the generated path in the given list.
@ -87,7 +117,7 @@ namespace osu.Game.Rulesets.Objects
/// <param name="p1">End progress. Ranges from 0 (beginning of the slider) to 1 (end of the slider).</param> /// <param name="p1">End progress. Ranges from 0 (beginning of the slider) to 1 (end of the slider).</param>
public void GetPathToProgress(List<Vector2> path, double p0, double p1) public void GetPathToProgress(List<Vector2> path, double p0, double p1)
{ {
ensureInitialised(); ensureValid();
double d0 = progressToDistance(p0); double d0 = progressToDistance(p0);
double d1 = progressToDistance(p1); double d1 = progressToDistance(p1);
@ -116,40 +146,73 @@ namespace osu.Game.Rulesets.Objects
/// <returns></returns> /// <returns></returns>
public Vector2 PositionAt(double progress) public Vector2 PositionAt(double progress)
{ {
ensureInitialised(); ensureValid();
double d = progressToDistance(progress); double d = progressToDistance(progress);
return interpolateVertices(indexOfDistance(d), d); return interpolateVertices(indexOfDistance(d), d);
} }
private void ensureInitialised() private void invalidate()
{ {
if (isInitialised) pathCache.Invalidate();
return; version.Value++;
isInitialised = true;
controlPoints ??= Array.Empty<Vector2>();
calculatedPath = new List<Vector2>();
cumulativeLength = new List<double>();
calculatePath();
calculateCumulativeLength();
} }
private List<Vector2> calculateSubpath(ReadOnlySpan<Vector2> subControlPoints) private void ensureValid()
{ {
switch (Type) if (pathCache.IsValid)
return;
calculatePath();
calculateLength();
pathCache.Validate();
}
private void calculatePath()
{
calculatedPath.Clear();
if (ControlPoints.Count == 0)
return;
Vector2[] vertices = new Vector2[ControlPoints.Count];
for (int i = 0; i < ControlPoints.Count; i++)
vertices[i] = ControlPoints[i].Position.Value;
int start = 0;
for (int i = 0; i < ControlPoints.Count; i++)
{
if (ControlPoints[i].Type.Value == null && i < ControlPoints.Count - 1)
continue;
// The current vertex ends the segment
var segmentVertices = vertices.AsSpan().Slice(start, i - start + 1);
var segmentType = ControlPoints[start].Type.Value ?? PathType.Linear;
foreach (Vector2 t in calculateSubPath(segmentVertices, segmentType))
{
if (calculatedPath.Count == 0 || calculatedPath.Last() != t)
calculatedPath.Add(t);
}
// Start the new segment at the current vertex
start = i;
}
}
private List<Vector2> calculateSubPath(ReadOnlySpan<Vector2> subControlPoints, PathType type)
{
switch (type)
{ {
case PathType.Linear: case PathType.Linear:
return PathApproximator.ApproximateLinear(subControlPoints); return PathApproximator.ApproximateLinear(subControlPoints);
case PathType.PerfectCurve: case PathType.PerfectCurve:
//we can only use CircularArc iff we have exactly three control points and no dissection. if (subControlPoints.Length != 3)
if (ControlPoints.Length != 3 || subControlPoints.Length != 3)
break; break;
// Here we have exactly 3 control points. Attempt to fit a circular arc.
List<Vector2> subpath = PathApproximator.ApproximateCircularArc(subControlPoints); List<Vector2> subpath = PathApproximator.ApproximateCircularArc(subControlPoints);
// If for some reason a circular arc could not be fit to the 3 given points, fall back to a numerically stable bezier approximation. // If for some reason a circular arc could not be fit to the 3 given points, fall back to a numerically stable bezier approximation.
@ -165,74 +228,49 @@ namespace osu.Game.Rulesets.Objects
return PathApproximator.ApproximateBezier(subControlPoints); return PathApproximator.ApproximateBezier(subControlPoints);
} }
private void calculatePath() private void calculateLength()
{ {
calculatedPath.Clear(); calculatedLength = 0;
// Sliders may consist of various subpaths separated by two consecutive vertices
// with the same position. The following loop parses these subpaths and computes
// their shape independently, consecutively appending them to calculatedPath.
int start = 0;
int end = 0;
for (int i = 0; i < ControlPoints.Length; ++i)
{
end++;
if (i == ControlPoints.Length - 1 || ControlPoints[i] == ControlPoints[i + 1])
{
ReadOnlySpan<Vector2> cpSpan = ControlPoints.Slice(start, end - start);
foreach (Vector2 t in calculateSubpath(cpSpan))
{
if (calculatedPath.Count == 0 || calculatedPath.Last() != t)
calculatedPath.Add(t);
}
start = end;
}
}
}
private void calculateCumulativeLength()
{
double l = 0;
cumulativeLength.Clear(); cumulativeLength.Clear();
cumulativeLength.Add(l); cumulativeLength.Add(0);
for (int i = 0; i < calculatedPath.Count - 1; ++i) for (int i = 0; i < calculatedPath.Count - 1; i++)
{ {
Vector2 diff = calculatedPath[i + 1] - calculatedPath[i]; Vector2 diff = calculatedPath[i + 1] - calculatedPath[i];
double d = diff.Length; calculatedLength += diff.Length;
cumulativeLength.Add(calculatedLength);
// Shorted slider paths that are too long compared to the expected distance
if (ExpectedDistance.HasValue && ExpectedDistance - l < d)
{
calculatedPath[i + 1] = calculatedPath[i] + diff * (float)((ExpectedDistance - l) / d);
calculatedPath.RemoveRange(i + 2, calculatedPath.Count - 2 - i);
l = ExpectedDistance.Value;
cumulativeLength.Add(l);
break;
} }
l += d; if (ExpectedDistance.Value is double expectedDistance && calculatedLength != expectedDistance)
cumulativeLength.Add(l); {
// The last length is always incorrect
cumulativeLength.RemoveAt(cumulativeLength.Count - 1);
int pathEndIndex = calculatedPath.Count - 1;
if (calculatedLength > expectedDistance)
{
// The path will be shortened further, in which case we should trim any more unnecessary lengths and their associated path segments
while (cumulativeLength.Count > 0 && cumulativeLength[cumulativeLength.Count - 1] >= expectedDistance)
{
cumulativeLength.RemoveAt(cumulativeLength.Count - 1);
calculatedPath.RemoveAt(pathEndIndex--);
}
} }
// Lengthen slider paths that are too short compared to the expected distance if (pathEndIndex <= 0)
if (ExpectedDistance.HasValue && l < ExpectedDistance && calculatedPath.Count > 1)
{ {
Vector2 diff = calculatedPath[calculatedPath.Count - 1] - calculatedPath[calculatedPath.Count - 2]; // The expected distance is negative or zero
double d = diff.Length; // TODO: Perhaps negative path lengths should be disallowed altogether
cumulativeLength.Add(0);
if (d <= 0)
return; return;
}
calculatedPath[calculatedPath.Count - 1] += diff * (float)((ExpectedDistance - l) / d); // The direction of the segment to shorten or lengthen
cumulativeLength[calculatedPath.Count - 1] = ExpectedDistance.Value; Vector2 dir = (calculatedPath[pathEndIndex] - calculatedPath[pathEndIndex - 1]).Normalized();
calculatedPath[pathEndIndex] = calculatedPath[pathEndIndex - 1] + dir * (float)(expectedDistance - cumulativeLength[cumulativeLength.Count - 1]);
cumulativeLength.Add(expectedDistance);
} }
} }
@ -272,7 +310,5 @@ namespace osu.Game.Rulesets.Objects
double w = (d - d0) / (d1 - d0); double w = (d - d0) / (d1 - d0);
return p0 + (p1 - p0) * (float)w; return p0 + (p1 - p0) * (float)w;
} }
public bool Equals(SliderPath other) => ControlPoints.SequenceEqual(other.ControlPoints) && ExpectedDistance == other.ExpectedDistance && Type == other.Type;
} }
} }

View File

@ -511,15 +511,19 @@ namespace osu.Game.Rulesets.UI
public IEnumerable<string> GetAvailableResources() => throw new NotImplementedException(); public IEnumerable<string> GetAvailableResources() => throw new NotImplementedException();
public void AddAdjustment(AdjustableProperty type, BindableDouble adjustBindable) => throw new NotImplementedException(); public void AddAdjustment(AdjustableProperty type, BindableNumber<double> adjustBindable) => throw new NotImplementedException();
public void RemoveAdjustment(AdjustableProperty type, BindableDouble adjustBindable) => throw new NotImplementedException(); public void RemoveAdjustment(AdjustableProperty type, BindableNumber<double> adjustBindable) => throw new NotImplementedException();
public BindableDouble Volume => throw new NotImplementedException(); public BindableNumber<double> Volume => throw new NotImplementedException();
public BindableDouble Balance => throw new NotImplementedException(); public BindableNumber<double> Balance => throw new NotImplementedException();
public BindableDouble Frequency => throw new NotImplementedException(); public BindableNumber<double> Frequency => throw new NotImplementedException();
public BindableNumber<double> Tempo => throw new NotImplementedException();
public IBindable<double> GetAggregate(AdjustableProperty type) => throw new NotImplementedException();
public IBindable<double> AggregateVolume => throw new NotImplementedException(); public IBindable<double> AggregateVolume => throw new NotImplementedException();
@ -527,6 +531,8 @@ namespace osu.Game.Rulesets.UI
public IBindable<double> AggregateFrequency => throw new NotImplementedException(); public IBindable<double> AggregateFrequency => throw new NotImplementedException();
public IBindable<double> AggregateTempo => throw new NotImplementedException();
public int PlaybackConcurrency public int PlaybackConcurrency
{ {
get => throw new NotImplementedException(); get => throw new NotImplementedException();

View File

@ -14,12 +14,14 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
/// <summary> /// <summary>
/// Represents a part of the summary timeline.. /// Represents a part of the summary timeline..
/// </summary> /// </summary>
public abstract class TimelinePart : CompositeDrawable public abstract class TimelinePart : Container
{ {
protected readonly IBindable<WorkingBeatmap> Beatmap = new Bindable<WorkingBeatmap>(); protected readonly IBindable<WorkingBeatmap> Beatmap = new Bindable<WorkingBeatmap>();
private readonly Container timeline; private readonly Container timeline;
protected override Container<Drawable> Content => timeline;
protected TimelinePart() protected TimelinePart()
{ {
AddInternal(timeline = new Container { RelativeSizeAxes = Axes.Both }); AddInternal(timeline = new Container { RelativeSizeAxes = Axes.Both });
@ -50,8 +52,6 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
timeline.RelativeChildSize = new Vector2((float)Math.Max(1, Beatmap.Value.Track.Length), 1); timeline.RelativeChildSize = new Vector2((float)Math.Max(1, Beatmap.Value.Track.Length), 1);
} }
protected void Add(Drawable visualisation) => timeline.Add(visualisation);
protected virtual void LoadBeatmap(WorkingBeatmap beatmap) protected virtual void LoadBeatmap(WorkingBeatmap beatmap)
{ {
timeline.Clear(); timeline.Clear();

View File

@ -36,7 +36,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{ {
this.adjustableClock = adjustableClock; this.adjustableClock = adjustableClock;
Child = waveform = new WaveformGraph Add(waveform = new WaveformGraph
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Colour = colours.Blue.Opacity(0.2f), Colour = colours.Blue.Opacity(0.2f),
@ -44,7 +44,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
MidColour = colours.BlueDark, MidColour = colours.BlueDark,
HighColour = colours.BlueDarker, HighColour = colours.BlueDarker,
Depth = float.MaxValue Depth = float.MaxValue
}; });
// We don't want the centre marker to scroll // We don't want the centre marker to scroll
AddInternal(new CentreMarker()); AddInternal(new CentreMarker());

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
@ -11,17 +12,18 @@ using osuTK;
namespace osu.Game.Screens.Edit.Compose.Components.Timeline namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{ {
public class TimelineArea : CompositeDrawable public class TimelineArea : Container
{ {
private readonly Timeline timeline; private readonly Timeline timeline = new Timeline { RelativeSizeAxes = Axes.Both };
public TimelineArea() protected override Container<Drawable> Content => timeline;
[BackgroundDependencyLoader]
private void load()
{ {
Masking = true; Masking = true;
CornerRadius = 5; CornerRadius = 5;
OsuCheckbox hitObjectsCheckbox;
OsuCheckbox hitSoundsCheckbox;
OsuCheckbox waveformCheckbox; OsuCheckbox waveformCheckbox;
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
@ -60,8 +62,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
Spacing = new Vector2(0, 4), Spacing = new Vector2(0, 4),
Children = new[] Children = new[]
{ {
hitObjectsCheckbox = new OsuCheckbox { LabelText = "Hit objects" },
hitSoundsCheckbox = new OsuCheckbox { LabelText = "Hit sounds" },
waveformCheckbox = new OsuCheckbox { LabelText = "Waveform" } waveformCheckbox = new OsuCheckbox { LabelText = "Waveform" }
} }
} }
@ -107,7 +107,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
} }
} }
}, },
timeline = new Timeline { RelativeSizeAxes = Axes.Both } timeline
}, },
}, },
ColumnDimensions = new[] ColumnDimensions = new[]
@ -119,8 +119,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
} }
}; };
hitObjectsCheckbox.Current.Value = true;
hitSoundsCheckbox.Current.Value = true;
waveformCheckbox.Current.Value = true; waveformCheckbox.Current.Value = true;
timeline.WaveformVisible.BindTo(waveformCheckbox.Current); timeline.WaveformVisible.BindTo(waveformCheckbox.Current);

View File

@ -0,0 +1,108 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Screens.Edit.Components.Timelines.Summary.Parts;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
internal class TimelineHitObjectDisplay : TimelinePart
{
private IEditorBeatmap beatmap { get; }
public TimelineHitObjectDisplay(IEditorBeatmap beatmap)
{
RelativeSizeAxes = Axes.Both;
this.beatmap = beatmap;
}
[BackgroundDependencyLoader]
private void load()
{
foreach (var h in beatmap.HitObjects)
add(h);
beatmap.HitObjectAdded += add;
beatmap.HitObjectRemoved += remove;
beatmap.StartTimeChanged += h =>
{
remove(h);
add(h);
};
}
private void remove(HitObject h)
{
foreach (var d in Children.OfType<TimelineHitObjectRepresentation>().Where(c => c.HitObject == h))
d.Expire();
}
private void add(HitObject h)
{
var yOffset = Children.Count(d => d.X == h.StartTime);
Add(new TimelineHitObjectRepresentation(h) { Y = -yOffset * TimelineHitObjectRepresentation.THICKNESS });
}
private class TimelineHitObjectRepresentation : CompositeDrawable
{
public const float THICKNESS = 3;
public readonly HitObject HitObject;
public TimelineHitObjectRepresentation(HitObject hitObject)
{
HitObject = hitObject;
Anchor = Anchor.CentreLeft;
Origin = Anchor.CentreLeft;
Width = (float)(hitObject.GetEndTime() - hitObject.StartTime);
X = (float)hitObject.StartTime;
RelativePositionAxes = Axes.X;
RelativeSizeAxes = Axes.X;
if (hitObject is IHasEndTime)
{
AddInternal(new Container
{
CornerRadius = 2,
Masking = true,
Size = new Vector2(1, THICKNESS),
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
RelativePositionAxes = Axes.X,
RelativeSizeAxes = Axes.X,
Colour = Color4.Black,
Child = new Box
{
RelativeSizeAxes = Axes.Both,
}
});
}
AddInternal(new Circle
{
Size = new Vector2(16),
Anchor = Anchor.CentreLeft,
Origin = Anchor.Centre,
RelativePositionAxes = Axes.X,
AlwaysPresent = true,
Colour = Color4.White,
BorderColour = Color4.Black,
BorderThickness = THICKNESS,
});
}
}
}
}

View File

@ -3,20 +3,24 @@
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Edit;
using osu.Game.Screens.Edit.Compose.Components.Timeline;
using osu.Game.Skinning; using osu.Game.Skinning;
namespace osu.Game.Screens.Edit.Compose namespace osu.Game.Screens.Edit.Compose
{ {
public class ComposeScreen : EditorScreenWithTimeline public class ComposeScreen : EditorScreenWithTimeline
{ {
private HitObjectComposer composer;
protected override Drawable CreateMainContent() protected override Drawable CreateMainContent()
{ {
var ruleset = Beatmap.Value.BeatmapInfo.Ruleset?.CreateInstance(); var ruleset = Beatmap.Value.BeatmapInfo.Ruleset?.CreateInstance();
composer = ruleset?.CreateHitObjectComposer();
var composer = ruleset?.CreateHitObjectComposer(); if (ruleset == null || composer == null)
return new ScreenWhiteBox.UnderConstructionMessage(ruleset == null ? "This beatmap" : $"{ruleset.Description}'s composer");
if (composer != null)
{
var beatmapSkinProvider = new BeatmapSkinProvidingContainer(Beatmap.Value.Skin); var beatmapSkinProvider = new BeatmapSkinProvidingContainer(Beatmap.Value.Skin);
// the beatmapSkinProvider is used as the fallback source here to allow the ruleset-specific skin implementation // the beatmapSkinProvider is used as the fallback source here to allow the ruleset-specific skin implementation
@ -25,10 +29,9 @@ namespace osu.Game.Screens.Edit.Compose
// load the skinning hierarchy first. // load the skinning hierarchy first.
// this is intentionally done in two stages to ensure things are in a loaded state before exposing the ruleset to skin sources. // this is intentionally done in two stages to ensure things are in a loaded state before exposing the ruleset to skin sources.
return beatmapSkinProvider.WithChild(rulesetSkinProvider.WithChild(ruleset.CreateHitObjectComposer())); return beatmapSkinProvider.WithChild(rulesetSkinProvider.WithChild(composer));
} }
return new ScreenWhiteBox.UnderConstructionMessage(ruleset == null ? "This beatmap" : $"{ruleset.Description}'s composer"); protected override Drawable CreateTimelineContent() => new TimelineHitObjectDisplay(composer.EditorBeatmap);
}
} }
} }

View File

@ -20,6 +20,8 @@ namespace osu.Game.Screens.Edit
private readonly BindableBeatDivisor beatDivisor = new BindableBeatDivisor(); private readonly BindableBeatDivisor beatDivisor = new BindableBeatDivisor();
private TimelineArea timelineArea;
[BackgroundDependencyLoader(true)] [BackgroundDependencyLoader(true)]
private void load([CanBeNull] BindableBeatDivisor beatDivisor) private void load([CanBeNull] BindableBeatDivisor beatDivisor)
{ {
@ -64,7 +66,7 @@ namespace osu.Game.Screens.Edit
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Right = 5 }, Padding = new MarginPadding { Right = 5 },
Child = CreateTimeline() Child = timelineArea = CreateTimelineArea()
}, },
new BeatDivisorControl(beatDivisor) { RelativeSizeAxes = Axes.Both } new BeatDivisorControl(beatDivisor) { RelativeSizeAxes = Axes.Both }
}, },
@ -97,11 +99,15 @@ namespace osu.Game.Screens.Edit
{ {
mainContent.Add(content); mainContent.Add(content);
content.FadeInFromZero(300, Easing.OutQuint); content.FadeInFromZero(300, Easing.OutQuint);
LoadComponentAsync(CreateTimelineContent(), timelineArea.Add);
}); });
} }
protected abstract Drawable CreateMainContent(); protected abstract Drawable CreateMainContent();
protected virtual Drawable CreateTimeline() => new TimelineArea { RelativeSizeAxes = Axes.Both }; protected virtual Drawable CreateTimelineContent() => new Container();
protected TimelineArea CreateTimelineArea() => new TimelineArea { RelativeSizeAxes = Axes.Both };
} }
} }

View File

@ -23,6 +23,11 @@ namespace osu.Game.Screens.Edit
/// Invoked when a <see cref="HitObject"/> is removed from this <see cref="IEditorBeatmap"/>. /// Invoked when a <see cref="HitObject"/> is removed from this <see cref="IEditorBeatmap"/>.
/// </summary> /// </summary>
event Action<HitObject> HitObjectRemoved; event Action<HitObject> HitObjectRemoved;
/// <summary>
/// Invoked when the start time of a <see cref="HitObject"/> in this <see cref="EditorBeatmap{T}"/> was changed.
/// </summary>
event Action<HitObject> StartTimeChanged;
} }
/// <summary> /// <summary>

View File

@ -28,9 +28,9 @@ namespace osu.Game.Screens.Play
private readonly IReadOnlyList<Mod> mods; private readonly IReadOnlyList<Mod> mods;
/// <summary> /// <summary>
/// The original source (usually a <see cref="WorkingBeatmap"/>'s track). /// The <see cref="WorkingBeatmap"/>'s track.
/// </summary> /// </summary>
private IAdjustableClock sourceClock; private Track track;
public readonly BindableBool IsPaused = new BindableBool(); public readonly BindableBool IsPaused = new BindableBool();
@ -72,8 +72,8 @@ namespace osu.Game.Screens.Play
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
sourceClock = (IAdjustableClock)beatmap.Track ?? new StopwatchClock(); track = beatmap.Track;
(sourceClock as IAdjustableAudioComponent)?.AddAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust); track.AddAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust);
adjustableClock = new DecoupleableInterpolatingFramedClock { IsCoupled = false }; adjustableClock = new DecoupleableInterpolatingFramedClock { IsCoupled = false };
@ -127,11 +127,11 @@ namespace osu.Game.Screens.Play
{ {
Task.Run(() => Task.Run(() =>
{ {
sourceClock.Reset(); track.Reset();
Schedule(() => Schedule(() =>
{ {
adjustableClock.ChangeSource(sourceClock); adjustableClock.ChangeSource(track);
updateRate(); updateRate();
if (!IsPaused.Value) if (!IsPaused.Value)
@ -197,13 +197,13 @@ namespace osu.Game.Screens.Play
/// </summary> /// </summary>
public void StopUsingBeatmapClock() public void StopUsingBeatmapClock()
{ {
if (sourceClock != beatmap.Track) if (track != beatmap.Track)
return; return;
removeSourceClockAdjustments(); removeSourceClockAdjustments();
sourceClock = new TrackVirtual(beatmap.Track.Length); track = new TrackVirtual(beatmap.Track.Length);
adjustableClock.ChangeSource(sourceClock); adjustableClock.ChangeSource(track);
} }
protected override void Update() protected override void Update()
@ -214,19 +214,19 @@ namespace osu.Game.Screens.Play
base.Update(); base.Update();
} }
private bool speedAdjustmentsApplied;
private void updateRate() private void updateRate()
{ {
if (sourceClock == null) return; if (track == null) return;
sourceClock.ResetSpeedAdjustments(); speedAdjustmentsApplied = true;
track.ResetSpeedAdjustments();
if (sourceClock is IHasTempoAdjust tempo) track.Tempo.Value = UserPlaybackRate.Value;
tempo.TempoAdjust = UserPlaybackRate.Value;
else
sourceClock.Rate = UserPlaybackRate.Value;
foreach (var mod in mods.OfType<IApplicableToClock>()) foreach (var mod in mods.OfType<IApplicableToTrack>())
mod.ApplyToClock(sourceClock); mod.ApplyToTrack(track);
} }
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)
@ -234,13 +234,18 @@ namespace osu.Game.Screens.Play
base.Dispose(isDisposing); base.Dispose(isDisposing);
removeSourceClockAdjustments(); removeSourceClockAdjustments();
sourceClock = null; track = null;
} }
private void removeSourceClockAdjustments() private void removeSourceClockAdjustments()
{ {
sourceClock.ResetSpeedAdjustments(); if (speedAdjustmentsApplied)
(sourceClock as IAdjustableAudioComponent)?.RemoveAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust); {
track.ResetSpeedAdjustments();
speedAdjustmentsApplied = false;
}
track.RemoveAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust);
} }
} }
} }

View File

@ -55,7 +55,9 @@ namespace osu.Game.Screens.Play
protected override bool PlayResumeSound => false; protected override bool PlayResumeSound => false;
private Task loadTask; protected Task LoadTask { get; private set; }
protected Task DisposalTask { get; private set; }
private InputManager inputManager; private InputManager inputManager;
private IdleTracker idleTracker; private IdleTracker idleTracker;
@ -159,7 +161,7 @@ namespace osu.Game.Screens.Play
player.RestartCount = restartCount; player.RestartCount = restartCount;
player.RestartRequested = restartRequested; player.RestartRequested = restartRequested;
loadTask = LoadComponentAsync(player, _ => info.Loading = false); LoadTask = LoadComponentAsync(player, _ => info.Loading = false);
} }
private void contentIn() private void contentIn()
@ -250,7 +252,7 @@ namespace osu.Game.Screens.Play
{ {
if (!this.IsCurrentScreen()) return; if (!this.IsCurrentScreen()) return;
loadTask = null; LoadTask = null;
//By default, we want to load the player and never be returned to. //By default, we want to load the player and never be returned to.
//Note that this may change if the player we load requested a re-run. //Note that this may change if the player we load requested a re-run.
@ -301,7 +303,7 @@ namespace osu.Game.Screens.Play
if (isDisposing) if (isDisposing)
{ {
// if the player never got pushed, we should explicitly dispose it. // if the player never got pushed, we should explicitly dispose it.
loadTask?.ContinueWith(_ => player.Dispose()); DisposalTask = LoadTask?.ContinueWith(_ => player.Dispose());
} }
} }

View File

@ -44,7 +44,7 @@ namespace osu.Game.Screens.Select
} }
public struct OptionalRange<T> : IEquatable<OptionalRange<T>> public struct OptionalRange<T> : IEquatable<OptionalRange<T>>
where T : struct, IComparable where T : struct
{ {
public bool HasFilter => Max != null || Min != null; public bool HasFilter => Max != null || Min != null;

View File

@ -170,7 +170,7 @@ namespace osu.Game.Screens.Select
} }
private static void updateCriteriaRange<T>(ref FilterCriteria.OptionalRange<T> range, string op, T value) private static void updateCriteriaRange<T>(ref FilterCriteria.OptionalRange<T> range, string op, T value)
where T : struct, IComparable where T : struct
{ {
switch (op) switch (op)
{ {

View File

@ -262,8 +262,10 @@ namespace osu.Game.Screens.Select
protected virtual void ApplyFilterToCarousel(FilterCriteria criteria) protected virtual void ApplyFilterToCarousel(FilterCriteria criteria)
{ {
if (this.IsCurrentScreen()) // if not the current screen, we want to get carousel in a good presentation state before displaying (resume or enter).
Carousel.Filter(criteria); bool shouldDebounce = this.IsCurrentScreen();
Schedule(() => Carousel.Filter(criteria, shouldDebounce));
} }
private DependencyContainer dependencies; private DependencyContainer dependencies;
@ -437,8 +439,6 @@ namespace osu.Game.Screens.Select
{ {
base.OnEntering(last); base.OnEntering(last);
Carousel.Filter(FilterControl.CreateCriteria(), false);
this.FadeInFromZero(250); this.FadeInFromZero(250);
FilterControl.Activate(); FilterControl.Activate();
} }

View File

@ -1,11 +1,12 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Configuration; using osu.Framework.Configuration;
namespace osu.Game.Skinning namespace osu.Game.Skinning
{ {
public class SkinConfigManager<T> : ConfigManager<T> where T : struct public class SkinConfigManager<TLookup> : ConfigManager<TLookup> where TLookup : struct, Enum
{ {
protected override void PerformLoad() protected override void PerformLoad()
{ {

View File

@ -29,13 +29,13 @@ namespace osu.Game.Skinning
/// <param name="defaultImplementation">A function to create the default skin implementation of this element.</param> /// <param name="defaultImplementation">A function to create the default skin implementation of this element.</param>
/// <param name="allowFallback">A conditional to decide whether to allow fallback to the default implementation if a skinned element is not present.</param> /// <param name="allowFallback">A conditional to decide whether to allow fallback to the default implementation if a skinned element is not present.</param>
/// <param name="confineMode">How (if at all) the <see cref="Drawable"/> should be resize to fit within our own bounds.</param> /// <param name="confineMode">How (if at all) the <see cref="Drawable"/> should be resize to fit within our own bounds.</param>
public SkinnableDrawable(ISkinComponent component, Func<ISkinComponent, Drawable> defaultImplementation, Func<ISkinSource, bool> allowFallback = null, ConfineMode confineMode = ConfineMode.ScaleDownToFit) public SkinnableDrawable(ISkinComponent component, Func<ISkinComponent, Drawable> defaultImplementation, Func<ISkinSource, bool> allowFallback = null, ConfineMode confineMode = ConfineMode.NoScaling)
: this(component, allowFallback, confineMode) : this(component, allowFallback, confineMode)
{ {
createDefault = defaultImplementation; createDefault = defaultImplementation;
} }
protected SkinnableDrawable(ISkinComponent component, Func<ISkinSource, bool> allowFallback = null, ConfineMode confineMode = ConfineMode.ScaleDownToFit) protected SkinnableDrawable(ISkinComponent component, Func<ISkinSource, bool> allowFallback = null, ConfineMode confineMode = ConfineMode.NoScaling)
: base(allowFallback) : base(allowFallback)
{ {
this.component = component; this.component = component;

View File

@ -19,7 +19,7 @@ namespace osu.Game.Skinning
[Resolved] [Resolved]
private TextureStore textures { get; set; } private TextureStore textures { get; set; }
public SkinnableSprite(string textureName, Func<ISkinSource, bool> allowFallback = null, ConfineMode confineMode = ConfineMode.ScaleDownToFit) public SkinnableSprite(string textureName, Func<ISkinSource, bool> allowFallback = null, ConfineMode confineMode = ConfineMode.NoScaling)
: base(new SpriteComponent(textureName), allowFallback, confineMode) : base(new SpriteComponent(textureName), allowFallback, confineMode)
{ {
} }

View File

@ -8,7 +8,7 @@ namespace osu.Game.Skinning
{ {
public class SkinnableSpriteText : SkinnableDrawable, IHasText public class SkinnableSpriteText : SkinnableDrawable, IHasText
{ {
public SkinnableSpriteText(ISkinComponent component, Func<ISkinComponent, SpriteText> defaultImplementation, Func<ISkinSource, bool> allowFallback = null, ConfineMode confineMode = ConfineMode.ScaleDownToFit) public SkinnableSpriteText(ISkinComponent component, Func<ISkinComponent, SpriteText> defaultImplementation, Func<ISkinSource, bool> allowFallback = null, ConfineMode confineMode = ConfineMode.NoScaling)
: base(component, defaultImplementation, allowFallback, confineMode) : base(component, defaultImplementation, allowFallback, confineMode)
{ {
} }

View File

@ -41,7 +41,7 @@ namespace osu.Game.Storyboards.Drawables
/// </summary> /// </summary>
/// <returns>A <see cref="TransformSequence{T}"/> to which further transforms can be added.</returns> /// <returns>A <see cref="TransformSequence{T}"/> to which further transforms can be added.</returns>
public static TransformSequence<T> TransformFlipH<T>(this T flippable, bool newValue, double delay = 0) public static TransformSequence<T> TransformFlipH<T>(this T flippable, bool newValue, double delay = 0)
where T : IFlippable where T : class, IFlippable
=> flippable.TransformTo(flippable.PopulateTransform(new TransformFlipH(), newValue, delay)); => flippable.TransformTo(flippable.PopulateTransform(new TransformFlipH(), newValue, delay));
/// <summary> /// <summary>
@ -49,7 +49,7 @@ namespace osu.Game.Storyboards.Drawables
/// </summary> /// </summary>
/// <returns>A <see cref="TransformSequence{T}"/> to which further transforms can be added.</returns> /// <returns>A <see cref="TransformSequence{T}"/> to which further transforms can be added.</returns>
public static TransformSequence<T> TransformFlipV<T>(this T flippable, bool newValue, double delay = 0) public static TransformSequence<T> TransformFlipV<T>(this T flippable, bool newValue, double delay = 0)
where T : IFlippable where T : class, IFlippable
=> flippable.TransformTo(flippable.PopulateTransform(new TransformFlipV(), newValue, delay)); => flippable.TransformTo(flippable.PopulateTransform(new TransformFlipV(), newValue, delay));
} }
} }

View File

@ -13,7 +13,7 @@ namespace osu.Game.Tests.Visual
base.Update(); base.Update();
// note that this will override any mod rate application // note that this will override any mod rate application
Beatmap.Value.Track.TempoAdjust = Clock.Rate; Beatmap.Value.Track.Tempo.Value = Clock.Rate;
} }
} }
} }

View File

@ -12,7 +12,7 @@ namespace osu.Game.Tests.Visual
/// </summary> /// </summary>
public abstract class ScreenTestScene : ManualInputManagerTestScene public abstract class ScreenTestScene : ManualInputManagerTestScene
{ {
private readonly OsuScreenStack stack; protected readonly OsuScreenStack Stack;
private readonly Container content; private readonly Container content;
@ -22,16 +22,16 @@ namespace osu.Game.Tests.Visual
{ {
base.Content.AddRange(new Drawable[] base.Content.AddRange(new Drawable[]
{ {
stack = new OsuScreenStack { RelativeSizeAxes = Axes.Both }, Stack = new OsuScreenStack { RelativeSizeAxes = Axes.Both },
content = new Container { RelativeSizeAxes = Axes.Both } content = new Container { RelativeSizeAxes = Axes.Both }
}); });
} }
protected void LoadScreen(OsuScreen screen) protected void LoadScreen(OsuScreen screen)
{ {
if (stack.CurrentScreen != null) if (Stack.CurrentScreen != null)
stack.Exit(); Stack.Exit();
stack.Push(screen); Stack.Push(screen);
} }
} }
} }

View File

@ -23,10 +23,10 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2019.1010.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2019.1010.0" />
<PackageReference Include="ppy.osu.Framework" Version="2019.1205.0" /> <PackageReference Include="ppy.osu.Framework" Version="2019.1210.1" />
<PackageReference Include="Sentry" Version="1.2.0" /> <PackageReference Include="Sentry" Version="1.2.0" />
<PackageReference Include="SharpCompress" Version="0.24.0" /> <PackageReference Include="SharpCompress" Version="0.24.0" />
<PackageReference Include="NUnit" Version="3.12.0" /> <PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="System.ComponentModel.Annotations" Version="4.6.0" /> <PackageReference Include="System.ComponentModel.Annotations" Version="4.7.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -74,7 +74,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="ppy.osu.Game.Resources" Version="2019.1010.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2019.1010.0" />
<PackageReference Include="ppy.osu.Framework.iOS" Version="2019.1205.0" /> <PackageReference Include="ppy.osu.Framework.iOS" Version="2019.1210.1" />
</ItemGroup> </ItemGroup>
<!-- Xamarin.iOS does not automatically handle transitive dependencies from NuGet packages. --> <!-- Xamarin.iOS does not automatically handle transitive dependencies from NuGet packages. -->
<ItemGroup Label="Transitive Dependencies"> <ItemGroup Label="Transitive Dependencies">
@ -82,11 +82,11 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="ppy.osu.Framework" Version="2019.1205.0" /> <PackageReference Include="ppy.osu.Framework" Version="2019.1210.1" />
<PackageReference Include="SharpCompress" Version="0.24.0" /> <PackageReference Include="SharpCompress" Version="0.24.0" />
<PackageReference Include="NUnit" Version="3.12.0" /> <PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="SharpRaven" Version="2.4.0" /> <PackageReference Include="SharpRaven" Version="2.4.0" />
<PackageReference Include="System.ComponentModel.Annotations" Version="4.6.0" /> <PackageReference Include="System.ComponentModel.Annotations" Version="4.7.0" />
<PackageReference Include="ppy.osu.Framework.NativeLibs" Version="2019.1104.0" ExcludeAssets="all" /> <PackageReference Include="ppy.osu.Framework.NativeLibs" Version="2019.1104.0" ExcludeAssets="all" />
</ItemGroup> </ItemGroup>
</Project> </Project>