Merge branch 'editor-scrolling-playfield-support' of https://github.com/peppy/osu; branch 'results-screen-condensed-panel' of https://github.com/smoogipoo/osu into results-screen-condensed-panel

This commit is contained in:
smoogipoo
2020-05-26 15:39:39 +09:00
76 changed files with 1205 additions and 582 deletions

View File

@ -15,6 +15,8 @@ Rhythm is just a *click* away. The future of [osu!](https://osu.ppy.sh) and the
This project is under heavy development, but is in a stable state. Users are encouraged to try it out and keep it installed alongside the stable *osu!* client. It will continue to evolve to the point of eventually replacing the existing stable client as an update. This project is under heavy development, but is in a stable state. Users are encouraged to try it out and keep it installed alongside the stable *osu!* client. It will continue to evolve to the point of eventually replacing the existing stable client as an update.
**IMPORTANT:** Gameplay mechanics (and other features which you may have come to know and love) are in a constant state of flux. Game balance and final quality-of-life passses come at the end of development, preceeded by experimentation and changes which may potentially **reduce playability or usability**. This is done in order to allow us to move forward as developers and designers more efficiently. If this offends you, please consider sticking to the stable releases of osu! (found on the website). We are not yet open to heated discussion over game mechanics and will not be using github as a forum for such discussions just yet.
We are accepting bug reports (please report with as much detail as possible and follow the existing issue templates). Feature requests are also welcome, but understand that our focus is on completing the game to feature parity before adding new features. A few resources are available as starting points to getting involved and understanding the project: We are accepting bug reports (please report with as much detail as possible and follow the existing issue templates). Feature requests are also welcome, but understand that our focus is on completing the game to feature parity before adding new features. A few resources are available as starting points to getting involved and understanding the project:
- Detailed release changelogs are available on the [official osu! site](https://osu.ppy.sh/home/changelog/lazer). - Detailed release changelogs are available on the [official osu! site](https://osu.ppy.sh/home/changelog/lazer).

View File

@ -52,6 +52,6 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.512.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2020.512.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.518.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2020.525.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -8,7 +8,7 @@ using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Formats; using osu.Game.Beatmaps.Formats;
using osu.Game.IO; using osu.Game.IO;
using osu.Game.IO.Archives; using osu.Game.IO.Archives;
using osu.Game.Resources; using osu.Game.Tests.Resources;
namespace osu.Game.Benchmarks namespace osu.Game.Benchmarks
{ {
@ -18,8 +18,8 @@ namespace osu.Game.Benchmarks
public override void SetUp() public override void SetUp()
{ {
using (var resources = new DllResourceStore(OsuResources.ResourceAssembly)) using (var resources = new DllResourceStore(typeof(TestResources).Assembly))
using (var archive = resources.GetStream("Beatmaps/241526 Soleily - Renatus.osz")) using (var archive = resources.GetStream("Resources/Archives/241526 Soleily - Renatus.osz"))
using (var reader = new ZipArchiveReader(archive)) using (var reader = new ZipArchiveReader(archive))
reader.GetStream("Soleily - Renatus (Gamu) [Insane].osu").CopyTo(beatmapStream); reader.GetStream("Soleily - Renatus (Gamu) [Insane].osu").CopyTo(beatmapStream);
} }

View File

@ -13,6 +13,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\osu.Game.Tests\osu.Game.Tests.csproj" />
<ProjectReference Include="..\osu.Game\osu.Game.csproj" /> <ProjectReference Include="..\osu.Game\osu.Game.csproj" />
</ItemGroup> </ItemGroup>

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.Threading;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
@ -14,13 +15,13 @@ namespace osu.Game.Rulesets.Catch.Objects
public override Judgement CreateJudgement() => new IgnoreJudgement(); public override Judgement CreateJudgement() => new IgnoreJudgement();
protected override void CreateNestedHitObjects() protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
{ {
base.CreateNestedHitObjects(); base.CreateNestedHitObjects(cancellationToken);
createBananas(); createBananas(cancellationToken);
} }
private void createBananas() private void createBananas(CancellationToken cancellationToken)
{ {
double spacing = Duration; double spacing = Duration;
while (spacing > 100) while (spacing > 100)
@ -31,6 +32,8 @@ namespace osu.Game.Rulesets.Catch.Objects
for (double i = StartTime; i <= EndTime; i += spacing) for (double i = StartTime; i <= EndTime; i += spacing)
{ {
cancellationToken.ThrowIfCancellationRequested();
AddNested(new Banana AddNested(new Banana
{ {
Samples = Samples, Samples = Samples,

View File

@ -3,6 +3,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
@ -45,9 +46,9 @@ namespace osu.Game.Rulesets.Catch.Objects
TickDistance = scoringDistance / difficulty.SliderTickRate; TickDistance = scoringDistance / difficulty.SliderTickRate;
} }
protected override void CreateNestedHitObjects() protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
{ {
base.CreateNestedHitObjects(); base.CreateNestedHitObjects(cancellationToken);
var dropletSamples = Samples.Select(s => new HitSampleInfo var dropletSamples = Samples.Select(s => new HitSampleInfo
{ {
@ -58,7 +59,7 @@ namespace osu.Game.Rulesets.Catch.Objects
SliderEventDescriptor? lastEvent = null; SliderEventDescriptor? lastEvent = null;
foreach (var e in SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset)) foreach (var e in SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset, cancellationToken))
{ {
// generate tiny droplets since the last point // generate tiny droplets since the last point
if (lastEvent != null) if (lastEvent != null)
@ -73,6 +74,8 @@ namespace osu.Game.Rulesets.Catch.Objects
for (double t = timeBetweenTiny; t < sinceLastTick; t += timeBetweenTiny) for (double t = timeBetweenTiny; t < sinceLastTick; t += timeBetweenTiny)
{ {
cancellationToken.ThrowIfCancellationRequested();
AddNested(new TinyDroplet AddNested(new TinyDroplet
{ {
StartTime = t + lastEvent.Value.Time, StartTime = t + lastEvent.Value.Time,

View File

@ -7,6 +7,7 @@ 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.Timing; using osu.Framework.Timing;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Edit; using osu.Game.Rulesets.Mania.Edit;
using osu.Game.Rulesets.Mania.Objects.Drawables; using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Mania.UI;
@ -14,7 +15,6 @@ using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
namespace osu.Game.Rulesets.Mania.Tests namespace osu.Game.Rulesets.Mania.Tests
@ -43,12 +43,18 @@ namespace osu.Game.Rulesets.Mania.Tests
}); });
} }
protected override SnapResult SnapForBlueprint(PlacementBlueprint blueprint)
{
var time = column.TimeAtScreenSpacePosition(InputManager.CurrentState.Mouse.Position);
var pos = column.ScreenSpacePositionAtTime(time);
return new SnapResult(pos, time, column);
}
protected override Container CreateHitObjectContainer() => new ScrollingTestContainer(ScrollingDirection.Down) { RelativeSizeAxes = Axes.Both }; protected override Container CreateHitObjectContainer() => new ScrollingTestContainer(ScrollingDirection.Down) { RelativeSizeAxes = Axes.Both };
protected override void AddHitObject(DrawableHitObject hitObject) => column.Add((DrawableManiaHitObject)hitObject); protected override void AddHitObject(DrawableHitObject hitObject) => column.Add((DrawableManiaHitObject)hitObject);
public Column ColumnAt(Vector2 screenSpacePosition) => column; public ManiaPlayfield Playfield => null;
public int TotalColumns => 1;
} }
} }

View File

@ -7,7 +7,6 @@ using osu.Framework.Timing;
using osu.Game.Rulesets.Mania.Edit; using osu.Game.Rulesets.Mania.Edit;
using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Mania.UI;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
namespace osu.Game.Rulesets.Mania.Tests namespace osu.Game.Rulesets.Mania.Tests
@ -18,11 +17,9 @@ namespace osu.Game.Rulesets.Mania.Tests
[Cached(Type = typeof(IAdjustableClock))] [Cached(Type = typeof(IAdjustableClock))]
private readonly IAdjustableClock clock = new StopwatchClock(); private readonly IAdjustableClock clock = new StopwatchClock();
private readonly Column column;
protected ManiaSelectionBlueprintTestScene() protected ManiaSelectionBlueprintTestScene()
{ {
Add(column = new Column(0) Add(new Column(0)
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
@ -31,8 +28,6 @@ namespace osu.Game.Rulesets.Mania.Tests
}); });
} }
public Column ColumnAt(Vector2 screenSpacePosition) => column; public ManiaPlayfield Playfield => null;
public int TotalColumns => 1;
} }
} }

View File

@ -0,0 +1,70 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Input.Events;
using osu.Framework.Timing;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Configuration;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Edit;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Screens.Edit;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Mania.Tests
{
[Cached(typeof(IManiaHitObjectComposer))]
public class TestSceneManiaBeatSnapGrid : EditorClockTestScene, IManiaHitObjectComposer
{
[Cached(typeof(IScrollingInfo))]
private ScrollingTestContainer.TestScrollingInfo scrollingInfo = new ScrollingTestContainer.TestScrollingInfo();
[Cached(typeof(EditorBeatmap))]
private EditorBeatmap editorBeatmap = new EditorBeatmap(new ManiaBeatmap(new StageDefinition()));
private readonly ManiaBeatSnapGrid beatSnapGrid;
public TestSceneManiaBeatSnapGrid()
{
editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 200 });
editorBeatmap.ControlPointInfo.Add(10000, new TimingControlPoint { BeatLength = 200 });
BeatDivisor.Value = 3;
// Some sane defaults
scrollingInfo.Algorithm.Algorithm = ScrollVisualisationMethod.Constant;
scrollingInfo.Direction.Value = ScrollingDirection.Up;
scrollingInfo.TimeRange.Value = 1000;
Children = new Drawable[]
{
Playfield = new ManiaPlayfield(new List<StageDefinition>
{
new StageDefinition { Columns = 4 },
new StageDefinition { Columns = 3 }
})
{
Clock = new FramedClock(new StopwatchClock())
},
beatSnapGrid = new ManiaBeatSnapGrid()
};
}
protected override bool OnMouseMove(MouseMoveEvent e)
{
// We're providing a constant scroll algorithm.
float relativePosition = Playfield.Stages[0].HitObjectContainer.ToLocalSpace(e.ScreenSpaceMousePosition).Y / Playfield.Stages[0].HitObjectContainer.DrawHeight;
double timeValue = scrollingInfo.TimeRange.Value * relativePosition;
beatSnapGrid.SelectionTimeRange = (timeValue, timeValue);
return true;
}
public ManiaPlayfield Playfield { get; }
}
}

View File

@ -42,6 +42,7 @@ namespace osu.Game.Rulesets.Mania.Tests
public void TestDragOffscreenSelectionVerticallyUpScroll() public void TestDragOffscreenSelectionVerticallyUpScroll()
{ {
DrawableHitObject lastObject = null; DrawableHitObject lastObject = null;
double originalTime = 0;
Vector2 originalPosition = Vector2.Zero; Vector2 originalPosition = Vector2.Zero;
setScrollStep(ScrollingDirection.Up); setScrollStep(ScrollingDirection.Up);
@ -49,6 +50,7 @@ namespace osu.Game.Rulesets.Mania.Tests
AddStep("seek to last object", () => AddStep("seek to last object", () =>
{ {
lastObject = this.ChildrenOfType<DrawableHitObject>().Single(d => d.HitObject == composer.EditorBeatmap.HitObjects.Last()); lastObject = this.ChildrenOfType<DrawableHitObject>().Single(d => d.HitObject == composer.EditorBeatmap.HitObjects.Last());
originalTime = lastObject.HitObject.StartTime;
Clock.Seek(composer.EditorBeatmap.HitObjects.Last().StartTime); Clock.Seek(composer.EditorBeatmap.HitObjects.Last().StartTime);
}); });
@ -64,19 +66,20 @@ namespace osu.Game.Rulesets.Mania.Tests
AddStep("move mouse downwards", () => AddStep("move mouse downwards", () =>
{ {
InputManager.MoveMouseTo(lastObject, new Vector2(0, 20)); InputManager.MoveMouseTo(lastObject, new Vector2(0, lastObject.ScreenSpaceDrawQuad.Height * 4));
InputManager.ReleaseButton(MouseButton.Left); InputManager.ReleaseButton(MouseButton.Left);
}); });
AddAssert("hitobjects not moved columns", () => composer.EditorBeatmap.HitObjects.All(h => ((ManiaHitObject)h).Column == 0)); AddAssert("hitobjects not moved columns", () => composer.EditorBeatmap.HitObjects.All(h => ((ManiaHitObject)h).Column == 0));
AddAssert("hitobjects moved downwards", () => lastObject.DrawPosition.Y - originalPosition.Y > 0); AddAssert("hitobjects moved downwards", () => lastObject.DrawPosition.Y - originalPosition.Y > 0);
AddAssert("hitobjects not moved too far", () => lastObject.DrawPosition.Y - originalPosition.Y < 50); AddAssert("hitobject has moved time", () => lastObject.HitObject.StartTime == originalTime + 125);
} }
[Test] [Test]
public void TestDragOffscreenSelectionVerticallyDownScroll() public void TestDragOffscreenSelectionVerticallyDownScroll()
{ {
DrawableHitObject lastObject = null; DrawableHitObject lastObject = null;
double originalTime = 0;
Vector2 originalPosition = Vector2.Zero; Vector2 originalPosition = Vector2.Zero;
setScrollStep(ScrollingDirection.Down); setScrollStep(ScrollingDirection.Down);
@ -84,6 +87,7 @@ namespace osu.Game.Rulesets.Mania.Tests
AddStep("seek to last object", () => AddStep("seek to last object", () =>
{ {
lastObject = this.ChildrenOfType<DrawableHitObject>().Single(d => d.HitObject == composer.EditorBeatmap.HitObjects.Last()); lastObject = this.ChildrenOfType<DrawableHitObject>().Single(d => d.HitObject == composer.EditorBeatmap.HitObjects.Last());
originalTime = lastObject.HitObject.StartTime;
Clock.Seek(composer.EditorBeatmap.HitObjects.Last().StartTime); Clock.Seek(composer.EditorBeatmap.HitObjects.Last().StartTime);
}); });
@ -99,13 +103,13 @@ namespace osu.Game.Rulesets.Mania.Tests
AddStep("move mouse upwards", () => AddStep("move mouse upwards", () =>
{ {
InputManager.MoveMouseTo(lastObject, new Vector2(0, -20)); InputManager.MoveMouseTo(lastObject, new Vector2(0, -lastObject.ScreenSpaceDrawQuad.Height * 4));
InputManager.ReleaseButton(MouseButton.Left); InputManager.ReleaseButton(MouseButton.Left);
}); });
AddAssert("hitobjects not moved columns", () => composer.EditorBeatmap.HitObjects.All(h => ((ManiaHitObject)h).Column == 0)); AddAssert("hitobjects not moved columns", () => composer.EditorBeatmap.HitObjects.All(h => ((ManiaHitObject)h).Column == 0));
AddAssert("hitobjects moved upwards", () => originalPosition.Y - lastObject.DrawPosition.Y > 0); AddAssert("hitobjects moved upwards", () => originalPosition.Y - lastObject.DrawPosition.Y > 0);
AddAssert("hitobjects not moved too far", () => originalPosition.Y - lastObject.DrawPosition.Y < 50); AddAssert("hitobject has moved time", () => lastObject.HitObject.StartTime == originalTime + 125);
} }
[Test] [Test]
@ -207,7 +211,7 @@ namespace osu.Game.Rulesets.Mania.Tests
}; };
for (int i = 0; i < 10; i++) for (int i = 0; i < 10; i++)
EditorBeatmap.Add(new Note { StartTime = 100 * i }); EditorBeatmap.Add(new Note { StartTime = 125 * i });
} }
} }
} }

View File

@ -2,10 +2,13 @@
// 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.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Edit.Blueprints.Components; using osu.Game.Rulesets.Mania.Edit.Blueprints.Components;
using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.UI.Scrolling;
using osuTK; using osuTK;
using osuTK.Input; using osuTK.Input;
@ -17,6 +20,12 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
private readonly EditNotePiece headPiece; private readonly EditNotePiece headPiece;
private readonly EditNotePiece tailPiece; private readonly EditNotePiece tailPiece;
[Resolved]
private IManiaHitObjectComposer composer { get; set; }
[Resolved]
private IScrollingInfo scrollingInfo { get; set; }
public HoldNotePlacementBlueprint() public HoldNotePlacementBlueprint()
: base(new HoldNote()) : base(new HoldNote())
{ {
@ -36,8 +45,21 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
if (Column != null) if (Column != null)
{ {
headPiece.Y = PositionAt(HitObject.StartTime); headPiece.Y = Parent.ToLocalSpace(Column.ScreenSpacePositionAtTime(HitObject.StartTime)).Y;
tailPiece.Y = PositionAt(HitObject.EndTime); tailPiece.Y = Parent.ToLocalSpace(Column.ScreenSpacePositionAtTime(HitObject.EndTime)).Y;
switch (scrollingInfo.Direction.Value)
{
case ScrollingDirection.Down:
headPiece.Y -= headPiece.DrawHeight / 2;
tailPiece.Y -= tailPiece.DrawHeight / 2;
break;
case ScrollingDirection.Up:
headPiece.Y += headPiece.DrawHeight / 2;
tailPiece.Y += tailPiece.DrawHeight / 2;
break;
}
} }
var topPosition = new Vector2(headPiece.DrawPosition.X, Math.Min(headPiece.DrawPosition.Y, tailPiece.DrawPosition.Y)); var topPosition = new Vector2(headPiece.DrawPosition.X, Math.Min(headPiece.DrawPosition.Y, tailPiece.DrawPosition.Y));
@ -59,23 +81,28 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
private double originalStartTime; private double originalStartTime;
public override void UpdatePosition(Vector2 screenSpacePosition) public override void UpdatePosition(SnapResult result)
{ {
base.UpdatePosition(screenSpacePosition); base.UpdatePosition(result);
if (PlacementActive) if (PlacementActive)
{ {
var endTime = TimeAt(screenSpacePosition); if (result.Time is double endTime)
{
HitObject.StartTime = endTime < originalStartTime ? endTime : originalStartTime; HitObject.StartTime = endTime < originalStartTime ? endTime : originalStartTime;
HitObject.Duration = Math.Abs(endTime - originalStartTime); HitObject.Duration = Math.Abs(endTime - originalStartTime);
} }
}
else else
{ {
headPiece.Width = tailPiece.Width = SnappedWidth; if (result.Playfield != null)
headPiece.X = tailPiece.X = SnappedMousePosition.X; {
headPiece.Width = tailPiece.Width = result.Playfield.DrawWidth;
headPiece.X = tailPiece.X = ToLocalSpace(result.ScreenSpacePosition).X;
}
originalStartTime = HitObject.StartTime = TimeAt(screenSpacePosition); if (result.Time is double startTime)
originalStartTime = HitObject.StartTime = startTime;
} }
} }
} }

View File

@ -77,6 +77,6 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
public override Quad SelectionQuad => ScreenSpaceDrawQuad; public override Quad SelectionQuad => ScreenSpaceDrawQuad;
public override Vector2 SelectionPoint => DrawableObject.Head.ScreenSpaceDrawQuad.Centre; public override Vector2 ScreenSpaceSelectionPoint => DrawableObject.Head.ScreenSpaceDrawQuad.Centre;
} }
} }

View File

@ -1,43 +1,34 @@
// 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.Input;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces;
using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osuTK;
using osuTK.Input; using osuTK.Input;
namespace osu.Game.Rulesets.Mania.Edit.Blueprints namespace osu.Game.Rulesets.Mania.Edit.Blueprints
{ {
public abstract class ManiaPlacementBlueprint<T> : PlacementBlueprint, public abstract class ManiaPlacementBlueprint<T> : PlacementBlueprint
IRequireHighFrequencyMousePosition // the playfield could be moving behind us
where T : ManiaHitObject where T : ManiaHitObject
{ {
protected new T HitObject => (T)base.HitObject; protected new T HitObject => (T)base.HitObject;
protected Column Column; private Column column;
/// <summary> public Column Column
/// The current mouse position, snapped to the closest column. {
/// </summary> get => column;
protected Vector2 SnappedMousePosition { get; private set; } set
{
if (value == column)
return;
/// <summary> column = value;
/// The width of the closest column to the current mouse position. HitObject.Column = column.Index;
/// </summary> }
protected float SnappedWidth { get; private set; } }
[Resolved]
private IManiaHitObjectComposer composer { get; set; }
[Resolved]
private IScrollingInfo scrollingInfo { get; set; }
protected ManiaPlacementBlueprint(T hitObject) protected ManiaPlacementBlueprint(T hitObject)
: base(hitObject) : base(hitObject)
@ -51,105 +42,18 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
return false; return false;
if (Column == null) if (Column == null)
return base.OnMouseDown(e); return false;
HitObject.Column = Column.Index; BeginPlacement(true);
BeginPlacement(TimeAt(e.ScreenSpaceMousePosition), true);
return true; return true;
} }
public override void UpdatePosition(Vector2 screenSpacePosition) public override void UpdatePosition(SnapResult result)
{ {
base.UpdatePosition(result);
if (!PlacementActive) if (!PlacementActive)
Column = ColumnAt(screenSpacePosition); Column = result.Playfield as Column;
if (Column == null) return;
SnappedWidth = Column.DrawWidth;
// Snap to the column
var parentPos = Parent.ToLocalSpace(Column.ToScreenSpace(new Vector2(Column.DrawWidth / 2, 0)));
SnappedMousePosition = new Vector2(parentPos.X, Parent.ToLocalSpace(screenSpacePosition).Y);
}
protected double TimeAt(Vector2 screenSpacePosition)
{
if (Column == null)
return 0;
var hitObjectContainer = Column.HitObjectContainer;
// If we're scrolling downwards, a position of 0 is actually further away from the hit target
// so we need to flip the vertical coordinate in the hitobject container's space
var hitObjectPos = mouseToHitObjectPosition(Column.HitObjectContainer.ToLocalSpace(screenSpacePosition)).Y;
if (scrollingInfo.Direction.Value == ScrollingDirection.Down)
hitObjectPos = hitObjectContainer.DrawHeight - hitObjectPos;
return scrollingInfo.Algorithm.TimeAt(hitObjectPos,
EditorClock.CurrentTime,
scrollingInfo.TimeRange.Value,
hitObjectContainer.DrawHeight);
}
protected float PositionAt(double time)
{
var pos = scrollingInfo.Algorithm.PositionAt(time,
EditorClock.CurrentTime,
scrollingInfo.TimeRange.Value,
Column.HitObjectContainer.DrawHeight);
if (scrollingInfo.Direction.Value == ScrollingDirection.Down)
pos = Column.HitObjectContainer.DrawHeight - pos;
return hitObjectToMousePosition(Column.HitObjectContainer.ToSpaceOfOtherDrawable(new Vector2(0, pos), Parent)).Y;
}
protected Column ColumnAt(Vector2 screenSpacePosition)
=> composer.ColumnAt(screenSpacePosition);
/// <summary>
/// Converts a mouse position to a hitobject position.
/// </summary>
/// <remarks>
/// Blueprints are centred on the mouse position, such that the hitobject position is anchored at the top or bottom of the blueprint depending on the scroll direction.
/// </remarks>
/// <param name="mousePosition">The mouse position.</param>
/// <returns>The resulting hitobject position, acnhored at the top or bottom of the blueprint depending on the scroll direction.</returns>
private Vector2 mouseToHitObjectPosition(Vector2 mousePosition)
{
switch (scrollingInfo.Direction.Value)
{
case ScrollingDirection.Up:
mousePosition.Y -= DefaultNotePiece.NOTE_HEIGHT / 2;
break;
case ScrollingDirection.Down:
mousePosition.Y += DefaultNotePiece.NOTE_HEIGHT / 2;
break;
}
return mousePosition;
}
/// <summary>
/// Converts a hitobject position to a mouse position.
/// </summary>
/// <param name="hitObjectPosition">The hitobject position.</param>
/// <returns>The resulting mouse position, anchored at the centre of the hitobject.</returns>
private Vector2 hitObjectToMousePosition(Vector2 hitObjectPosition)
{
switch (scrollingInfo.Direction.Value)
{
case ScrollingDirection.Up:
hitObjectPosition.Y += DefaultNotePiece.NOTE_HEIGHT / 2;
break;
case ScrollingDirection.Down:
hitObjectPosition.Y -= DefaultNotePiece.NOTE_HEIGHT / 2;
break;
}
return hitObjectPosition;
} }
} }
} }

View File

@ -11,7 +11,7 @@ using osuTK;
namespace osu.Game.Rulesets.Mania.Edit.Blueprints namespace osu.Game.Rulesets.Mania.Edit.Blueprints
{ {
public class ManiaSelectionBlueprint : OverlaySelectionBlueprint public abstract class ManiaSelectionBlueprint : OverlaySelectionBlueprint
{ {
public new DrawableManiaHitObject DrawableObject => (DrawableManiaHitObject)base.DrawableObject; public new DrawableManiaHitObject DrawableObject => (DrawableManiaHitObject)base.DrawableObject;
@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
[Resolved] [Resolved]
private IManiaHitObjectComposer composer { get; set; } private IManiaHitObjectComposer composer { get; set; }
public ManiaSelectionBlueprint(DrawableHitObject drawableObject) protected ManiaSelectionBlueprint(DrawableHitObject drawableObject)
: base(drawableObject) : base(drawableObject)
{ {
RelativeSizeAxes = Axes.None; RelativeSizeAxes = Axes.None;

View File

@ -3,6 +3,7 @@
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Edit.Blueprints.Components; using osu.Game.Rulesets.Mania.Edit.Blueprints.Components;
using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects;
using osuTK.Input; using osuTK.Input;
@ -11,22 +12,25 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
{ {
public class NotePlacementBlueprint : ManiaPlacementBlueprint<Note> public class NotePlacementBlueprint : ManiaPlacementBlueprint<Note>
{ {
private readonly EditNotePiece piece;
public NotePlacementBlueprint() public NotePlacementBlueprint()
: base(new Note()) : base(new Note())
{ {
Origin = Anchor.Centre; RelativeSizeAxes = Axes.Both;
AutoSizeAxes = Axes.Y; InternalChild = piece = new EditNotePiece { Origin = Anchor.Centre };
InternalChild = new EditNotePiece { RelativeSizeAxes = Axes.X };
} }
protected override void Update() public override void UpdatePosition(SnapResult result)
{ {
base.Update(); base.UpdatePosition(result);
Width = SnappedWidth; if (result.Playfield != null)
Position = SnappedMousePosition; {
piece.Width = result.Playfield.DrawWidth;
piece.Position = ToLocalSpace(result.ScreenSpacePosition);
}
} }
protected override bool OnMouseDown(MouseDownEvent e) protected override bool OnMouseDown(MouseDownEvent e)

View File

@ -2,14 +2,11 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Mania.UI;
using osuTK;
namespace osu.Game.Rulesets.Mania.Edit namespace osu.Game.Rulesets.Mania.Edit
{ {
public interface IManiaHitObjectComposer public interface IManiaHitObjectComposer
{ {
Column ColumnAt(Vector2 screenSpacePosition); ManiaPlayfield Playfield { get; }
int TotalColumns { get; }
} }
} }

View File

@ -0,0 +1,213 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Caching;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Screens.Edit;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Mania.Edit
{
/// <summary>
/// A grid which displays coloured beat divisor lines in proximity to the selection or placement cursor.
/// </summary>
public class ManiaBeatSnapGrid : Component
{
private const double visible_range = 750;
/// <summary>
/// The range of time values of the current selection.
/// </summary>
public (double start, double end)? SelectionTimeRange
{
set
{
if (value == selectionTimeRange)
return;
selectionTimeRange = value;
lineCache.Invalidate();
}
}
[Resolved]
private EditorBeatmap beatmap { get; set; }
[Resolved]
private IScrollingInfo scrollingInfo { get; set; }
[Resolved]
private Bindable<WorkingBeatmap> working { get; set; }
[Resolved]
private OsuColour colours { get; set; }
[Resolved]
private BindableBeatDivisor beatDivisor { get; set; }
private readonly List<ScrollingHitObjectContainer> grids = new List<ScrollingHitObjectContainer>();
private readonly Cached lineCache = new Cached();
private (double start, double end)? selectionTimeRange;
[BackgroundDependencyLoader]
private void load(IManiaHitObjectComposer composer)
{
foreach (var stage in composer.Playfield.Stages)
{
foreach (var column in stage.Columns)
{
var lineContainer = new ScrollingHitObjectContainer();
grids.Add(lineContainer);
column.UnderlayElements.Add(lineContainer);
}
}
beatDivisor.BindValueChanged(_ => createLines(), true);
}
protected override void Update()
{
base.Update();
if (!lineCache.IsValid)
{
lineCache.Validate();
createLines();
}
}
private readonly Stack<DrawableGridLine> availableLines = new Stack<DrawableGridLine>();
private void createLines()
{
foreach (var grid in grids)
{
foreach (var line in grid.Objects.OfType<DrawableGridLine>())
availableLines.Push(line);
grid.Clear(false);
}
if (selectionTimeRange == null)
return;
var range = selectionTimeRange.Value;
var timingPoint = beatmap.ControlPointInfo.TimingPointAt(range.start - visible_range);
double time = timingPoint.Time;
int beat = 0;
// progress time until in the visible range.
while (time < range.start - visible_range)
{
time += timingPoint.BeatLength / beatDivisor.Value;
beat++;
}
while (time < range.end + visible_range)
{
var nextTimingPoint = beatmap.ControlPointInfo.TimingPointAt(time);
// switch to the next timing point if we have reached it.
if (nextTimingPoint.Time > timingPoint.Time)
{
beat = 0;
time = nextTimingPoint.Time;
timingPoint = nextTimingPoint;
}
Color4 colour = BindableBeatDivisor.GetColourFor(
BindableBeatDivisor.GetDivisorForBeatIndex(Math.Max(1, beat), beatDivisor.Value), colours);
foreach (var grid in grids)
{
if (!availableLines.TryPop(out var line))
line = new DrawableGridLine();
line.HitObject.StartTime = time;
line.Colour = colour;
grid.Add(line);
}
beat++;
time += timingPoint.BeatLength / beatDivisor.Value;
}
foreach (var grid in grids)
{
// required to update ScrollingHitObjectContainer's cache.
grid.UpdateSubTree();
foreach (var line in grid.Objects.OfType<DrawableGridLine>())
{
time = line.HitObject.StartTime;
if (time >= range.start && time <= range.end)
line.Alpha = 1;
else
{
double timeSeparation = time < range.start ? range.start - time : time - range.end;
line.Alpha = (float)Math.Max(0, 1 - timeSeparation / visible_range);
}
}
}
}
private class DrawableGridLine : DrawableHitObject
{
[Resolved]
private IScrollingInfo scrollingInfo { get; set; }
private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>();
public DrawableGridLine()
: base(new HitObject())
{
RelativeSizeAxes = Axes.X;
Height = 2;
AddInternal(new Box { RelativeSizeAxes = Axes.Both });
}
[BackgroundDependencyLoader]
private void load()
{
direction.BindTo(scrollingInfo.Direction);
direction.BindValueChanged(onDirectionChanged, true);
}
private void onDirectionChanged(ValueChangedEvent<ScrollingDirection> direction)
{
Origin = Anchor = direction.NewValue == ScrollingDirection.Up
? Anchor.TopLeft
: Anchor.BottomLeft;
}
protected override void UpdateInitialTransforms()
{
// don't perform any fading we are handling that ourselves.
}
protected override void UpdateStateTransforms(ArmedState state)
{
LifetimeEnd = HitObject.StartTime + visible_range;
}
}
}
}

View File

@ -6,9 +6,13 @@ using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Input;
using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces;
using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Screens.Edit.Compose.Components; using osu.Game.Screens.Edit.Compose.Components;
@ -20,18 +24,26 @@ namespace osu.Game.Rulesets.Mania.Edit
public class ManiaHitObjectComposer : HitObjectComposer<ManiaHitObject>, IManiaHitObjectComposer public class ManiaHitObjectComposer : HitObjectComposer<ManiaHitObject>, IManiaHitObjectComposer
{ {
private DrawableManiaEditRuleset drawableRuleset; private DrawableManiaEditRuleset drawableRuleset;
private ManiaBeatSnapGrid beatSnapGrid;
private InputManager inputManager;
public ManiaHitObjectComposer(Ruleset ruleset) public ManiaHitObjectComposer(Ruleset ruleset)
: base(ruleset) : base(ruleset)
{ {
} }
/// <summary> [BackgroundDependencyLoader]
/// Retrieves the column that intersects a screen-space position. private void load()
/// </summary> {
/// <param name="screenSpacePosition">The screen-space position.</param> AddInternal(beatSnapGrid = new ManiaBeatSnapGrid());
/// <returns>The column which intersects with <paramref name="screenSpacePosition"/>.</returns> }
public Column ColumnAt(Vector2 screenSpacePosition) => drawableRuleset.GetColumnByPosition(screenSpacePosition);
protected override void LoadComplete()
{
base.LoadComplete();
inputManager = GetContainingInputManager();
}
private DependencyContainer dependencies; private DependencyContainer dependencies;
@ -42,30 +54,31 @@ namespace osu.Game.Rulesets.Mania.Edit
public IScrollingInfo ScrollingInfo => drawableRuleset.ScrollingInfo; public IScrollingInfo ScrollingInfo => drawableRuleset.ScrollingInfo;
public int TotalColumns => Playfield.TotalColumns; protected override Playfield PlayfieldAtScreenSpacePosition(Vector2 screenSpacePosition) =>
Playfield.GetColumnByPosition(screenSpacePosition);
public override (Vector2 position, double time) GetSnappedPosition(Vector2 position, double time) public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition)
{ {
var hoc = Playfield.GetColumn(0).HitObjectContainer; var result = base.SnapScreenSpacePositionToValidTime(screenSpacePosition);
float targetPosition = hoc.ToLocalSpace(ToScreenSpace(position)).Y; switch (ScrollingInfo.Direction.Value)
if (drawableRuleset.ScrollingInfo.Direction.Value == ScrollingDirection.Down)
{ {
// We're dealing with screen coordinates in which the position decreases towards the centre of the screen resulting in an increase in start time. case ScrollingDirection.Down:
// The scrolling algorithm instead assumes a top anchor meaning an increase in time corresponds to an increase in position, result.ScreenSpacePosition -= new Vector2(0, getNoteHeight() / 2);
// so when scrolling downwards the coordinates need to be flipped. break;
targetPosition = hoc.DrawHeight - targetPosition;
case ScrollingDirection.Up:
result.ScreenSpacePosition += new Vector2(0, getNoteHeight() / 2);
break;
} }
double targetTime = drawableRuleset.ScrollingInfo.Algorithm.TimeAt(targetPosition, return result;
EditorClock.CurrentTime,
drawableRuleset.ScrollingInfo.TimeRange.Value,
hoc.DrawHeight);
return base.GetSnappedPosition(position, targetTime);
} }
private float getNoteHeight() =>
Playfield.GetColumn(0).ToScreenSpace(new Vector2(DefaultNotePiece.NOTE_HEIGHT)).Y -
Playfield.GetColumn(0).ToScreenSpace(Vector2.Zero).Y;
protected override DrawableRuleset<ManiaHitObject> CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods = null) protected override DrawableRuleset<ManiaHitObject> CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods = null)
{ {
drawableRuleset = new DrawableManiaEditRuleset(ruleset, beatmap, mods); drawableRuleset = new DrawableManiaEditRuleset(ruleset, beatmap, mods);
@ -83,5 +96,28 @@ namespace osu.Game.Rulesets.Mania.Edit
new NoteCompositionTool(), new NoteCompositionTool(),
new HoldNoteCompositionTool() new HoldNoteCompositionTool()
}; };
protected override void UpdateAfterChildren()
{
base.UpdateAfterChildren();
if (BlueprintContainer.CurrentTool is SelectTool)
{
if (EditorBeatmap.SelectedHitObjects.Any())
{
beatSnapGrid.SelectionTimeRange = (EditorBeatmap.SelectedHitObjects.Min(h => h.StartTime), EditorBeatmap.SelectedHitObjects.Max(h => h.GetEndTime()));
}
else
beatSnapGrid.SelectionTimeRange = null;
}
else
{
var result = SnapScreenSpacePositionToValidTime(inputManager.CurrentState.Mouse.Position);
if (result.Time is double time)
beatSnapGrid.SelectionTimeRange = (time, time);
else
beatSnapGrid.SelectionTimeRange = null;
}
}
} }
} }

View File

@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Mania.Edit
private void performColumnMovement(int lastColumn, MoveSelectionEvent moveEvent) private void performColumnMovement(int lastColumn, MoveSelectionEvent moveEvent)
{ {
var currentColumn = composer.ColumnAt(moveEvent.ScreenSpacePosition); var currentColumn = composer.Playfield.GetColumnByPosition(moveEvent.ScreenSpacePosition);
if (currentColumn == null) if (currentColumn == null)
return; return;
@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Mania.Edit
maxColumn = obj.Column; maxColumn = obj.Column;
} }
columnDelta = Math.Clamp(columnDelta, -minColumn, composer.TotalColumns - 1 - maxColumn); columnDelta = Math.Clamp(columnDelta, -minColumn, composer.Playfield.TotalColumns - 1 - maxColumn);
foreach (var obj in SelectedHitObjects.OfType<ManiaHitObject>()) foreach (var obj in SelectedHitObjects.OfType<ManiaHitObject>())
obj.Column += columnDelta; obj.Column += columnDelta;

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
@ -91,11 +92,11 @@ namespace osu.Game.Rulesets.Mania.Objects
tickSpacing = timingPoint.BeatLength / difficulty.SliderTickRate; tickSpacing = timingPoint.BeatLength / difficulty.SliderTickRate;
} }
protected override void CreateNestedHitObjects() protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
{ {
base.CreateNestedHitObjects(); base.CreateNestedHitObjects(cancellationToken);
createTicks(); createTicks(cancellationToken);
AddNested(Head = new Note AddNested(Head = new Note
{ {
@ -112,13 +113,15 @@ namespace osu.Game.Rulesets.Mania.Objects
}); });
} }
private void createTicks() private void createTicks(CancellationToken cancellationToken)
{ {
if (tickSpacing == 0) if (tickSpacing == 0)
return; return;
for (double t = StartTime + tickSpacing; t <= EndTime - tickSpacing; t += tickSpacing) for (double t = StartTime + tickSpacing; t <= EndTime - tickSpacing; t += tickSpacing)
{ {
cancellationToken.ThrowIfCancellationRequested();
AddNested(new HoldNoteTick AddNested(new HoldNoteTick
{ {
StartTime = t, StartTime = t,

View File

@ -37,6 +37,8 @@ namespace osu.Game.Rulesets.Mania.UI
internal readonly Container TopLevelContainer; internal readonly Container TopLevelContainer;
public Container UnderlayElements => hitObjectArea.UnderlayElements;
public Column(int index) public Column(int index)
{ {
Index = index; Index = index;

View File

@ -12,6 +12,9 @@ namespace osu.Game.Rulesets.Mania.UI.Components
public class ColumnHitObjectArea : HitObjectArea public class ColumnHitObjectArea : HitObjectArea
{ {
public readonly Container Explosions; public readonly Container Explosions;
public readonly Container UnderlayElements;
private readonly Drawable hitTarget; private readonly Drawable hitTarget;
public ColumnHitObjectArea(int columnIndex, HitObjectContainer hitObjectContainer) public ColumnHitObjectArea(int columnIndex, HitObjectContainer hitObjectContainer)
@ -19,6 +22,11 @@ namespace osu.Game.Rulesets.Mania.UI.Components
{ {
AddRangeInternal(new[] AddRangeInternal(new[]
{ {
UnderlayElements = new Container
{
RelativeSizeAxes = Axes.Both,
Depth = 2,
},
hitTarget = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HitTarget, columnIndex), _ => new DefaultHitTarget()) hitTarget = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HitTarget, columnIndex), _ => new DefaultHitTarget())
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,

View File

@ -23,7 +23,6 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Rulesets.UI.Scrolling;
using osuTK;
namespace osu.Game.Rulesets.Mania.UI namespace osu.Game.Rulesets.Mania.UI
{ {
@ -108,13 +107,6 @@ namespace osu.Game.Rulesets.Mania.UI
private void updateTimeRange() => TimeRange.Value = configTimeRange.Value * speedAdjustmentTrack.AggregateTempo.Value * speedAdjustmentTrack.AggregateFrequency.Value; private void updateTimeRange() => TimeRange.Value = configTimeRange.Value * speedAdjustmentTrack.AggregateTempo.Value * speedAdjustmentTrack.AggregateFrequency.Value;
/// <summary>
/// Retrieves the column that intersects a screen-space position.
/// </summary>
/// <param name="screenSpacePosition">The screen-space position.</param>
/// <returns>The column which intersects with <paramref name="screenSpacePosition"/>.</returns>
public Column GetColumnByPosition(Vector2 screenSpacePosition) => Playfield.GetColumnByPosition(screenSpacePosition);
public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new ManiaPlayfieldAdjustmentContainer(); public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new ManiaPlayfieldAdjustmentContainer();
protected override Playfield CreatePlayfield() => new ManiaPlayfield(Beatmap.Stages); protected override Playfield CreatePlayfield() => new ManiaPlayfield(Beatmap.Stages);

View File

@ -18,6 +18,8 @@ namespace osu.Game.Rulesets.Mania.UI
[Cached] [Cached]
public class ManiaPlayfield : ScrollingPlayfield public class ManiaPlayfield : ScrollingPlayfield
{ {
public IReadOnlyList<Stage> Stages => stages;
private readonly List<Stage> stages = new List<Stage>(); private readonly List<Stage> stages = new List<Stage>();
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => stages.Any(s => s.ReceivePositionalInputAt(screenSpacePos)); public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => stages.Any(s => s.ReceivePositionalInputAt(screenSpacePos));

View File

@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Osu.Tests
[Cached] [Cached]
private readonly BindableBeatDivisor beatDivisor = new BindableBeatDivisor(); private readonly BindableBeatDivisor beatDivisor = new BindableBeatDivisor();
[Cached(typeof(IDistanceSnapProvider))] [Cached(typeof(IPositionSnapProvider))]
private readonly SnapProvider snapProvider = new SnapProvider(); private readonly SnapProvider snapProvider = new SnapProvider();
private TestOsuDistanceSnapGrid grid; private TestOsuDistanceSnapGrid grid;
@ -172,9 +172,9 @@ namespace osu.Game.Rulesets.Osu.Tests
} }
} }
private class SnapProvider : IDistanceSnapProvider private class SnapProvider : IPositionSnapProvider
{ {
public (Vector2 position, double time) GetSnappedPosition(Vector2 position, double time) => (position, time); public SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) => new SnapResult(screenSpacePosition, 0);
public float GetBeatSnapDistanceAt(double referenceTime) => (float)beat_length; public float GetBeatSnapDistanceAt(double referenceTime) => (float)beat_length;

View File

@ -5,7 +5,6 @@ using osu.Framework.Input.Events;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components; using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osuTK;
using osuTK.Input; using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles
@ -40,6 +39,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles
return base.OnMouseDown(e); return base.OnMouseDown(e);
} }
public override void UpdatePosition(Vector2 screenSpacePosition) => HitObject.Position = ToLocalSpace(screenSpacePosition); public override void UpdatePosition(SnapResult result)
{
base.UpdatePosition(result);
HitObject.Position = ToLocalSpace(result.ScreenSpacePosition);
}
} }
} }

View File

@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
private IEditorChangeHandler changeHandler { get; set; } private IEditorChangeHandler changeHandler { get; set; }
[Resolved(CanBeNull = true)] [Resolved(CanBeNull = true)]
private IDistanceSnapProvider snapProvider { get; set; } private IPositionSnapProvider snapProvider { get; set; }
[Resolved] [Resolved]
private OsuColour colours { get; set; } private OsuColour colours { get; set; }
@ -162,11 +162,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
if (ControlPoint == slider.Path.ControlPoints[0]) if (ControlPoint == slider.Path.ControlPoints[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
(Vector2 snappedPosition, double snappedTime) = snapProvider?.GetSnappedPosition(e.MousePosition, slider.StartTime) ?? (e.MousePosition, slider.StartTime); var result = snapProvider?.SnapScreenSpacePositionToValidTime(e.ScreenSpaceMousePosition);
Vector2 movementDelta = snappedPosition - slider.Position;
Vector2 movementDelta = Parent.ToLocalSpace(result?.ScreenSpacePosition ?? e.ScreenSpaceMousePosition) - slider.Position;
slider.Position += movementDelta; slider.Position += movementDelta;
slider.StartTime = snappedTime; slider.StartTime = result?.Time ?? slider.StartTime;
// 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 < slider.Path.ControlPoints.Count; i++) for (int i = 1; i < slider.Path.ControlPoints.Count; i++)

View File

@ -67,13 +67,15 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
inputManager = GetContainingInputManager(); inputManager = GetContainingInputManager();
} }
public override void UpdatePosition(Vector2 screenSpacePosition) public override void UpdatePosition(SnapResult result)
{ {
base.UpdatePosition(result);
switch (state) switch (state)
{ {
case PlacementState.Initial: case PlacementState.Initial:
BeginPlacement(); BeginPlacement();
HitObject.Position = ToLocalSpace(screenSpacePosition); HitObject.Position = ToLocalSpace(result.ScreenSpacePosition);
break; break;
case PlacementState.Body: case PlacementState.Body:

View File

@ -190,7 +190,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
new OsuMenuItem("Add control point", MenuItemType.Standard, () => addControlPoint(rightClickPosition)), new OsuMenuItem("Add control point", MenuItemType.Standard, () => addControlPoint(rightClickPosition)),
}; };
public override Vector2 SelectionPoint => ((DrawableSlider)DrawableObject).HeadCircle.ScreenSpaceDrawQuad.Centre; public override Vector2 ScreenSpaceSelectionPoint => ((DrawableSlider)DrawableObject).HeadCircle.ScreenSpaceDrawQuad.Centre;
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => BodyPiece.ReceivePositionalInputAt(screenSpacePos); public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => BodyPiece.ReceivePositionalInputAt(screenSpacePos);

View File

@ -8,7 +8,6 @@ using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners.Components; using osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners.Components;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Osu.UI;
using osuTK;
using osuTK.Input; using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners
@ -60,9 +59,5 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners
return true; return true;
} }
public override void UpdatePosition(Vector2 screenSpacePosition)
{
}
} }
} }

View File

@ -4,6 +4,10 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Caching;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Edit.Tools;
@ -12,6 +16,7 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Screens.Edit.Compose.Components; using osu.Game.Screens.Edit.Compose.Components;
using osuTK;
namespace osu.Game.Rulesets.Osu.Edit namespace osu.Game.Rulesets.Osu.Edit
{ {
@ -32,9 +37,80 @@ namespace osu.Game.Rulesets.Osu.Edit
new SpinnerCompositionTool() new SpinnerCompositionTool()
}; };
[BackgroundDependencyLoader]
private void load()
{
LayerBelowRuleset.Add(distanceSnapGridContainer = new Container { RelativeSizeAxes = Axes.Both });
EditorBeatmap.SelectedHitObjects.CollectionChanged += (_, __) => updateDistanceSnapGrid();
EditorBeatmap.PlacementObject.ValueChanged += _ => updateDistanceSnapGrid();
}
protected override ComposeBlueprintContainer CreateBlueprintContainer() => new OsuBlueprintContainer(HitObjects); protected override ComposeBlueprintContainer CreateBlueprintContainer() => new OsuBlueprintContainer(HitObjects);
protected override DistanceSnapGrid CreateDistanceSnapGrid(IEnumerable<HitObject> selectedHitObjects) private DistanceSnapGrid distanceSnapGrid;
private Container distanceSnapGridContainer;
private readonly Cached distanceSnapGridCache = new Cached();
private double? lastDistanceSnapGridTime;
protected override void Update()
{
base.Update();
if (!(BlueprintContainer.CurrentTool is SelectTool))
{
if (EditorClock.CurrentTime != lastDistanceSnapGridTime)
{
distanceSnapGridCache.Invalidate();
lastDistanceSnapGridTime = EditorClock.CurrentTime;
}
if (!distanceSnapGridCache.IsValid)
updateDistanceSnapGrid();
}
}
public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition)
{
if (distanceSnapGrid == null)
return base.SnapScreenSpacePositionToValidTime(screenSpacePosition);
(Vector2 pos, double time) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(screenSpacePosition));
return new SnapResult(distanceSnapGrid.ToScreenSpace(pos), time, PlayfieldAtScreenSpacePosition(screenSpacePosition));
}
private void updateDistanceSnapGrid()
{
distanceSnapGridContainer.Clear();
distanceSnapGridCache.Invalidate();
switch (BlueprintContainer.CurrentTool)
{
case SelectTool _:
if (!EditorBeatmap.SelectedHitObjects.Any())
return;
distanceSnapGrid = createDistanceSnapGrid(EditorBeatmap.SelectedHitObjects);
break;
default:
if (!CursorInPlacementArea)
return;
distanceSnapGrid = createDistanceSnapGrid(Enumerable.Empty<HitObject>());
break;
}
if (distanceSnapGrid != null)
{
distanceSnapGridContainer.Add(distanceSnapGrid);
distanceSnapGridCache.Validate();
}
}
private DistanceSnapGrid createDistanceSnapGrid(IEnumerable<HitObject> selectedHitObjects)
{ {
if (BlueprintContainer.CurrentTool is SpinnerCompositionTool) if (BlueprintContainer.CurrentTool is SpinnerCompositionTool)
return null; return null;
@ -42,7 +118,8 @@ namespace osu.Game.Rulesets.Osu.Edit
var objects = selectedHitObjects.ToList(); var objects = selectedHitObjects.ToList();
if (objects.Count == 0) if (objects.Count == 0)
return createGrid(h => h.StartTime <= EditorClock.CurrentTime); // use accurate time value to give more instantaneous feedback to the user.
return createGrid(h => h.StartTime <= EditorClock.CurrentTimeAccurate);
double minTime = objects.Min(h => h.StartTime); double minTime = objects.Min(h => h.StartTime);
return createGrid(h => h.StartTime < minTime, objects.Count + 1); return createGrid(h => h.StartTime < minTime, objects.Count + 1);

View File

@ -6,6 +6,7 @@ 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 System.Threading;
using osu.Framework.Caching; using osu.Framework.Caching;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
@ -133,12 +134,12 @@ namespace osu.Game.Rulesets.Osu.Objects
TickDistance = scoringDistance / difficulty.SliderTickRate * TickDistanceMultiplier; TickDistance = scoringDistance / difficulty.SliderTickRate * TickDistanceMultiplier;
} }
protected override void CreateNestedHitObjects() protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
{ {
base.CreateNestedHitObjects(); base.CreateNestedHitObjects(cancellationToken);
foreach (var e in foreach (var e in
SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset)) SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset, cancellationToken))
{ {
switch (e.Type) switch (e.Type)
{ {

View File

@ -4,6 +4,7 @@
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
@ -73,17 +74,17 @@ namespace osu.Game.Rulesets.Taiko.Objects
overallDifficulty = difficulty.OverallDifficulty; overallDifficulty = difficulty.OverallDifficulty;
} }
protected override void CreateNestedHitObjects() protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
{ {
createTicks(); createTicks(cancellationToken);
RequiredGoodHits = NestedHitObjects.Count * Math.Min(0.15, 0.05 + 0.10 / 6 * overallDifficulty); RequiredGoodHits = NestedHitObjects.Count * Math.Min(0.15, 0.05 + 0.10 / 6 * overallDifficulty);
RequiredGreatHits = NestedHitObjects.Count * Math.Min(0.30, 0.10 + 0.20 / 6 * overallDifficulty); RequiredGreatHits = NestedHitObjects.Count * Math.Min(0.30, 0.10 + 0.20 / 6 * overallDifficulty);
base.CreateNestedHitObjects(); base.CreateNestedHitObjects(cancellationToken);
} }
private void createTicks() private void createTicks(CancellationToken cancellationToken)
{ {
if (tickSpacing == 0) if (tickSpacing == 0)
return; return;
@ -92,6 +93,8 @@ namespace osu.Game.Rulesets.Taiko.Objects
for (double t = StartTime; t < EndTime + tickSpacing / 2; t += tickSpacing) for (double t = StartTime; t < EndTime + tickSpacing / 2; t += tickSpacing)
{ {
cancellationToken.ThrowIfCancellationRequested();
AddNested(new DrumRollTick AddNested(new DrumRollTick
{ {
FirstTick = first, FirstTick = first,

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Threading;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
@ -29,13 +30,16 @@ namespace osu.Game.Rulesets.Taiko.Objects
set => throw new NotSupportedException($"{nameof(Swell)} cannot be a strong hitobject."); set => throw new NotSupportedException($"{nameof(Swell)} cannot be a strong hitobject.");
} }
protected override void CreateNestedHitObjects() protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
{ {
base.CreateNestedHitObjects(); base.CreateNestedHitObjects(cancellationToken);
for (int i = 0; i < RequiredHits; i++) for (int i = 0; i < RequiredHits; i++)
{
cancellationToken.ThrowIfCancellationRequested();
AddNested(new SwellTick()); AddNested(new SwellTick());
} }
}
public override Judgement CreateJudgement() => new TaikoSwellJudgement(); public override Judgement CreateJudgement() => new TaikoSwellJudgement();

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.Threading;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
@ -32,9 +33,9 @@ namespace osu.Game.Rulesets.Taiko.Objects
/// </summary> /// </summary>
public virtual bool IsStrong { get; set; } public virtual bool IsStrong { get; set; }
protected override void CreateNestedHitObjects() protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
{ {
base.CreateNestedHitObjects(); base.CreateNestedHitObjects(cancellationToken);
if (IsStrong) if (IsStrong)
AddNested(new StrongHitObject { StartTime = this.GetEndTime() }); AddNested(new StrongHitObject { StartTime = this.GetEndTime() });

View File

@ -16,7 +16,7 @@ namespace osu.Game.Tests.Beatmaps
[Test] [Test]
public void TestSingleSpan() public void TestSingleSpan()
{ {
var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 1, null).ToArray(); var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 1, null, default).ToArray();
Assert.That(events[0].Type, Is.EqualTo(SliderEventType.Head)); Assert.That(events[0].Type, Is.EqualTo(SliderEventType.Head));
Assert.That(events[0].Time, Is.EqualTo(start_time)); Assert.That(events[0].Time, Is.EqualTo(start_time));
@ -31,7 +31,7 @@ namespace osu.Game.Tests.Beatmaps
[Test] [Test]
public void TestRepeat() public void TestRepeat()
{ {
var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 2, null).ToArray(); var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 2, null, default).ToArray();
Assert.That(events[0].Type, Is.EqualTo(SliderEventType.Head)); Assert.That(events[0].Type, Is.EqualTo(SliderEventType.Head));
Assert.That(events[0].Time, Is.EqualTo(start_time)); Assert.That(events[0].Time, Is.EqualTo(start_time));
@ -52,7 +52,7 @@ namespace osu.Game.Tests.Beatmaps
[Test] [Test]
public void TestNonEvenTicks() public void TestNonEvenTicks()
{ {
var events = SliderEventGenerator.Generate(start_time, span_duration, 1, 300, span_duration, 2, null).ToArray(); var events = SliderEventGenerator.Generate(start_time, span_duration, 1, 300, span_duration, 2, null, default).ToArray();
Assert.That(events[0].Type, Is.EqualTo(SliderEventType.Head)); Assert.That(events[0].Type, Is.EqualTo(SliderEventType.Head));
Assert.That(events[0].Time, Is.EqualTo(start_time)); Assert.That(events[0].Time, Is.EqualTo(start_time));
@ -85,7 +85,7 @@ namespace osu.Game.Tests.Beatmaps
[Test] [Test]
public void TestLegacyLastTickOffset() public void TestLegacyLastTickOffset()
{ {
var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 1, 100).ToArray(); var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 1, 100, default).ToArray();
Assert.That(events[2].Type, Is.EqualTo(SliderEventType.LegacyLastTick)); Assert.That(events[2].Type, Is.EqualTo(SliderEventType.LegacyLastTick));
Assert.That(events[2].Time, Is.EqualTo(900)); Assert.That(events[2].Time, Is.EqualTo(900));
@ -97,7 +97,7 @@ namespace osu.Game.Tests.Beatmaps
const double velocity = 5; const double velocity = 5;
const double min_distance = velocity * 10; const double min_distance = velocity * 10;
var events = SliderEventGenerator.Generate(start_time, span_duration, velocity, velocity, span_duration, 2, 0).ToArray(); var events = SliderEventGenerator.Generate(start_time, span_duration, velocity, velocity, span_duration, 2, 0, default).ToArray();
Assert.Multiple(() => Assert.Multiple(() =>
{ {

View File

@ -68,7 +68,7 @@ namespace osu.Game.Tests.Visual.Background
/// Check if <see cref="PlayerLoader"/> properly triggers the visual settings preview when a user hovers over the visual settings panel. /// Check if <see cref="PlayerLoader"/> properly triggers the visual settings preview when a user hovers over the visual settings panel.
/// </summary> /// </summary>
[Test] [Test]
public void PlayerLoaderSettingsHoverTest() public void TestPlayerLoaderSettingsHover()
{ {
setupUserSettings(); setupUserSettings();
AddStep("Start player loader", () => songSelect.Push(playerLoader = new TestPlayerLoader(player = new LoadBlockingTestPlayer { BlockLoad = true }))); AddStep("Start player loader", () => songSelect.Push(playerLoader = new TestPlayerLoader(player = new LoadBlockingTestPlayer { BlockLoad = true })));
@ -79,11 +79,9 @@ namespace osu.Game.Tests.Visual.Background
InputManager.MoveMouseTo(playerLoader.ScreenPos); InputManager.MoveMouseTo(playerLoader.ScreenPos);
InputManager.MoveMouseTo(playerLoader.VisualSettingsPos); InputManager.MoveMouseTo(playerLoader.VisualSettingsPos);
}); });
waitForDim(); AddUntilStep("Screen is dimmed and blur applied", () => songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied());
AddAssert("Screen is dimmed and blur applied", () => songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied());
AddStep("Stop background preview", () => InputManager.MoveMouseTo(playerLoader.ScreenPos)); AddStep("Stop background preview", () => InputManager.MoveMouseTo(playerLoader.ScreenPos));
waitForDim(); AddUntilStep("Screen is undimmed and user blur removed", () => songSelect.IsBackgroundUndimmed() && playerLoader.IsBlurCorrect());
AddAssert("Screen is undimmed and user blur removed", () => songSelect.IsBackgroundUndimmed() && playerLoader.IsBlurCorrect());
} }
/// <summary> /// <summary>
@ -92,20 +90,19 @@ namespace osu.Game.Tests.Visual.Background
/// We need to check that in this scenario, the dim and blur is still properly applied after entering player. /// We need to check that in this scenario, the dim and blur is still properly applied after entering player.
/// </summary> /// </summary>
[Test] [Test]
public void PlayerLoaderTransitionTest() public void TestPlayerLoaderTransition()
{ {
performFullSetup(); performFullSetup();
AddStep("Trigger hover event", () => playerLoader.TriggerOnHover()); AddStep("Trigger hover event", () => playerLoader.TriggerOnHover());
AddAssert("Background retained from song select", () => songSelect.IsBackgroundCurrent()); AddAssert("Background retained from song select", () => songSelect.IsBackgroundCurrent());
waitForDim(); AddUntilStep("Screen is dimmed and blur applied", () => songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied());
AddAssert("Screen is dimmed and blur applied", () => songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied());
} }
/// <summary> /// <summary>
/// Make sure the background is fully invisible (Alpha == 0) when the background should be disabled by the storyboard. /// Make sure the background is fully invisible (Alpha == 0) when the background should be disabled by the storyboard.
/// </summary> /// </summary>
[Test] [Test]
public void StoryboardBackgroundVisibilityTest() public void TestStoryboardBackgroundVisibility()
{ {
performFullSetup(); performFullSetup();
createFakeStoryboard(); createFakeStoryboard();
@ -114,52 +111,46 @@ namespace osu.Game.Tests.Visual.Background
player.ReplacesBackground.Value = true; player.ReplacesBackground.Value = true;
player.StoryboardEnabled.Value = true; player.StoryboardEnabled.Value = true;
}); });
waitForDim(); AddUntilStep("Background is invisible, storyboard is visible", () => songSelect.IsBackgroundInvisible() && player.IsStoryboardVisible);
AddAssert("Background is invisible, storyboard is visible", () => songSelect.IsBackgroundInvisible() && player.IsStoryboardVisible);
AddStep("Disable Storyboard", () => AddStep("Disable Storyboard", () =>
{ {
player.ReplacesBackground.Value = false; player.ReplacesBackground.Value = false;
player.StoryboardEnabled.Value = false; player.StoryboardEnabled.Value = false;
}); });
waitForDim(); AddUntilStep("Background is visible, storyboard is invisible", () => songSelect.IsBackgroundVisible() && !player.IsStoryboardVisible);
AddAssert("Background is visible, storyboard is invisible", () => songSelect.IsBackgroundVisible() && !player.IsStoryboardVisible);
} }
/// <summary> /// <summary>
/// When exiting player, the screen that it suspends/exits to needs to have a fully visible (Alpha == 1) background. /// When exiting player, the screen that it suspends/exits to needs to have a fully visible (Alpha == 1) background.
/// </summary> /// </summary>
[Test] [Test]
public void StoryboardTransitionTest() public void TestStoryboardTransition()
{ {
performFullSetup(); performFullSetup();
createFakeStoryboard(); createFakeStoryboard();
AddStep("Exit to song select", () => player.Exit()); AddStep("Exit to song select", () => player.Exit());
waitForDim(); AddUntilStep("Background is visible", () => songSelect.IsBackgroundVisible());
AddAssert("Background is visible", () => songSelect.IsBackgroundVisible());
} }
/// <summary> /// <summary>
/// Ensure <see cref="UserDimContainer"/> is properly accepting user-defined visual changes for a background. /// Ensure <see cref="UserDimContainer"/> is properly accepting user-defined visual changes for a background.
/// </summary> /// </summary>
[Test] [Test]
public void DisableUserDimBackgroundTest() public void TestDisableUserDimBackground()
{ {
performFullSetup(); performFullSetup();
waitForDim(); AddUntilStep("Screen is dimmed and blur applied", () => songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied());
AddAssert("Screen is dimmed and blur applied", () => songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied());
AddStep("Enable user dim", () => songSelect.DimEnabled.Value = false); AddStep("Enable user dim", () => songSelect.DimEnabled.Value = false);
waitForDim(); AddUntilStep("Screen is undimmed and user blur removed", () => songSelect.IsBackgroundUndimmed() && songSelect.IsUserBlurDisabled());
AddAssert("Screen is undimmed and user blur removed", () => songSelect.IsBackgroundUndimmed() && songSelect.IsUserBlurDisabled());
AddStep("Disable user dim", () => songSelect.DimEnabled.Value = true); AddStep("Disable user dim", () => songSelect.DimEnabled.Value = true);
waitForDim(); AddUntilStep("Screen is dimmed and blur applied", () => songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied());
AddAssert("Screen is dimmed and blur applied", () => songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied());
} }
/// <summary> /// <summary>
/// Ensure <see cref="UserDimContainer"/> is properly accepting user-defined visual changes for a storyboard. /// Ensure <see cref="UserDimContainer"/> is properly accepting user-defined visual changes for a storyboard.
/// </summary> /// </summary>
[Test] [Test]
public void DisableUserDimStoryboardTest() public void TestDisableUserDimStoryboard()
{ {
performFullSetup(); performFullSetup();
createFakeStoryboard(); createFakeStoryboard();
@ -170,41 +161,36 @@ namespace osu.Game.Tests.Visual.Background
}); });
AddStep("Enable user dim", () => player.DimmableStoryboard.EnableUserDim.Value = true); AddStep("Enable user dim", () => player.DimmableStoryboard.EnableUserDim.Value = true);
AddStep("Set dim level to 1", () => songSelect.DimLevel.Value = 1f); AddStep("Set dim level to 1", () => songSelect.DimLevel.Value = 1f);
waitForDim(); AddUntilStep("Storyboard is invisible", () => !player.IsStoryboardVisible);
AddAssert("Storyboard is invisible", () => !player.IsStoryboardVisible);
AddStep("Disable user dim", () => player.DimmableStoryboard.EnableUserDim.Value = false); AddStep("Disable user dim", () => player.DimmableStoryboard.EnableUserDim.Value = false);
waitForDim(); AddUntilStep("Storyboard is visible", () => player.IsStoryboardVisible);
AddAssert("Storyboard is visible", () => player.IsStoryboardVisible);
} }
/// <summary> /// <summary>
/// Check if the visual settings container retains dim and blur when pausing /// Check if the visual settings container retains dim and blur when pausing
/// </summary> /// </summary>
[Test] [Test]
public void PauseTest() public void TestPause()
{ {
performFullSetup(true); performFullSetup(true);
AddStep("Pause", () => player.Pause()); AddStep("Pause", () => player.Pause());
waitForDim(); AddUntilStep("Screen is dimmed and blur applied", () => songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied());
AddAssert("Screen is dimmed and blur applied", () => songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied());
AddStep("Unpause", () => player.Resume()); AddStep("Unpause", () => player.Resume());
waitForDim(); AddUntilStep("Screen is dimmed and blur applied", () => songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied());
AddAssert("Screen is dimmed and blur applied", () => songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied());
} }
/// <summary> /// <summary>
/// Check if the visual settings container removes user dim when suspending <see cref="Player"/> for <see cref="ResultsScreen"/> /// Check if the visual settings container removes user dim when suspending <see cref="Player"/> for <see cref="ResultsScreen"/>
/// </summary> /// </summary>
[Test] [Test]
public void TransitionTest() public void TestTransition()
{ {
performFullSetup(); performFullSetup();
FadeAccessibleResults results = null; FadeAccessibleResults results = null;
AddStep("Transition to Results", () => player.Push(results = AddStep("Transition to Results", () => player.Push(results =
new FadeAccessibleResults(new ScoreInfo { User = new User { Username = "osu!" } }))); new FadeAccessibleResults(new ScoreInfo { User = new User { Username = "osu!" } })));
AddUntilStep("Wait for results is current", () => results.IsCurrentScreen()); AddUntilStep("Wait for results is current", () => results.IsCurrentScreen());
waitForDim(); AddUntilStep("Screen is undimmed, original background retained", () =>
AddAssert("Screen is undimmed, original background retained", () =>
songSelect.IsBackgroundUndimmed() && songSelect.IsBackgroundCurrent() && results.IsBlurCorrect()); songSelect.IsBackgroundUndimmed() && songSelect.IsBackgroundCurrent() && results.IsBlurCorrect());
} }
@ -212,32 +198,27 @@ namespace osu.Game.Tests.Visual.Background
/// Check if background gets undimmed and unblurred when leaving <see cref="Player"/> for <see cref="PlaySongSelect"/> /// Check if background gets undimmed and unblurred when leaving <see cref="Player"/> for <see cref="PlaySongSelect"/>
/// </summary> /// </summary>
[Test] [Test]
public void TransitionOutTest() public void TestTransitionOut()
{ {
performFullSetup(); performFullSetup();
AddStep("Exit to song select", () => player.Exit()); AddStep("Exit to song select", () => player.Exit());
waitForDim(); AddUntilStep("Screen is undimmed and user blur removed", () => songSelect.IsBackgroundUndimmed() && songSelect.IsBlurCorrect());
AddAssert("Screen is undimmed and user blur removed", () => songSelect.IsBackgroundUndimmed() && songSelect.IsBlurCorrect());
} }
/// <summary> /// <summary>
/// Check if hovering on the visual settings dialogue after resuming from player still previews the background dim. /// Check if hovering on the visual settings dialogue after resuming from player still previews the background dim.
/// </summary> /// </summary>
[Test] [Test]
public void ResumeFromPlayerTest() public void TestResumeFromPlayer()
{ {
performFullSetup(); performFullSetup();
AddStep("Move mouse to Visual Settings", () => InputManager.MoveMouseTo(playerLoader.VisualSettingsPos)); AddStep("Move mouse to Visual Settings", () => InputManager.MoveMouseTo(playerLoader.VisualSettingsPos));
AddStep("Resume PlayerLoader", () => player.Restart()); AddStep("Resume PlayerLoader", () => player.Restart());
waitForDim(); AddUntilStep("Screen is dimmed and blur applied", () => songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied());
AddAssert("Screen is dimmed and blur applied", () => songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied());
AddStep("Move mouse to center of screen", () => InputManager.MoveMouseTo(playerLoader.ScreenPos)); AddStep("Move mouse to center of screen", () => InputManager.MoveMouseTo(playerLoader.ScreenPos));
waitForDim(); AddUntilStep("Screen is undimmed and user blur removed", () => songSelect.IsBackgroundUndimmed() && playerLoader.IsBlurCorrect());
AddAssert("Screen is undimmed and user blur removed", () => songSelect.IsBackgroundUndimmed() && playerLoader.IsBlurCorrect());
} }
private void waitForDim() => AddWaitStep("Wait for dim", 5);
private void createFakeStoryboard() => AddStep("Create storyboard", () => private void createFakeStoryboard() => AddStep("Create storyboard", () =>
{ {
player.StoryboardEnabled.Value = false; player.StoryboardEnabled.Value = false;

View File

@ -23,7 +23,7 @@ namespace osu.Game.Tests.Visual.Editing
[Cached(typeof(EditorBeatmap))] [Cached(typeof(EditorBeatmap))]
private readonly EditorBeatmap editorBeatmap; private readonly EditorBeatmap editorBeatmap;
[Cached(typeof(IDistanceSnapProvider))] [Cached(typeof(IPositionSnapProvider))]
private readonly SnapProvider snapProvider = new SnapProvider(); private readonly SnapProvider snapProvider = new SnapProvider();
public TestSceneDistanceSnapGrid() public TestSceneDistanceSnapGrid()
@ -151,9 +151,9 @@ namespace osu.Game.Tests.Visual.Editing
=> (Vector2.Zero, 0); => (Vector2.Zero, 0);
} }
private class SnapProvider : IDistanceSnapProvider private class SnapProvider : IPositionSnapProvider
{ {
public (Vector2 position, double time) GetSnappedPosition(Vector2 position, double time) => (position, time); public SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) => new SnapResult(screenSpacePosition, 0);
public float GetBeatSnapDistanceAt(double referenceTime) => 10; public float GetBeatSnapDistanceAt(double referenceTime) => 10;

View File

@ -4,8 +4,8 @@
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Timing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Components; using osu.Game.Screens.Edit.Components;
using osuTK; using osuTK;
@ -17,9 +17,8 @@ namespace osu.Game.Tests.Visual.Editing
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
var clock = new DecoupleableInterpolatingFramedClock { IsCoupled = false }; var clock = new EditorClock { IsCoupled = false };
Dependencies.CacheAs<IAdjustableClock>(clock); Dependencies.CacheAs(clock);
Dependencies.CacheAs<IFrameBasedClock>(clock);
var playback = new PlaybackControl var playback = new PlaybackControl
{ {

View File

@ -7,7 +7,6 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Timing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
@ -69,7 +68,7 @@ namespace osu.Game.Tests.Visual.Editing
private IBindable<WorkingBeatmap> beatmap { get; set; } private IBindable<WorkingBeatmap> beatmap { get; set; }
[Resolved] [Resolved]
private IAdjustableClock adjustableClock { get; set; } private EditorClock editorClock { get; set; }
public AudioVisualiser() public AudioVisualiser()
{ {
@ -96,13 +95,15 @@ namespace osu.Game.Tests.Visual.Editing
base.Update(); base.Update();
if (beatmap.Value.Track.IsLoaded) if (beatmap.Value.Track.IsLoaded)
marker.X = (float)(adjustableClock.CurrentTime / beatmap.Value.Track.Length); marker.X = (float)(editorClock.CurrentTime / beatmap.Value.Track.Length);
} }
} }
private class StartStopButton : OsuButton private class StartStopButton : OsuButton
{ {
private IAdjustableClock adjustableClock; [Resolved]
private EditorClock editorClock { get; set; }
private bool started; private bool started;
public StartStopButton() public StartStopButton()
@ -114,22 +115,16 @@ namespace osu.Game.Tests.Visual.Editing
Action = onClick; Action = onClick;
} }
[BackgroundDependencyLoader]
private void load(IAdjustableClock adjustableClock)
{
this.adjustableClock = adjustableClock;
}
private void onClick() private void onClick()
{ {
if (started) if (started)
{ {
adjustableClock.Stop(); editorClock.Stop();
Text = "Start"; Text = "Start";
} }
else else
{ {
adjustableClock.Start(); editorClock.Start();
Text = "Stop"; Text = "Stop";
} }

View File

@ -27,14 +27,13 @@ using osu.Game.Online.API.Requests;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using Decoder = osu.Game.Beatmaps.Formats.Decoder; using Decoder = osu.Game.Beatmaps.Formats.Decoder;
using ZipArchive = SharpCompress.Archives.Zip.ZipArchive;
namespace osu.Game.Beatmaps namespace osu.Game.Beatmaps
{ {
/// <summary> /// <summary>
/// Handles the storage and retrieval of Beatmaps/WorkingBeatmaps. /// Handles the storage and retrieval of Beatmaps/WorkingBeatmaps.
/// </summary> /// </summary>
public partial class BeatmapManager : DownloadableArchiveModelManager<BeatmapSetInfo, BeatmapSetFileInfo> public partial class BeatmapManager : DownloadableArchiveModelManager<BeatmapSetInfo, BeatmapSetFileInfo>, IDisposable
{ {
/// <summary> /// <summary>
/// Fired when a single difficulty has been hidden. /// Fired when a single difficulty has been hidden.
@ -66,7 +65,6 @@ namespace osu.Game.Beatmaps
private readonly AudioManager audioManager; private readonly AudioManager audioManager;
private readonly GameHost host; private readonly GameHost host;
private readonly BeatmapOnlineLookupQueue onlineLookupQueue; private readonly BeatmapOnlineLookupQueue onlineLookupQueue;
private readonly Storage exportStorage;
public BeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, AudioManager audioManager, GameHost host = null, public BeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, AudioManager audioManager, GameHost host = null,
WorkingBeatmap defaultBeatmap = null) WorkingBeatmap defaultBeatmap = null)
@ -83,7 +81,6 @@ namespace osu.Game.Beatmaps
beatmaps.BeatmapRestored += b => beatmapRestored.Value = new WeakReference<BeatmapInfo>(b); beatmaps.BeatmapRestored += b => beatmapRestored.Value = new WeakReference<BeatmapInfo>(b);
onlineLookupQueue = new BeatmapOnlineLookupQueue(api, storage); onlineLookupQueue = new BeatmapOnlineLookupQueue(api, storage);
exportStorage = storage.GetStorageForDirectory("exports");
} }
protected override ArchiveDownloadRequest<BeatmapSetInfo> CreateDownloadRequest(BeatmapSetInfo set, bool minimiseDownloadSize) => protected override ArchiveDownloadRequest<BeatmapSetInfo> CreateDownloadRequest(BeatmapSetInfo set, bool minimiseDownloadSize) =>
@ -214,26 +211,6 @@ namespace osu.Game.Beatmaps
workingCache.Remove(working); workingCache.Remove(working);
} }
/// <summary>
/// Exports a <see cref="BeatmapSetInfo"/> to an .osz package.
/// </summary>
/// <param name="set">The <see cref="BeatmapSetInfo"/> to export.</param>
public void Export(BeatmapSetInfo set)
{
var localSet = QueryBeatmapSet(s => s.ID == set.ID);
using (var archive = ZipArchive.Create())
{
foreach (var file in localSet.Files)
archive.AddEntry(file.Filename, Files.Storage.GetStream(file.FileInfo.StoragePath));
using (var outputStream = exportStorage.GetStream($"{set}.osz", FileAccess.Write, FileMode.Create))
archive.SaveTo(outputStream);
exportStorage.OpenInNativeExplorer();
}
}
private readonly WeakList<WorkingBeatmap> workingCache = new WeakList<WorkingBeatmap>(); private readonly WeakList<WorkingBeatmap> workingCache = new WeakList<WorkingBeatmap>();
/// <summary> /// <summary>
@ -433,6 +410,11 @@ namespace osu.Game.Beatmaps
return endTime - startTime; return endTime - startTime;
} }
public void Dispose()
{
onlineLookupQueue?.Dispose();
}
/// <summary> /// <summary>
/// A dummy WorkingBeatmap for the purpose of retrieving a beatmap for star difficulty calculation. /// A dummy WorkingBeatmap for the purpose of retrieving a beatmap for star difficulty calculation.
/// </summary> /// </summary>

View File

@ -23,7 +23,7 @@ namespace osu.Game.Beatmaps
{ {
public partial class BeatmapManager public partial class BeatmapManager
{ {
private class BeatmapOnlineLookupQueue private class BeatmapOnlineLookupQueue : IDisposable
{ {
private readonly IAPIProvider api; private readonly IAPIProvider api;
private readonly Storage storage; private readonly Storage storage;
@ -180,6 +180,11 @@ namespace osu.Game.Beatmaps
return false; return false;
} }
public void Dispose()
{
cacheDownloadRequest?.Dispose();
}
[Serializable] [Serializable]
[SuppressMessage("ReSharper", "InconsistentNaming")] [SuppressMessage("ReSharper", "InconsistentNaming")]
private class CachedOnlineBeatmapLookup private class CachedOnlineBeatmapLookup

View File

@ -129,12 +129,19 @@ namespace osu.Game.Beatmaps
processor?.PreProcess(); processor?.PreProcess();
// Compute default values for hitobjects, including creating nested hitobjects in-case they're needed // Compute default values for hitobjects, including creating nested hitobjects in-case they're needed
try
{
foreach (var obj in converted.HitObjects) foreach (var obj in converted.HitObjects)
{ {
if (cancellationSource.IsCancellationRequested) if (cancellationSource.IsCancellationRequested)
throw new BeatmapLoadTimeoutException(BeatmapInfo); throw new BeatmapLoadTimeoutException(BeatmapInfo);
obj.ApplyDefaults(converted.ControlPointInfo, converted.BeatmapInfo.BaseDifficulty); obj.ApplyDefaults(converted.ControlPointInfo, converted.BeatmapInfo.BaseDifficulty, cancellationSource.Token);
}
}
catch (OperationCanceledException)
{
throw new BeatmapLoadTimeoutException(BeatmapInfo);
} }
foreach (var mod in mods.OfType<IApplicableToHitObject>()) foreach (var mod in mods.OfType<IApplicableToHitObject>())

View File

@ -22,6 +22,7 @@ using osu.Game.IO.Archives;
using osu.Game.IPC; using osu.Game.IPC;
using osu.Game.Overlays.Notifications; using osu.Game.Overlays.Notifications;
using osu.Game.Utils; using osu.Game.Utils;
using SharpCompress.Archives.Zip;
using SharpCompress.Common; using SharpCompress.Common;
using FileInfo = osu.Game.IO.FileInfo; using FileInfo = osu.Game.IO.FileInfo;
@ -82,6 +83,8 @@ namespace osu.Game.Database
// ReSharper disable once NotAccessedField.Local (we should keep a reference to this so it is not finalised) // ReSharper disable once NotAccessedField.Local (we should keep a reference to this so it is not finalised)
private ArchiveImportIPCChannel ipc; private ArchiveImportIPCChannel ipc;
private readonly Storage exportStorage;
protected ArchiveModelManager(Storage storage, IDatabaseContextFactory contextFactory, MutableDatabaseBackedStoreWithFileIncludes<TModel, TFileModel> modelStore, IIpcHost importHost = null) protected ArchiveModelManager(Storage storage, IDatabaseContextFactory contextFactory, MutableDatabaseBackedStoreWithFileIncludes<TModel, TFileModel> modelStore, IIpcHost importHost = null)
{ {
ContextFactory = contextFactory; ContextFactory = contextFactory;
@ -90,6 +93,8 @@ namespace osu.Game.Database
ModelStore.ItemAdded += item => handleEvent(() => itemAdded.Value = new WeakReference<TModel>(item)); ModelStore.ItemAdded += item => handleEvent(() => itemAdded.Value = new WeakReference<TModel>(item));
ModelStore.ItemRemoved += item => handleEvent(() => itemRemoved.Value = new WeakReference<TModel>(item)); ModelStore.ItemRemoved += item => handleEvent(() => itemRemoved.Value = new WeakReference<TModel>(item));
exportStorage = storage.GetStorageForDirectory("exports");
Files = new FileStore(contextFactory, storage); Files = new FileStore(contextFactory, storage);
if (importHost != null) if (importHost != null)
@ -369,6 +374,29 @@ namespace osu.Game.Database
return item; return item;
}, cancellationToken, TaskCreationOptions.HideScheduler, import_scheduler).Unwrap(); }, cancellationToken, TaskCreationOptions.HideScheduler, import_scheduler).Unwrap();
/// <summary>
/// Exports an item to a legacy (.zip based) package.
/// </summary>
/// <param name="item">The item to export.</param>
public void Export(TModel item)
{
var retrievedItem = ModelStore.ConsumableItems.FirstOrDefault(s => s.ID == item.ID);
if (retrievedItem == null)
throw new ArgumentException("Specified model could not be found", nameof(item));
using (var archive = ZipArchive.Create())
{
foreach (var file in retrievedItem.Files)
archive.AddEntry(file.Filename, Files.Storage.GetStream(file.FileInfo.StoragePath));
using (var outputStream = exportStorage.GetStream($"{getValidFilename(item.ToString())}{HandledExtensions.First()}", FileAccess.Write, FileMode.Create))
archive.SaveTo(outputStream);
exportStorage.OpenInNativeExplorer();
}
}
public void UpdateFile(TModel model, TFileModel file, Stream contents) public void UpdateFile(TModel model, TFileModel file, Stream contents)
{ {
using (var usage = ContextFactory.GetForWrite()) using (var usage = ContextFactory.GetForWrite())
@ -710,5 +738,12 @@ namespace osu.Game.Database
} }
#endregion #endregion
private string getValidFilename(string filename)
{
foreach (char c in Path.GetInvalidFileNameChars())
filename = filename.Replace(c, '_');
return filename;
}
} }
} }

View File

@ -69,7 +69,7 @@ namespace osu.Game.IO
public override void DeleteDatabase(string name) => UnderlyingStorage.DeleteDatabase(MutatePath(name)); public override void DeleteDatabase(string name) => UnderlyingStorage.DeleteDatabase(MutatePath(name));
public override void OpenInNativeExplorer() => UnderlyingStorage.OpenInNativeExplorer(); public override void OpenPathInNativeExplorer(string path) => UnderlyingStorage.OpenPathInNativeExplorer(MutatePath(path));
public override Storage GetStorageForDirectory(string path) public override Storage GetStorageForDirectory(string path)
{ {

View File

@ -337,6 +337,7 @@ namespace osu.Game
{ {
base.Dispose(isDisposing); base.Dispose(isDisposing);
RulesetStore?.Dispose(); RulesetStore?.Dispose();
BeatmapManager?.Dispose();
contextFactory.FlushConnections(); contextFactory.FlushConnections();
} }

View File

@ -7,6 +7,7 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Logging;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Skinning; using osu.Game.Skinning;
@ -38,9 +39,11 @@ namespace osu.Game.Overlays.Settings.Sections
private void load(OsuConfigManager config) private void load(OsuConfigManager config)
{ {
FlowContent.Spacing = new Vector2(0, 5); FlowContent.Spacing = new Vector2(0, 5);
Children = new Drawable[] Children = new Drawable[]
{ {
skinDropdown = new SkinSettingsDropdown(), skinDropdown = new SkinSettingsDropdown(),
new ExportSkinButton(),
new SettingsSlider<float, SizeSlider> new SettingsSlider<float, SizeSlider>
{ {
LabelText = "Menu cursor size", LabelText = "Menu cursor size",
@ -117,5 +120,35 @@ namespace osu.Game.Overlays.Settings.Sections
protected override DropdownMenu CreateMenu() => base.CreateMenu().With(m => m.MaxHeight = 200); protected override DropdownMenu CreateMenu() => base.CreateMenu().With(m => m.MaxHeight = 200);
} }
} }
private class ExportSkinButton : SettingsButton
{
[Resolved]
private SkinManager skins { get; set; }
private Bindable<Skin> currentSkin;
[BackgroundDependencyLoader]
private void load()
{
Text = "Export selected skin";
Action = export;
currentSkin = skins.CurrentSkin.GetBoundCopy();
currentSkin.BindValueChanged(skin => Enabled.Value = skin.NewValue.SkinInfo.ID > 0, true);
}
private void export()
{
try
{
skins.Export(currentSkin.Value.SkinInfo);
}
catch (Exception e)
{
Logger.Log($"Could not export current skin: {e.Message}", level: LogLevel.Error);
}
}
}
} }
} }

View File

@ -3,15 +3,14 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq; using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Input; using osu.Framework.Input;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Framework.Timing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Configuration; using osu.Game.Rulesets.Configuration;
@ -20,6 +19,7 @@ using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Screens.Edit; using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Components.RadioButtons; using osu.Game.Screens.Edit.Components.RadioButtons;
using osu.Game.Screens.Edit.Compose; using osu.Game.Screens.Edit.Compose;
@ -38,23 +38,19 @@ namespace osu.Game.Rulesets.Edit
protected readonly Ruleset Ruleset; protected readonly Ruleset Ruleset;
[Resolved] [Resolved]
protected IFrameBasedClock EditorClock { get; private set; } protected EditorClock EditorClock { get; private set; }
[Resolved] [Resolved]
protected EditorBeatmap EditorBeatmap { get; private set; } protected EditorBeatmap EditorBeatmap { get; private set; }
[Resolved] [Resolved]
private IAdjustableClock adjustableClock { get; set; } protected IBeatSnapProvider BeatSnapProvider { get; private set; }
[Resolved]
private IBeatSnapProvider beatSnapProvider { get; set; }
protected ComposeBlueprintContainer BlueprintContainer { get; private set; } protected ComposeBlueprintContainer BlueprintContainer { get; private set; }
private DrawableEditRulesetWrapper<TObject> drawableRulesetWrapper; private DrawableEditRulesetWrapper<TObject> drawableRulesetWrapper;
private Container distanceSnapGridContainer;
private DistanceSnapGrid distanceSnapGrid; protected readonly Container LayerBelowRuleset = new Container { RelativeSizeAxes = Axes.Both };
private readonly List<Container> layerContainers = new List<Container>();
private InputManager inputManager; private InputManager inputManager;
@ -67,7 +63,7 @@ namespace osu.Game.Rulesets.Edit
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(IFrameBasedClock framedClock) private void load()
{ {
Config = Dependencies.Get<RulesetConfigCache>().GetConfigFor(Ruleset); Config = Dependencies.Get<RulesetConfigCache>().GetConfigFor(Ruleset);
@ -75,7 +71,7 @@ namespace osu.Game.Rulesets.Edit
{ {
drawableRulesetWrapper = new DrawableEditRulesetWrapper<TObject>(CreateDrawableRuleset(Ruleset, EditorBeatmap.PlayableBeatmap)) drawableRulesetWrapper = new DrawableEditRulesetWrapper<TObject>(CreateDrawableRuleset(Ruleset, EditorBeatmap.PlayableBeatmap))
{ {
Clock = framedClock, Clock = EditorClock,
ProcessCustomClock = false ProcessCustomClock = false
}; };
} }
@ -85,17 +81,6 @@ namespace osu.Game.Rulesets.Edit
return; return;
} }
var layerBelowRuleset = drawableRulesetWrapper.CreatePlayfieldAdjustmentContainer().WithChildren(new Drawable[]
{
distanceSnapGridContainer = new Container { RelativeSizeAxes = Axes.Both },
new EditorPlayfieldBorder { RelativeSizeAxes = Axes.Both }
});
var layerAboveRuleset = drawableRulesetWrapper.CreatePlayfieldAdjustmentContainer().WithChild(BlueprintContainer = CreateBlueprintContainer());
layerContainers.Add(layerBelowRuleset);
layerContainers.Add(layerAboveRuleset);
InternalChild = new GridContainer InternalChild = new GridContainer
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
@ -119,9 +104,16 @@ namespace osu.Game.Rulesets.Edit
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Children = new Drawable[] Children = new Drawable[]
{ {
layerBelowRuleset, // layers below playfield
drawableRulesetWrapper.CreatePlayfieldAdjustmentContainer().WithChildren(new Drawable[]
{
LayerBelowRuleset,
new EditorPlayfieldBorder { RelativeSizeAxes = Axes.Both }
}),
drawableRulesetWrapper, drawableRulesetWrapper,
layerAboveRuleset // layers above playfield
drawableRulesetWrapper.CreatePlayfieldAdjustmentContainer()
.WithChild(BlueprintContainer = CreateBlueprintContainer())
} }
} }
}, },
@ -139,7 +131,7 @@ namespace osu.Game.Rulesets.Edit
setSelectTool(); setSelectTool();
BlueprintContainer.SelectionChanged += selectionChanged; EditorBeatmap.SelectedHitObjects.CollectionChanged += selectionChanged;
} }
protected override bool OnKeyDown(KeyDownEvent e) protected override bool OnKeyDown(KeyDownEvent e)
@ -165,42 +157,13 @@ namespace osu.Game.Rulesets.Edit
inputManager = GetContainingInputManager(); inputManager = GetContainingInputManager();
} }
private double lastGridUpdateTime; private void selectionChanged(object sender, NotifyCollectionChangedEventArgs changedArgs)
protected override void Update()
{ {
base.Update(); if (EditorBeatmap.SelectedHitObjects.Any())
if (EditorClock.CurrentTime != lastGridUpdateTime && !(BlueprintContainer.CurrentTool is SelectTool))
showGridFor(Enumerable.Empty<HitObject>());
}
protected override void UpdateAfterChildren()
{
base.UpdateAfterChildren();
layerContainers.ForEach(l =>
{
l.Anchor = drawableRulesetWrapper.Playfield.Anchor;
l.Origin = drawableRulesetWrapper.Playfield.Origin;
l.Position = drawableRulesetWrapper.Playfield.Position;
l.Size = drawableRulesetWrapper.Playfield.Size;
});
}
private void selectionChanged(IEnumerable<HitObject> selectedHitObjects)
{
var hitObjects = selectedHitObjects.ToArray();
if (hitObjects.Any())
{ {
// ensure in selection mode if a selection is made. // ensure in selection mode if a selection is made.
setSelectTool(); setSelectTool();
showGridFor(hitObjects);
} }
else
distanceSnapGridContainer.Hide();
} }
private void setSelectTool() => toolboxCollection.Items.First().Select(); private void setSelectTool() => toolboxCollection.Items.First().Select();
@ -209,30 +172,12 @@ namespace osu.Game.Rulesets.Edit
{ {
BlueprintContainer.CurrentTool = tool; BlueprintContainer.CurrentTool = tool;
if (tool is SelectTool) if (!(tool is SelectTool))
distanceSnapGridContainer.Hide();
else
{
EditorBeatmap.SelectedHitObjects.Clear(); EditorBeatmap.SelectedHitObjects.Clear();
showGridFor(Enumerable.Empty<HitObject>());
}
}
private void showGridFor(IEnumerable<HitObject> selectedHitObjects)
{
distanceSnapGridContainer.Clear();
distanceSnapGrid = CreateDistanceSnapGrid(selectedHitObjects);
if (distanceSnapGrid != null)
{
distanceSnapGridContainer.Child = distanceSnapGrid;
distanceSnapGridContainer.Show();
}
lastGridUpdateTime = EditorClock.CurrentTime;
} }
public override IEnumerable<DrawableHitObject> HitObjects => drawableRulesetWrapper.Playfield.AllHitObjects; public override IEnumerable<DrawableHitObject> HitObjects => drawableRulesetWrapper.Playfield.AllHitObjects;
public override bool CursorInPlacementArea => drawableRulesetWrapper.Playfield.ReceivePositionalInputAt(inputManager.CurrentState.Mouse.Position); public override bool CursorInPlacementArea => drawableRulesetWrapper.Playfield.ReceivePositionalInputAt(inputManager.CurrentState.Mouse.Position);
protected abstract IReadOnlyList<HitObjectCompositionTool> CompositionTools { get; } protected abstract IReadOnlyList<HitObjectCompositionTool> CompositionTools { get; }
@ -244,9 +189,6 @@ namespace osu.Game.Rulesets.Edit
public void BeginPlacement(HitObject hitObject) public void BeginPlacement(HitObject hitObject)
{ {
EditorBeatmap.PlacementObject.Value = hitObject; EditorBeatmap.PlacementObject.Value = hitObject;
if (distanceSnapGrid != null)
hitObject.StartTime = GetSnappedPosition(distanceSnapGrid.ToLocalSpace(inputManager.CurrentState.Mouse.Position), hitObject.StartTime).time;
} }
public void EndPlacement(HitObject hitObject, bool commit) public void EndPlacement(HitObject hitObject, bool commit)
@ -257,49 +199,66 @@ namespace osu.Game.Rulesets.Edit
{ {
EditorBeatmap.Add(hitObject); EditorBeatmap.Add(hitObject);
if (adjustableClock.CurrentTime < hitObject.StartTime) if (EditorClock.CurrentTime < hitObject.StartTime)
adjustableClock.Seek(hitObject.StartTime); EditorClock.SeekTo(hitObject.StartTime);
} }
showGridFor(Enumerable.Empty<HitObject>());
} }
public void Delete(HitObject hitObject) => EditorBeatmap.Remove(hitObject); public void Delete(HitObject hitObject) => EditorBeatmap.Remove(hitObject);
public override (Vector2 position, double time) GetSnappedPosition(Vector2 position, double time) => distanceSnapGrid?.GetSnappedPosition(position) ?? (position, time); protected virtual Playfield PlayfieldAtScreenSpacePosition(Vector2 screenSpacePosition) => drawableRulesetWrapper.Playfield;
public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition)
{
var playfield = PlayfieldAtScreenSpacePosition(screenSpacePosition);
double? targetTime = null;
if (playfield is ScrollingPlayfield scrollingPlayfield)
{
targetTime = scrollingPlayfield.TimeAtScreenSpacePosition(screenSpacePosition);
// apply beat snapping
targetTime = BeatSnapProvider.SnapTime(targetTime.Value);
// convert back to screen space
screenSpacePosition = scrollingPlayfield.ScreenSpacePositionAtTime(targetTime.Value);
}
return new SnapResult(screenSpacePosition, targetTime, playfield);
}
public override float GetBeatSnapDistanceAt(double referenceTime) public override float GetBeatSnapDistanceAt(double referenceTime)
{ {
DifficultyControlPoint difficultyPoint = EditorBeatmap.ControlPointInfo.DifficultyPointAt(referenceTime); DifficultyControlPoint difficultyPoint = EditorBeatmap.ControlPointInfo.DifficultyPointAt(referenceTime);
return (float)(100 * EditorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier / beatSnapProvider.BeatDivisor); return (float)(100 * EditorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier / BeatSnapProvider.BeatDivisor);
} }
public override float DurationToDistance(double referenceTime, double duration) public override float DurationToDistance(double referenceTime, double duration)
{ {
double beatLength = beatSnapProvider.GetBeatLengthAtTime(referenceTime); double beatLength = BeatSnapProvider.GetBeatLengthAtTime(referenceTime);
return (float)(duration / beatLength * GetBeatSnapDistanceAt(referenceTime)); return (float)(duration / beatLength * GetBeatSnapDistanceAt(referenceTime));
} }
public override double DistanceToDuration(double referenceTime, float distance) public override double DistanceToDuration(double referenceTime, float distance)
{ {
double beatLength = beatSnapProvider.GetBeatLengthAtTime(referenceTime); double beatLength = BeatSnapProvider.GetBeatLengthAtTime(referenceTime);
return distance / GetBeatSnapDistanceAt(referenceTime) * beatLength; return distance / GetBeatSnapDistanceAt(referenceTime) * beatLength;
} }
public override double GetSnappedDurationFromDistance(double referenceTime, float distance) public override double GetSnappedDurationFromDistance(double referenceTime, float distance)
=> beatSnapProvider.SnapTime(referenceTime + DistanceToDuration(referenceTime, distance), referenceTime) - referenceTime; => BeatSnapProvider.SnapTime(referenceTime + DistanceToDuration(referenceTime, distance), referenceTime) - referenceTime;
public override float GetSnappedDistanceFromDistance(double referenceTime, float distance) public override float GetSnappedDistanceFromDistance(double referenceTime, float distance)
{ {
var snappedEndTime = beatSnapProvider.SnapTime(referenceTime + DistanceToDuration(referenceTime, distance), referenceTime); var snappedEndTime = BeatSnapProvider.SnapTime(referenceTime + DistanceToDuration(referenceTime, distance), referenceTime);
return DurationToDistance(referenceTime, snappedEndTime - referenceTime); return DurationToDistance(referenceTime, snappedEndTime - referenceTime);
} }
} }
[Cached(typeof(HitObjectComposer))] [Cached(typeof(HitObjectComposer))]
[Cached(typeof(IDistanceSnapProvider))] [Cached(typeof(IPositionSnapProvider))]
public abstract class HitObjectComposer : CompositeDrawable, IDistanceSnapProvider public abstract class HitObjectComposer : CompositeDrawable, IPositionSnapProvider
{ {
internal HitObjectComposer() internal HitObjectComposer()
{ {
@ -316,15 +275,7 @@ namespace osu.Game.Rulesets.Edit
/// </summary> /// </summary>
public abstract bool CursorInPlacementArea { get; } public abstract bool CursorInPlacementArea { get; }
/// <summary> public abstract SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition);
/// Creates the <see cref="DistanceSnapGrid"/> applicable for a <see cref="HitObject"/> selection.
/// </summary>
/// <param name="selectedHitObjects">The <see cref="HitObject"/> selection.</param>
/// <returns>The <see cref="DistanceSnapGrid"/> for <paramref name="selectedHitObjects"/>. If empty, a grid is returned for the current point in time.</returns>
[CanBeNull]
protected virtual DistanceSnapGrid CreateDistanceSnapGrid([NotNull] IEnumerable<HitObject> selectedHitObjects) => null;
public abstract (Vector2 position, double time) GetSnappedPosition(Vector2 position, double time);
public abstract float GetBeatSnapDistanceAt(double referenceTime); public abstract float GetBeatSnapDistanceAt(double referenceTime);

View File

@ -5,9 +5,14 @@ using osuTK;
namespace osu.Game.Rulesets.Edit namespace osu.Game.Rulesets.Edit
{ {
public interface IDistanceSnapProvider public interface IPositionSnapProvider
{ {
(Vector2 position, double time) GetSnappedPosition(Vector2 position, double time); /// <summary>
/// Given a position, find a valid time snap.
/// </summary>
/// <param name="screenSpacePosition">The screen-space position to be snapped.</param>
/// <returns>The time and position post-snapping.</returns>
SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition);
/// <summary> /// <summary>
/// Retrieves the distance between two points within a timing point that are one beat length apart. /// Retrieves the distance between two points within a timing point that are one beat length apart.

View File

@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Edit
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => DrawableObject.ReceivePositionalInputAt(screenSpacePos); public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => DrawableObject.ReceivePositionalInputAt(screenSpacePos);
public override Vector2 SelectionPoint => DrawableObject.ScreenSpaceDrawQuad.Centre; public override Vector2 ScreenSpaceSelectionPoint => DrawableObject.ScreenSpaceDrawQuad.Centre;
public override Quad SelectionQuad => DrawableObject.ScreenSpaceDrawQuad; public override Quad SelectionQuad => DrawableObject.ScreenSpaceDrawQuad;

View File

@ -1,15 +1,16 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Threading;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Timing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Compose; using osu.Game.Screens.Edit.Compose;
using osuTK; using osuTK;
@ -30,10 +31,13 @@ namespace osu.Game.Rulesets.Edit
/// </summary> /// </summary>
protected readonly HitObject HitObject; protected readonly HitObject HitObject;
protected IClock EditorClock { get; private set; } [Resolved(canBeNull: true)]
protected EditorClock EditorClock { get; private set; }
private readonly IBindable<WorkingBeatmap> beatmap = new Bindable<WorkingBeatmap>(); private readonly IBindable<WorkingBeatmap> beatmap = new Bindable<WorkingBeatmap>();
private Bindable<double> startTimeBindable;
[Resolved] [Resolved]
private IPlacementHandler placementHandler { get; set; } private IPlacementHandler placementHandler { get; set; }
@ -49,23 +53,20 @@ namespace osu.Game.Rulesets.Edit
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(IBindable<WorkingBeatmap> beatmap, IAdjustableClock clock) private void load(IBindable<WorkingBeatmap> beatmap)
{ {
this.beatmap.BindTo(beatmap); this.beatmap.BindTo(beatmap);
EditorClock = clock; startTimeBindable = HitObject.StartTimeBindable.GetBoundCopy();
startTimeBindable.BindValueChanged(_ => ApplyDefaultsToHitObject(), true);
ApplyDefaultsToHitObject();
} }
/// <summary> /// <summary>
/// Signals that the placement of <see cref="HitObject"/> has started. /// Signals that the placement of <see cref="HitObject"/> has started.
/// </summary> /// </summary>
/// <param name="startTime">The start time of <see cref="HitObject"/> at the placement point. If null, the current clock time is used.</param>
/// <param name="commitStart">Whether this call is committing a value for HitObject.StartTime and continuing with further adjustments.</param> /// <param name="commitStart">Whether this call is committing a value for HitObject.StartTime and continuing with further adjustments.</param>
protected void BeginPlacement(double? startTime = null, bool commitStart = false) protected void BeginPlacement(bool commitStart = false)
{ {
HitObject.StartTime = startTime ?? EditorClock.CurrentTime;
placementHandler.BeginPlacement(HitObject); placementHandler.BeginPlacement(HitObject);
PlacementActive |= commitStart; PlacementActive |= commitStart;
} }
@ -86,11 +87,15 @@ namespace osu.Game.Rulesets.Edit
/// <summary> /// <summary>
/// Updates the position of this <see cref="PlacementBlueprint"/> to a new screen-space position. /// Updates the position of this <see cref="PlacementBlueprint"/> to a new screen-space position.
/// </summary> /// </summary>
/// <param name="screenSpacePosition">The screen-space position.</param> /// <param name="snapResult">The snap result information.</param>
public abstract void UpdatePosition(Vector2 screenSpacePosition); public virtual void UpdatePosition(SnapResult snapResult)
{
if (!PlacementActive)
HitObject.StartTime = snapResult.Time ?? EditorClock?.CurrentTime ?? Time.Current;
}
/// <summary> /// <summary>
/// Invokes <see cref="Objects.HitObject.ApplyDefaults(ControlPointInfo,BeatmapDifficulty)"/>, /// Invokes <see cref="Objects.HitObject.ApplyDefaults(ControlPointInfo,BeatmapDifficulty, CancellationToken)"/>,
/// refreshing <see cref="Objects.HitObject.NestedHitObjects"/> and parameters for the <see cref="HitObject"/>. /// refreshing <see cref="Objects.HitObject.NestedHitObjects"/> and parameters for the <see cref="HitObject"/>.
/// </summary> /// </summary>
protected void ApplyDefaultsToHitObject() => HitObject.ApplyDefaults(beatmap.Value.Beatmap.ControlPointInfo, beatmap.Value.Beatmap.BeatmapInfo.BaseDifficulty); protected void ApplyDefaultsToHitObject() => HitObject.ApplyDefaults(beatmap.Value.Beatmap.ControlPointInfo, beatmap.Value.Beatmap.BeatmapInfo.BaseDifficulty);

View File

@ -116,7 +116,7 @@ namespace osu.Game.Rulesets.Edit
/// <summary> /// <summary>
/// The screen-space point that causes this <see cref="OverlaySelectionBlueprint"/> to be selected. /// The screen-space point that causes this <see cref="OverlaySelectionBlueprint"/> to be selected.
/// </summary> /// </summary>
public virtual Vector2 SelectionPoint => ScreenSpaceDrawQuad.Centre; public virtual Vector2 ScreenSpaceSelectionPoint => ScreenSpaceDrawQuad.Centre;
/// <summary> /// <summary>
/// The screen-space quad that outlines this <see cref="OverlaySelectionBlueprint"/> for selections. /// The screen-space quad that outlines this <see cref="OverlaySelectionBlueprint"/> for selections.

View File

@ -0,0 +1,33 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.UI;
using osuTK;
namespace osu.Game.Rulesets.Edit
{
/// <summary>
/// The result of a position/time snapping process.
/// </summary>
public class SnapResult
{
/// <summary>
/// The screen space position, potentially altered for snapping.
/// </summary>
public Vector2 ScreenSpacePosition;
/// <summary>
/// The resultant time for snapping, if a value could be attained.
/// </summary>
public double? Time;
public readonly Playfield Playfield;
public SnapResult(Vector2 screenSpacePosition, double? time, Playfield playfield = null)
{
ScreenSpacePosition = screenSpacePosition;
Time = time;
Playfield = playfield;
}
}
}

View File

@ -257,7 +257,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
} }
} }
if (state.Value != ArmedState.Idle && LifetimeEnd == double.MaxValue || HitObject.HitWindows == null) if (LifetimeEnd == double.MaxValue && (state.Value != ArmedState.Idle || HitObject.HitWindows == null))
Expire(); Expire();
// apply any custom state overrides // apply any custom state overrides

View File

@ -4,6 +4,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading;
using JetBrains.Annotations; using JetBrains.Annotations;
using Newtonsoft.Json; using Newtonsoft.Json;
using osu.Framework.Bindables; using osu.Framework.Bindables;
@ -99,7 +100,8 @@ namespace osu.Game.Rulesets.Objects
/// </summary> /// </summary>
/// <param name="controlPointInfo">The control points.</param> /// <param name="controlPointInfo">The control points.</param>
/// <param name="difficulty">The difficulty settings to use.</param> /// <param name="difficulty">The difficulty settings to use.</param>
public void ApplyDefaults(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) /// <param name="cancellationToken">The cancellation token.</param>
public void ApplyDefaults(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty, CancellationToken cancellationToken = default)
{ {
ApplyDefaultsToSelf(controlPointInfo, difficulty); ApplyDefaultsToSelf(controlPointInfo, difficulty);
@ -108,7 +110,7 @@ namespace osu.Game.Rulesets.Objects
nestedHitObjects.Clear(); nestedHitObjects.Clear();
CreateNestedHitObjects(); CreateNestedHitObjects(cancellationToken);
if (this is IHasComboInformation hasCombo) if (this is IHasComboInformation hasCombo)
{ {
@ -122,7 +124,7 @@ namespace osu.Game.Rulesets.Objects
nestedHitObjects.Sort((h1, h2) => h1.StartTime.CompareTo(h2.StartTime)); nestedHitObjects.Sort((h1, h2) => h1.StartTime.CompareTo(h2.StartTime));
foreach (var h in nestedHitObjects) foreach (var h in nestedHitObjects)
h.ApplyDefaults(controlPointInfo, difficulty); h.ApplyDefaults(controlPointInfo, difficulty, cancellationToken);
DefaultsApplied?.Invoke(this); DefaultsApplied?.Invoke(this);
} }
@ -136,6 +138,14 @@ namespace osu.Game.Rulesets.Objects
HitWindows?.SetDifficulty(difficulty.OverallDifficulty); HitWindows?.SetDifficulty(difficulty.OverallDifficulty);
} }
protected virtual void CreateNestedHitObjects(CancellationToken cancellationToken)
{
// ReSharper disable once MethodSupportsCancellation (https://youtrack.jetbrains.com/issue/RIDER-44520)
#pragma warning disable 618
CreateNestedHitObjects();
#pragma warning restore 618
}
protected virtual void CreateNestedHitObjects() protected virtual void CreateNestedHitObjects()
{ {
} }

View File

@ -4,13 +4,22 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading;
namespace osu.Game.Rulesets.Objects namespace osu.Game.Rulesets.Objects
{ {
public static class SliderEventGenerator public static class SliderEventGenerator
{ {
[Obsolete("Use the overload with cancellation support instead.")] // can be removed 20201115
public static IEnumerable<SliderEventDescriptor> Generate(double startTime, double spanDuration, double velocity, double tickDistance, double totalDistance, int spanCount, public static IEnumerable<SliderEventDescriptor> Generate(double startTime, double spanDuration, double velocity, double tickDistance, double totalDistance, int spanCount,
double? legacyLastTickOffset) double? legacyLastTickOffset)
{
return Generate(startTime, spanDuration, velocity, tickDistance, totalDistance, spanCount, legacyLastTickOffset, default);
}
// ReSharper disable once MethodOverloadWithOptionalParameter
public static IEnumerable<SliderEventDescriptor> Generate(double startTime, double spanDuration, double velocity, double tickDistance, double totalDistance, int spanCount,
double? legacyLastTickOffset, CancellationToken cancellationToken = default)
{ {
// A very lenient maximum length of a slider for ticks to be generated. // A very lenient maximum length of a slider for ticks to be generated.
// This exists for edge cases such as /b/1573664 where the beatmap has been edited by the user, and should never be reached in normal usage. // This exists for edge cases such as /b/1573664 where the beatmap has been edited by the user, and should never be reached in normal usage.
@ -37,7 +46,7 @@ namespace osu.Game.Rulesets.Objects
var spanStartTime = startTime + span * spanDuration; var spanStartTime = startTime + span * spanDuration;
var reversed = span % 2 == 1; var reversed = span % 2 == 1;
var ticks = generateTicks(span, spanStartTime, spanDuration, reversed, length, tickDistance, minDistanceFromEnd); var ticks = generateTicks(span, spanStartTime, spanDuration, reversed, length, tickDistance, minDistanceFromEnd, cancellationToken);
if (reversed) if (reversed)
{ {
@ -108,12 +117,15 @@ namespace osu.Game.Rulesets.Objects
/// <param name="length">The length of the path.</param> /// <param name="length">The length of the path.</param>
/// <param name="tickDistance">The distance between each tick.</param> /// <param name="tickDistance">The distance between each tick.</param>
/// <param name="minDistanceFromEnd">The distance from the end of the path at which ticks are not allowed to be added.</param> /// <param name="minDistanceFromEnd">The distance from the end of the path at which ticks are not allowed to be added.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A <see cref="SliderEventDescriptor"/> for each tick. If <paramref name="reversed"/> is true, the ticks will be returned in reverse-StartTime order.</returns> /// <returns>A <see cref="SliderEventDescriptor"/> for each tick. If <paramref name="reversed"/> is true, the ticks will be returned in reverse-StartTime order.</returns>
private static IEnumerable<SliderEventDescriptor> generateTicks(int spanIndex, double spanStartTime, double spanDuration, bool reversed, double length, double tickDistance, private static IEnumerable<SliderEventDescriptor> generateTicks(int spanIndex, double spanStartTime, double spanDuration, bool reversed, double length, double tickDistance,
double minDistanceFromEnd) double minDistanceFromEnd, CancellationToken cancellationToken = default)
{ {
for (var d = tickDistance; d <= length; d += tickDistance) for (var d = tickDistance; d <= length; d += tickDistance)
{ {
cancellationToken.ThrowIfCancellationRequested();
if (d >= length - minDistanceFromEnd) if (d >= length - minDistanceFromEnd)
break; break;

View File

@ -9,6 +9,7 @@ using osu.Framework.Graphics;
using osu.Framework.Layout; using osu.Framework.Layout;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osuTK;
namespace osu.Game.Rulesets.UI.Scrolling namespace osu.Game.Rulesets.UI.Scrolling
{ {
@ -78,6 +79,98 @@ namespace osu.Game.Rulesets.UI.Scrolling
hitObjectInitialStateCache.Clear(); hitObjectInitialStateCache.Clear();
} }
/// <summary>
/// Given a position in screen space, return the time within this column.
/// </summary>
public double TimeAtScreenSpacePosition(Vector2 screenSpacePosition)
{
// convert to local space of column so we can snap and fetch correct location.
Vector2 localPosition = ToLocalSpace(screenSpacePosition);
float position = 0;
switch (scrollingInfo.Direction.Value)
{
case ScrollingDirection.Up:
case ScrollingDirection.Down:
position = localPosition.Y;
break;
case ScrollingDirection.Right:
case ScrollingDirection.Left:
position = localPosition.X;
break;
}
flipPositionIfRequired(ref position);
return scrollingInfo.Algorithm.TimeAt(position, Time.Current, scrollingInfo.TimeRange.Value, getLength());
}
/// <summary>
/// Given a time, return the screen space position within this column.
/// </summary>
public Vector2 ScreenSpacePositionAtTime(double time)
{
var pos = scrollingInfo.Algorithm.PositionAt(time, Time.Current, scrollingInfo.TimeRange.Value, getLength());
flipPositionIfRequired(ref pos);
switch (scrollingInfo.Direction.Value)
{
case ScrollingDirection.Up:
case ScrollingDirection.Down:
return ToScreenSpace(new Vector2(getBreadth() / 2, pos));
default:
return ToScreenSpace(new Vector2(pos, getBreadth() / 2));
}
}
private float getLength()
{
switch (scrollingInfo.Direction.Value)
{
case ScrollingDirection.Left:
case ScrollingDirection.Right:
return DrawWidth;
default:
return DrawHeight;
}
}
private float getBreadth()
{
switch (scrollingInfo.Direction.Value)
{
case ScrollingDirection.Up:
case ScrollingDirection.Down:
return DrawWidth;
default:
return DrawHeight;
}
}
private void flipPositionIfRequired(ref float position)
{
// We're dealing with screen coordinates in which the position decreases towards the centre of the screen resulting in an increase in start time.
// The scrolling algorithm instead assumes a top anchor meaning an increase in time corresponds to an increase in position,
// so when scrolling downwards the coordinates need to be flipped.
switch (scrollingInfo.Direction.Value)
{
case ScrollingDirection.Down:
position = DrawHeight - position;
break;
case ScrollingDirection.Right:
position = DrawWidth - position;
break;
}
}
private void onDefaultsApplied(DrawableHitObject drawableObject) private void onDefaultsApplied(DrawableHitObject drawableObject)
{ {
// The cache may not exist if the hitobject state hasn't been computed yet (e.g. if the hitobject was added + defaults applied in the same frame). // The cache may not exist if the hitobject state hasn't been computed yet (e.g. if the hitobject was added + defaults applied in the same frame).

View File

@ -4,6 +4,7 @@
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osuTK;
namespace osu.Game.Rulesets.UI.Scrolling namespace osu.Game.Rulesets.UI.Scrolling
{ {
@ -15,14 +16,26 @@ namespace osu.Game.Rulesets.UI.Scrolling
protected readonly IBindable<ScrollingDirection> Direction = new Bindable<ScrollingDirection>(); protected readonly IBindable<ScrollingDirection> Direction = new Bindable<ScrollingDirection>();
[Resolved] [Resolved]
private IScrollingInfo scrollingInfo { get; set; } protected IScrollingInfo ScrollingInfo { get; private set; }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
Direction.BindTo(scrollingInfo.Direction); Direction.BindTo(ScrollingInfo.Direction);
} }
/// <summary>
/// Given a position in screen space, return the time within this column.
/// </summary>
public virtual double TimeAtScreenSpacePosition(Vector2 screenSpacePosition) =>
((ScrollingHitObjectContainer)HitObjectContainer).TimeAtScreenSpacePosition(screenSpacePosition);
/// <summary>
/// Given a time, return the screen space position within this column.
/// </summary>
public virtual Vector2 ScreenSpacePositionAtTime(double time)
=> ((ScrollingHitObjectContainer)HitObjectContainer).ScreenSpacePositionAtTime(time);
protected sealed override HitObjectContainer CreateHitObjectContainer() => new ScrollingHitObjectContainer(); protected sealed override HitObjectContainer CreateHitObjectContainer() => new ScrollingHitObjectContainer();
} }
} }

View File

@ -13,7 +13,6 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Timing;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
@ -26,7 +25,7 @@ namespace osu.Game.Screens.Edit.Components
private IconButton playButton; private IconButton playButton;
[Resolved] [Resolved]
private IAdjustableClock adjustableClock { get; set; } private EditorClock editorClock { get; set; }
private readonly BindableNumber<double> tempo = new BindableDouble(1); private readonly BindableNumber<double> tempo = new BindableDouble(1);
@ -87,17 +86,17 @@ namespace osu.Game.Screens.Edit.Components
private void togglePause() private void togglePause()
{ {
if (adjustableClock.IsRunning) if (editorClock.IsRunning)
adjustableClock.Stop(); editorClock.Stop();
else else
adjustableClock.Start(); editorClock.Start();
} }
protected override void Update() protected override void Update()
{ {
base.Update(); base.Update();
playButton.Icon = adjustableClock.IsRunning ? FontAwesome.Regular.PauseCircle : FontAwesome.Regular.PlayCircle; playButton.Icon = editorClock.IsRunning ? FontAwesome.Regular.PauseCircle : FontAwesome.Regular.PlayCircle;
} }
private class PlaybackTabControl : OsuTabControl<double> private class PlaybackTabControl : OsuTabControl<double>

View File

@ -5,7 +5,6 @@ using osu.Framework.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using System; using System;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Timing;
using osu.Game.Graphics; using osu.Game.Graphics;
namespace osu.Game.Screens.Edit.Components namespace osu.Game.Screens.Edit.Components
@ -15,7 +14,7 @@ namespace osu.Game.Screens.Edit.Components
private readonly OsuSpriteText trackTimer; private readonly OsuSpriteText trackTimer;
[Resolved] [Resolved]
private IAdjustableClock adjustableClock { get; set; } private EditorClock editorClock { get; set; }
public TimeInfoContainer() public TimeInfoContainer()
{ {
@ -35,7 +34,7 @@ namespace osu.Game.Screens.Edit.Components
{ {
base.Update(); base.Update();
trackTimer.Text = TimeSpan.FromMilliseconds(adjustableClock.CurrentTime).ToString(@"mm\:ss\:fff"); trackTimer.Text = TimeSpan.FromMilliseconds(editorClock.CurrentTime).ToString(@"mm\:ss\:fff");
} }
} }
} }

View File

@ -9,7 +9,6 @@ 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.Framework.Threading; using osu.Framework.Threading;
using osu.Framework.Timing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Graphics; using osu.Game.Graphics;
@ -20,14 +19,14 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
/// </summary> /// </summary>
public class MarkerPart : TimelinePart public class MarkerPart : TimelinePart
{ {
private readonly Drawable marker; private Drawable marker;
private readonly IAdjustableClock adjustableClock; [Resolved]
private EditorClock editorClock { get; set; }
public MarkerPart(IAdjustableClock adjustableClock) [BackgroundDependencyLoader]
private void load()
{ {
this.adjustableClock = adjustableClock;
Add(marker = new MarkerVisualisation()); Add(marker = new MarkerVisualisation());
} }
@ -59,14 +58,14 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
return; return;
float markerPos = Math.Clamp(ToLocalSpace(screenPosition).X, 0, DrawWidth); float markerPos = Math.Clamp(ToLocalSpace(screenPosition).X, 0, DrawWidth);
adjustableClock.Seek(markerPos / DrawWidth * Beatmap.Value.Track.Length); editorClock.SeekTo(markerPos / DrawWidth * editorClock.TrackLength);
}); });
} }
protected override void Update() protected override void Update()
{ {
base.Update(); base.Update();
marker.X = (float)adjustableClock.CurrentTime; marker.X = (float)editorClock.CurrentTime;
} }
protected override void LoadBeatmap(WorkingBeatmap beatmap) protected override void LoadBeatmap(WorkingBeatmap beatmap)

View File

@ -6,7 +6,6 @@ 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;
using osu.Framework.Timing;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Screens.Edit.Components.Timelines.Summary.Parts; using osu.Game.Screens.Edit.Components.Timelines.Summary.Parts;
@ -18,11 +17,11 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary
public class SummaryTimeline : BottomBarContainer public class SummaryTimeline : BottomBarContainer
{ {
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colours, IAdjustableClock adjustableClock) private void load(OsuColour colours)
{ {
Children = new Drawable[] Children = new Drawable[]
{ {
new MarkerPart(adjustableClock) { RelativeSizeAxes = Axes.Both }, new MarkerPart { RelativeSizeAxes = Axes.Both },
new ControlPointPart new ControlPointPart
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,

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 System; using System;
using System.Collections.Generic;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
@ -14,7 +13,6 @@ using osu.Framework.Graphics.Primitives;
using osu.Framework.Input; 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.Framework.Timing;
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.Drawables; using osu.Game.Rulesets.Objects.Drawables;
@ -29,8 +27,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// </summary> /// </summary>
public abstract class BlueprintContainer : CompositeDrawable, IKeyBindingHandler<PlatformAction> public abstract class BlueprintContainer : CompositeDrawable, IKeyBindingHandler<PlatformAction>
{ {
public event Action<IEnumerable<HitObject>> SelectionChanged;
protected DragBox DragBox { get; private set; } protected DragBox DragBox { get; private set; }
protected Container<SelectionBlueprint> SelectionBlueprints { get; private set; } protected Container<SelectionBlueprint> SelectionBlueprints { get; private set; }
@ -41,7 +37,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
private IEditorChangeHandler changeHandler { get; set; } private IEditorChangeHandler changeHandler { get; set; }
[Resolved] [Resolved]
private IAdjustableClock adjustableClock { get; set; } private EditorClock editorClock { get; set; }
[Resolved] [Resolved]
private EditorBeatmap beatmap { get; set; } private EditorBeatmap beatmap { get; set; }
@ -49,7 +45,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
private readonly BindableList<HitObject> selectedHitObjects = new BindableList<HitObject>(); private readonly BindableList<HitObject> selectedHitObjects = new BindableList<HitObject>();
[Resolved(canBeNull: true)] [Resolved(canBeNull: true)]
private IDistanceSnapProvider snapProvider { get; set; } private IPositionSnapProvider snapProvider { get; set; }
protected BlueprintContainer() protected BlueprintContainer()
{ {
@ -88,8 +84,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
SelectionBlueprints.FirstOrDefault(b => b.HitObject == o)?.Deselect(); SelectionBlueprints.FirstOrDefault(b => b.HitObject == o)?.Deselect();
break; break;
} }
SelectionChanged?.Invoke(selectedHitObjects);
}; };
} }
@ -149,7 +143,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
if (clickedBlueprint == null) if (clickedBlueprint == null)
return false; return false;
adjustableClock?.Seek(clickedBlueprint.HitObject.StartTime); editorClock?.SeekTo(clickedBlueprint.HitObject.StartTime);
return true; return true;
} }
@ -326,7 +320,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
{ {
foreach (var blueprint in SelectionBlueprints) foreach (var blueprint in SelectionBlueprints)
{ {
if (blueprint.IsAlive && blueprint.IsPresent && rect.Contains(blueprint.SelectionPoint)) if (blueprint.IsAlive && blueprint.IsPresent && rect.Contains(blueprint.ScreenSpaceSelectionPoint))
blueprint.Select(); blueprint.Select();
else else
blueprint.Deselect(); blueprint.Deselect();
@ -384,7 +378,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
// Movement is tracked from the blueprint of the earliest hitobject, since it only makes sense to distance snap from that hitobject // Movement is tracked from the blueprint of the earliest hitobject, since it only makes sense to distance snap from that hitobject
movementBlueprint = selectionHandler.SelectedBlueprints.OrderBy(b => b.HitObject.StartTime).First(); movementBlueprint = selectionHandler.SelectedBlueprints.OrderBy(b => b.HitObject.StartTime).First();
movementBlueprintOriginalPosition = movementBlueprint.SelectionPoint; // todo: unsure if correct movementBlueprintOriginalPosition = movementBlueprint.ScreenSpaceSelectionPoint; // todo: unsure if correct
} }
/// <summary> /// <summary>
@ -405,16 +399,19 @@ namespace osu.Game.Screens.Edit.Compose.Components
Vector2 movePosition = movementBlueprintOriginalPosition.Value + e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition; Vector2 movePosition = movementBlueprintOriginalPosition.Value + e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition;
// Retrieve a snapped position. // Retrieve a snapped position.
(Vector2 snappedPosition, double snappedTime) = snapProvider.GetSnappedPosition(ToLocalSpace(movePosition), draggedObject.StartTime); var result = snapProvider.SnapScreenSpacePositionToValidTime(movePosition);
// Move the hitobjects. // Move the hitobjects.
if (!selectionHandler.HandleMovement(new MoveSelectionEvent(movementBlueprint, ToScreenSpace(snappedPosition)))) if (!selectionHandler.HandleMovement(new MoveSelectionEvent(movementBlueprint, result.ScreenSpacePosition)))
return true; return true;
if (result.Time.HasValue)
{
// Apply the start time at the newly snapped-to position // Apply the start time at the newly snapped-to position
double offset = snappedTime - draggedObject.StartTime; double offset = result.Time.Value - draggedObject.StartTime;
foreach (HitObject obj in selectionHandler.SelectedHitObjects) foreach (HitObject obj in selectionHandler.SelectedHitObjects)
obj.StartTime += offset; obj.StartTime += offset;
}
return true; return true;
} }

View File

@ -11,7 +11,6 @@ using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osuTK;
namespace osu.Game.Screens.Edit.Compose.Components namespace osu.Game.Screens.Edit.Compose.Components
{ {
@ -65,12 +64,11 @@ namespace osu.Game.Screens.Edit.Compose.Components
createPlacement(); createPlacement();
} }
private void updatePlacementPosition(Vector2 screenSpacePosition) private void updatePlacementPosition()
{ {
Vector2 snappedGridPosition = composer.GetSnappedPosition(ToLocalSpace(screenSpacePosition), 0).position; var snapResult = composer.SnapScreenSpacePositionToValidTime(inputManager.CurrentState.Mouse.Position);
Vector2 snappedScreenSpacePosition = ToScreenSpace(snappedGridPosition);
currentPlacement.UpdatePosition(snappedScreenSpacePosition); currentPlacement.UpdatePosition(snapResult);
} }
#endregion #endregion
@ -85,7 +83,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
removePlacement(); removePlacement();
if (currentPlacement != null) if (currentPlacement != null)
updatePlacementPosition(inputManager.CurrentState.Mouse.Position); updatePlacementPosition();
} }
protected sealed override SelectionBlueprint CreateBlueprintFor(HitObject hitObject) protected sealed override SelectionBlueprint CreateBlueprintFor(HitObject hitObject)
@ -117,7 +115,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
placementBlueprintContainer.Child = currentPlacement = blueprint; placementBlueprintContainer.Child = currentPlacement = blueprint;
// Fixes a 1-frame position discrepancy due to the first mouse move event happening in the next frame // Fixes a 1-frame position discrepancy due to the first mouse move event happening in the next frame
updatePlacementPosition(inputManager.CurrentState.Mouse.Position); updatePlacementPosition();
} }
} }

View File

@ -43,7 +43,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
protected OsuColour Colours { get; private set; } protected OsuColour Colours { get; private set; }
[Resolved] [Resolved]
protected IDistanceSnapProvider SnapProvider { get; private set; } protected IPositionSnapProvider SnapProvider { get; private set; }
[Resolved] [Resolved]
private EditorBeatmap beatmap { get; set; } private EditorBeatmap beatmap { get; set; }

View File

@ -244,14 +244,21 @@ namespace osu.Game.Screens.Edit.Compose.Components
#region Context Menu #region Context Menu
public virtual MenuItem[] ContextMenuItems public MenuItem[] ContextMenuItems
{ {
get get
{ {
if (!selectedBlueprints.Any(b => b.IsHovered)) if (!selectedBlueprints.Any(b => b.IsHovered))
return Array.Empty<MenuItem>(); return Array.Empty<MenuItem>();
var items = new List<MenuItem> var items = new List<MenuItem>();
items.AddRange(GetContextMenuItemsForSelection(selectedBlueprints));
if (selectedBlueprints.Count == 1)
items.AddRange(selectedBlueprints[0].ContextMenuItems);
items.AddRange(new[]
{ {
new OsuMenuItem("Sound") new OsuMenuItem("Sound")
{ {
@ -263,15 +270,20 @@ namespace osu.Game.Screens.Edit.Compose.Components
} }
}, },
new OsuMenuItem("Delete", MenuItemType.Destructive, deleteSelected), new OsuMenuItem("Delete", MenuItemType.Destructive, deleteSelected),
}; });
if (selectedBlueprints.Count == 1)
items.AddRange(selectedBlueprints[0].ContextMenuItems);
return items.ToArray(); return items.ToArray();
} }
} }
/// <summary>
/// Provide context menu items relevant to current selection. Calling base is not required.
/// </summary>
/// <param name="selection">The current selection.</param>
/// <returns>The relevant menu items.</returns>
protected virtual IEnumerable<MenuItem> GetContextMenuItemsForSelection(IEnumerable<SelectionBlueprint> selection)
=> Enumerable.Empty<MenuItem>();
private MenuItem createHitSampleMenuItem(string name, string sampleName) private MenuItem createHitSampleMenuItem(string name, string sampleName)
{ {
return new TernaryStateMenuItem(name, MenuItemType.Standard, setHitSampleState) return new TernaryStateMenuItem(name, MenuItemType.Standard, setHitSampleState)

View File

@ -9,7 +9,6 @@ using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Audio; using osu.Framework.Graphics.Audio;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Timing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
@ -17,15 +16,15 @@ using osuTK;
namespace osu.Game.Screens.Edit.Compose.Components.Timeline namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{ {
[Cached(typeof(IDistanceSnapProvider))] [Cached(typeof(IPositionSnapProvider))]
[Cached] [Cached]
public class Timeline : ZoomableScrollContainer, IDistanceSnapProvider public class Timeline : ZoomableScrollContainer, IPositionSnapProvider
{ {
public readonly Bindable<bool> WaveformVisible = new Bindable<bool>(); public readonly Bindable<bool> WaveformVisible = new Bindable<bool>();
public readonly IBindable<WorkingBeatmap> Beatmap = new Bindable<WorkingBeatmap>(); public readonly IBindable<WorkingBeatmap> Beatmap = new Bindable<WorkingBeatmap>();
[Resolved] [Resolved]
private IAdjustableClock adjustableClock { get; set; } private EditorClock editorClock { get; set; }
public Timeline() public Timeline()
{ {
@ -101,7 +100,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
Content.Margin = new MarginPadding { Horizontal = DrawWidth / 2 }; Content.Margin = new MarginPadding { Horizontal = DrawWidth / 2 };
// This needs to happen after transforms are updated, but before the scroll position is updated in base.UpdateAfterChildren // This needs to happen after transforms are updated, but before the scroll position is updated in base.UpdateAfterChildren
if (adjustableClock.IsRunning) if (editorClock.IsRunning)
scrollToTrackTime(); scrollToTrackTime();
} }
@ -111,21 +110,21 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
if (handlingDragInput) if (handlingDragInput)
seekTrackToCurrent(); seekTrackToCurrent();
else if (!adjustableClock.IsRunning) else if (!editorClock.IsRunning)
{ {
// The track isn't running. There are two cases we have to be wary of: // The track isn't running. There are two cases we have to be wary of:
// 1) The user flick-drags on this timeline: We want the track to follow us // 1) The user flick-drags on this timeline: We want the track to follow us
// 2) The user changes the track time through some other means (scrolling in the editor or overview timeline): We want to follow the track time // 2) The user changes the track time through some other means (scrolling in the editor or overview timeline): We want to follow the track time
// The simplest way to cover both cases is by checking whether the scroll position has changed and the audio hasn't been changed externally // The simplest way to cover both cases is by checking whether the scroll position has changed and the audio hasn't been changed externally
if (Current != lastScrollPosition && adjustableClock.CurrentTime == lastTrackTime) if (Current != lastScrollPosition && editorClock.CurrentTime == lastTrackTime)
seekTrackToCurrent(); seekTrackToCurrent();
else else
scrollToTrackTime(); scrollToTrackTime();
} }
lastScrollPosition = Current; lastScrollPosition = Current;
lastTrackTime = adjustableClock.CurrentTime; lastTrackTime = editorClock.CurrentTime;
} }
private void seekTrackToCurrent() private void seekTrackToCurrent()
@ -133,7 +132,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
if (!track.IsLoaded) if (!track.IsLoaded)
return; return;
adjustableClock.Seek(Current / Content.DrawWidth * track.Length); editorClock.Seek(Current / Content.DrawWidth * track.Length);
} }
private void scrollToTrackTime() private void scrollToTrackTime()
@ -141,7 +140,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
if (!track.IsLoaded || track.Length == 0) if (!track.IsLoaded || track.Length == 0)
return; return;
ScrollTo((float)(adjustableClock.CurrentTime / track.Length) * Content.DrawWidth, false); ScrollTo((float)(editorClock.CurrentTime / track.Length) * Content.DrawWidth, false);
} }
protected override bool OnMouseDown(MouseDownEvent e) protected override bool OnMouseDown(MouseDownEvent e)
@ -164,15 +163,15 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
private void beginUserDrag() private void beginUserDrag()
{ {
handlingDragInput = true; handlingDragInput = true;
trackWasPlaying = adjustableClock.IsRunning; trackWasPlaying = editorClock.IsRunning;
adjustableClock.Stop(); editorClock.Stop();
} }
private void endUserDrag() private void endUserDrag()
{ {
handlingDragInput = false; handlingDragInput = false;
if (trackWasPlaying) if (trackWasPlaying)
adjustableClock.Start(); editorClock.Start();
} }
[Resolved] [Resolved]
@ -181,11 +180,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
[Resolved] [Resolved]
private IBeatSnapProvider beatSnapProvider { get; set; } private IBeatSnapProvider beatSnapProvider { get; set; }
public double GetTimeFromScreenSpacePosition(Vector2 position) public SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) =>
=> getTimeFromPosition(Content.ToLocalSpace(position)); new SnapResult(screenSpacePosition, beatSnapProvider.SnapTime(getTimeFromPosition(Content.ToLocalSpace(screenSpacePosition))));
public (Vector2 position, double time) GetSnappedPosition(Vector2 position, double time) =>
(position, beatSnapProvider.SnapTime(getTimeFromPosition(position)));
private double getTimeFromPosition(Vector2 localPosition) => private double getTimeFromPosition(Vector2 localPosition) =>
(localPosition.X / Content.DrawWidth) * track.Length; (localPosition.X / Content.DrawWidth) * track.Length;

View File

@ -186,7 +186,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
} }
} }
public override Vector2 SelectionPoint => ScreenSpaceDrawQuad.TopLeft; public override Vector2 ScreenSpaceSelectionPoint => ScreenSpaceDrawQuad.TopLeft;
public class DragBar : Container public class DragBar : Container
{ {
@ -275,14 +275,14 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
OnDragHandled?.Invoke(e); OnDragHandled?.Invoke(e);
var time = timeline.GetTimeFromScreenSpacePosition(e.ScreenSpaceMousePosition); if (timeline.SnapScreenSpacePositionToValidTime(e.ScreenSpaceMousePosition).Time is double time)
{
switch (hitObject) switch (hitObject)
{ {
case IHasRepeats repeatHitObject: case IHasRepeats repeatHitObject:
// find the number of repeats which can fit in the requested time. // find the number of repeats which can fit in the requested time.
var lengthOfOneRepeat = repeatHitObject.Duration / (repeatHitObject.RepeatCount + 1); var lengthOfOneRepeat = repeatHitObject.Duration / (repeatHitObject.RepeatCount + 1);
var proposedCount = Math.Max(0, (int)((time - hitObject.StartTime) / lengthOfOneRepeat) - 1); var proposedCount = Math.Max(0, (int)Math.Round((time - hitObject.StartTime) / lengthOfOneRepeat) - 1);
if (proposedCount == repeatHitObject.RepeatCount) if (proposedCount == repeatHitObject.RepeatCount)
return; return;
@ -302,6 +302,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
beatmap.UpdateHitObject(hitObject); beatmap.UpdateHitObject(hitObject);
} }
}
protected override void OnDragEnd(DragEndEvent e) protected override void OnDragEnd(DragEndEvent e)
{ {

View File

@ -83,8 +83,8 @@ namespace osu.Game.Screens.Edit
clock = new EditorClock(Beatmap.Value, beatDivisor) { IsCoupled = false }; clock = new EditorClock(Beatmap.Value, beatDivisor) { IsCoupled = false };
clock.ChangeSource(sourceClock); clock.ChangeSource(sourceClock);
dependencies.CacheAs<IFrameBasedClock>(clock); dependencies.CacheAs(clock);
dependencies.CacheAs<IAdjustableClock>(clock); AddInternal(clock);
// todo: remove caching of this and consume via editorBeatmap? // todo: remove caching of this and consume via editorBeatmap?
dependencies.Cache(beatDivisor); dependencies.Cache(beatDivisor);

View File

@ -3,6 +3,8 @@
using System; using System;
using System.Linq; using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Transforms;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Framework.Timing; using osu.Framework.Timing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
@ -13,7 +15,7 @@ namespace osu.Game.Screens.Edit
/// <summary> /// <summary>
/// A decoupled clock which adds editor-specific functionality, such as snapping to a user-defined beat divisor. /// A decoupled clock which adds editor-specific functionality, such as snapping to a user-defined beat divisor.
/// </summary> /// </summary>
public class EditorClock : DecoupleableInterpolatingFramedClock public class EditorClock : Component, IFrameBasedClock, IAdjustableClock, ISourceChangeableClock
{ {
public readonly double TrackLength; public readonly double TrackLength;
@ -21,12 +23,11 @@ namespace osu.Game.Screens.Edit
private readonly BindableBeatDivisor beatDivisor; private readonly BindableBeatDivisor beatDivisor;
public EditorClock(WorkingBeatmap beatmap, BindableBeatDivisor beatDivisor) private readonly DecoupleableInterpolatingFramedClock underlyingClock;
{
this.beatDivisor = beatDivisor;
ControlPointInfo = beatmap.Beatmap.ControlPointInfo; public EditorClock(WorkingBeatmap beatmap, BindableBeatDivisor beatDivisor)
TrackLength = beatmap.Track.Length; : this(beatmap.Beatmap.ControlPointInfo, beatmap.Track.Length, beatDivisor)
{
} }
public EditorClock(ControlPointInfo controlPointInfo, double trackLength, BindableBeatDivisor beatDivisor) public EditorClock(ControlPointInfo controlPointInfo, double trackLength, BindableBeatDivisor beatDivisor)
@ -35,6 +36,13 @@ namespace osu.Game.Screens.Edit
ControlPointInfo = controlPointInfo; ControlPointInfo = controlPointInfo;
TrackLength = trackLength; TrackLength = trackLength;
underlyingClock = new DecoupleableInterpolatingFramedClock();
}
public EditorClock()
: this(new ControlPointInfo(), 1000, new BindableBeatDivisor())
{
} }
/// <summary> /// <summary>
@ -79,20 +87,22 @@ namespace osu.Game.Screens.Edit
private void seek(int direction, bool snapped, double amount = 1) private void seek(int direction, bool snapped, double amount = 1)
{ {
double current = CurrentTimeAccurate;
if (amount <= 0) throw new ArgumentException("Value should be greater than zero", nameof(amount)); if (amount <= 0) throw new ArgumentException("Value should be greater than zero", nameof(amount));
var timingPoint = ControlPointInfo.TimingPointAt(CurrentTime); var timingPoint = ControlPointInfo.TimingPointAt(current);
if (direction < 0 && timingPoint.Time == CurrentTime) if (direction < 0 && timingPoint.Time == current)
// When going backwards and we're at the boundary of two timing points, we compute the seek distance with the timing point which we are seeking into // When going backwards and we're at the boundary of two timing points, we compute the seek distance with the timing point which we are seeking into
timingPoint = ControlPointInfo.TimingPointAt(CurrentTime - 1); timingPoint = ControlPointInfo.TimingPointAt(current - 1);
double seekAmount = timingPoint.BeatLength / beatDivisor.Value * amount; double seekAmount = timingPoint.BeatLength / beatDivisor.Value * amount;
double seekTime = CurrentTime + seekAmount * direction; double seekTime = current + seekAmount * direction;
if (!snapped || ControlPointInfo.TimingPoints.Count == 0) if (!snapped || ControlPointInfo.TimingPoints.Count == 0)
{ {
Seek(seekTime); SeekTo(seekTime);
return; return;
} }
@ -110,7 +120,7 @@ namespace osu.Game.Screens.Edit
// Due to the rounding above, we may end up on the current beat. This will effectively cause 0 seeking to happen, but we don't want this. // Due to the rounding above, we may end up on the current beat. This will effectively cause 0 seeking to happen, but we don't want this.
// Instead, we'll go to the next beat in the direction when this is the case // Instead, we'll go to the next beat in the direction when this is the case
if (Precision.AlmostEquals(CurrentTime, seekTime)) if (Precision.AlmostEquals(current, seekTime))
{ {
closestBeat += direction > 0 ? 1 : -1; closestBeat += direction > 0 ? 1 : -1;
seekTime = timingPoint.Time + closestBeat * seekAmount; seekTime = timingPoint.Time + closestBeat * seekAmount;
@ -125,7 +135,97 @@ namespace osu.Game.Screens.Edit
// Ensure the sought point is within the boundaries // Ensure the sought point is within the boundaries
seekTime = Math.Clamp(seekTime, 0, TrackLength); seekTime = Math.Clamp(seekTime, 0, TrackLength);
Seek(seekTime); SeekTo(seekTime);
}
/// <summary>
/// The current time of this clock, include any active transform seeks performed via <see cref="SeekTo"/>.
/// </summary>
public double CurrentTimeAccurate =>
Transforms.OfType<TransformSeek>().FirstOrDefault()?.EndValue ?? CurrentTime;
public double CurrentTime => underlyingClock.CurrentTime;
public void Reset()
{
ClearTransforms();
underlyingClock.Reset();
}
public void Start()
{
ClearTransforms();
underlyingClock.Start();
}
public void Stop()
{
underlyingClock.Stop();
}
public bool Seek(double position)
{
ClearTransforms();
return underlyingClock.Seek(position);
}
public void ResetSpeedAdjustments() => underlyingClock.ResetSpeedAdjustments();
double IAdjustableClock.Rate
{
get => underlyingClock.Rate;
set => underlyingClock.Rate = value;
}
double IClock.Rate => underlyingClock.Rate;
public bool IsRunning => underlyingClock.IsRunning;
public void ProcessFrame() => underlyingClock.ProcessFrame();
public double ElapsedFrameTime => underlyingClock.ElapsedFrameTime;
public double FramesPerSecond => underlyingClock.FramesPerSecond;
public FrameTimeInfo TimeInfo => underlyingClock.TimeInfo;
public void ChangeSource(IClock source) => underlyingClock.ChangeSource(source);
public IClock Source => underlyingClock.Source;
public bool IsCoupled
{
get => underlyingClock.IsCoupled;
set => underlyingClock.IsCoupled = value;
}
private const double transform_time = 300;
public void SeekTo(double seekDestination)
{
if (IsRunning)
Seek(seekDestination);
else
transformSeekTo(seekDestination, transform_time, Easing.OutQuint);
}
private void transformSeekTo(double seek, double duration = 0, Easing easing = Easing.None)
=> this.TransformTo(this.PopulateTransform(new TransformSeek(), seek, duration, easing));
private double currentTime
{
get => underlyingClock.CurrentTime;
set => underlyingClock.Seek(value);
}
private class TransformSeek : Transform<double, EditorClock>
{
public override string TargetMember => nameof(currentTime);
protected override void Apply(EditorClock clock, double time) =>
clock.currentTime = Interpolation.ValueAt(time, StartValue, EndValue, StartTime, EndTime, Easing);
protected override void ReadIntoStartValue(EditorClock clock) => StartValue = clock.currentTime;
} }
} }
} }

View File

@ -7,7 +7,6 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Timing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics; using osu.Game.Graphics;
@ -23,7 +22,7 @@ namespace osu.Game.Screens.Edit.Timing
private Bindable<ControlPointGroup> selectedGroup = new Bindable<ControlPointGroup>(); private Bindable<ControlPointGroup> selectedGroup = new Bindable<ControlPointGroup>();
[Resolved] [Resolved]
private IAdjustableClock clock { get; set; } private EditorClock clock { get; set; }
protected override Drawable CreateMainContent() => new GridContainer protected override Drawable CreateMainContent() => new GridContainer
{ {
@ -50,7 +49,7 @@ namespace osu.Game.Screens.Edit.Timing
selectedGroup.BindValueChanged(selected => selectedGroup.BindValueChanged(selected =>
{ {
if (selected.NewValue != null) if (selected.NewValue != null)
clock.Seek(selected.NewValue.Time); clock.SeekTo(selected.NewValue.Time);
}); });
} }
@ -62,7 +61,7 @@ namespace osu.Game.Screens.Edit.Timing
private IBindableList<ControlPointGroup> controlGroups; private IBindableList<ControlPointGroup> controlGroups;
[Resolved] [Resolved]
private IFrameBasedClock clock { get; set; } private EditorClock clock { get; set; }
[Resolved] [Resolved]
protected IBindable<WorkingBeatmap> Beatmap { get; private set; } protected IBindable<WorkingBeatmap> Beatmap { get; private set; }

View File

@ -24,8 +24,6 @@ namespace osu.Game.Skinning
public bool DeletePending { get; set; } public bool DeletePending { get; set; }
public string FullName => $"\"{Name}\" by {Creator}";
public static SkinInfo Default { get; } = new SkinInfo public static SkinInfo Default { get; } = new SkinInfo
{ {
Name = "osu!lazer", Name = "osu!lazer",
@ -34,6 +32,10 @@ namespace osu.Game.Skinning
public bool Equals(SkinInfo other) => other != null && ID == other.ID; public bool Equals(SkinInfo other) => other != null && ID == other.ID;
public override string ToString() => FullName; public override string ToString()
{
string author = Creator == null ? string.Empty : $"({Creator})";
return $"{Name} {author}".Trim();
}
} }
} }

View File

@ -30,8 +30,7 @@ namespace osu.Game.Tests.Visual
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
dependencies.Cache(BeatDivisor); dependencies.Cache(BeatDivisor);
dependencies.CacheAs<IFrameBasedClock>(Clock); dependencies.CacheAs(Clock);
dependencies.CacheAs<IAdjustableClock>(Clock);
return dependencies; return dependencies;
} }

View File

@ -8,6 +8,7 @@ using osu.Framework.Timing;
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.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Compose; using osu.Game.Screens.Edit.Compose;
namespace osu.Game.Tests.Visual namespace osu.Game.Tests.Visual
@ -32,7 +33,7 @@ namespace osu.Game.Tests.Visual
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{ {
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
dependencies.CacheAs<IAdjustableClock>(new StopwatchClock()); dependencies.CacheAs(new EditorClock());
return dependencies; return dependencies;
} }
@ -71,9 +72,12 @@ namespace osu.Game.Tests.Visual
{ {
base.Update(); base.Update();
currentBlueprint.UpdatePosition(InputManager.CurrentState.Mouse.Position); currentBlueprint.UpdatePosition(SnapForBlueprint(currentBlueprint));
} }
protected virtual SnapResult SnapForBlueprint(PlacementBlueprint blueprint) =>
new SnapResult(InputManager.CurrentState.Mouse.Position, null);
public override void Add(Drawable drawable) public override void Add(Drawable drawable)
{ {
base.Add(drawable); base.Add(drawable);
@ -81,7 +85,7 @@ namespace osu.Game.Tests.Visual
if (drawable is PlacementBlueprint blueprint) if (drawable is PlacementBlueprint blueprint)
{ {
blueprint.Show(); blueprint.Show();
blueprint.UpdatePosition(InputManager.CurrentState.Mouse.Position); blueprint.UpdatePosition(SnapForBlueprint(blueprint));
} }
} }

View File

@ -19,15 +19,15 @@
</ItemGroup> </ItemGroup>
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Dapper" Version="2.0.35" /> <PackageReference Include="Dapper" Version="2.0.35" />
<PackageReference Include="DiffPlex" Version="1.6.1" /> <PackageReference Include="DiffPlex" Version="1.6.2" />
<PackageReference Include="Humanizer" Version="2.8.11" /> <PackageReference Include="Humanizer" Version="2.8.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="2020.518.0" /> <PackageReference Include="ppy.osu.Framework" Version="2020.525.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.512.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2020.512.0" />
<PackageReference Include="Sentry" Version="2.1.1" /> <PackageReference Include="Sentry" Version="2.1.1" />
<PackageReference Include="SharpCompress" Version="0.25.0" /> <PackageReference Include="SharpCompress" Version="0.25.1" />
<PackageReference Include="NUnit" Version="3.12.0" /> <PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="System.ComponentModel.Annotations" Version="4.7.0" /> <PackageReference Include="System.ComponentModel.Annotations" Version="4.7.0" />
</ItemGroup> </ItemGroup>

View File

@ -70,18 +70,18 @@
<Reference Include="System.Net.Http" /> <Reference Include="System.Net.Http" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="ppy.osu.Framework.iOS" Version="2020.518.0" /> <PackageReference Include="ppy.osu.Framework.iOS" Version="2020.525.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.512.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2020.512.0" />
</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">
<PackageReference Include="DiffPlex" Version="1.6.1" /> <PackageReference Include="DiffPlex" Version="1.6.2" />
<PackageReference Include="Humanizer" Version="2.8.11" /> <PackageReference Include="Humanizer" Version="2.8.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="2020.518.0" /> <PackageReference Include="ppy.osu.Framework" Version="2020.525.0" />
<PackageReference Include="SharpCompress" Version="0.25.0" /> <PackageReference Include="SharpCompress" Version="0.25.1" />
<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.7.0" /> <PackageReference Include="System.ComponentModel.Annotations" Version="4.7.0" />