1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-16 21:42:55 +08:00

Merge branch 'master' into custom-ipc-location

This commit is contained in:
Shivam 2020-05-28 15:07:43 +02:00 committed by GitHub
commit 6e8a964c45
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
68 changed files with 801 additions and 478 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.
**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:
- 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>
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.512.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.519.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.528.0" />
</ItemGroup>
</Project>

View File

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

View File

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

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Threading;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Types;
@ -14,13 +15,13 @@ namespace osu.Game.Rulesets.Catch.Objects
public override Judgement CreateJudgement() => new IgnoreJudgement();
protected override void CreateNestedHitObjects()
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
{
base.CreateNestedHitObjects();
createBananas();
base.CreateNestedHitObjects(cancellationToken);
createBananas(cancellationToken);
}
private void createBananas()
private void createBananas(CancellationToken cancellationToken)
{
double spacing = Duration;
while (spacing > 100)
@ -31,6 +32,8 @@ namespace osu.Game.Rulesets.Catch.Objects
for (double i = StartTime; i <= EndTime; i += spacing)
{
cancellationToken.ThrowIfCancellationRequested();
AddNested(new Banana
{
Samples = Samples,

View File

@ -3,6 +3,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
@ -45,9 +46,9 @@ namespace osu.Game.Rulesets.Catch.Objects
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
{
@ -58,7 +59,7 @@ namespace osu.Game.Rulesets.Catch.Objects
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
if (lastEvent != null)
@ -73,6 +74,8 @@ namespace osu.Game.Rulesets.Catch.Objects
for (double t = timeBetweenTiny; t < sinceLastTick; t += timeBetweenTiny)
{
cancellationToken.ThrowIfCancellationRequested();
AddNested(new TinyDroplet
{
StartTime = t + lastEvent.Value.Time,

View File

@ -8,7 +8,6 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Timing;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Edit;
using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Mods;
@ -19,8 +18,7 @@ using osuTK.Graphics;
namespace osu.Game.Rulesets.Mania.Tests
{
[Cached(Type = typeof(IManiaHitObjectComposer))]
public abstract class ManiaPlacementBlueprintTestScene : PlacementBlueprintTestScene, IManiaHitObjectComposer
public abstract class ManiaPlacementBlueprintTestScene : PlacementBlueprintTestScene
{
private readonly Column column;
@ -48,7 +46,7 @@ namespace osu.Game.Rulesets.Mania.Tests
var time = column.TimeAtScreenSpacePosition(InputManager.CurrentState.Mouse.Position);
var pos = column.ScreenSpacePositionAtTime(time);
return new ManiaSnapResult(pos, time, column);
return new SnapResult(pos, time, column);
}
protected override Container CreateHitObjectContainer() => new ScrollingTestContainer(ScrollingDirection.Down) { RelativeSizeAxes = Axes.Both };

View File

@ -4,15 +4,13 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Timing;
using osu.Game.Rulesets.Mania.Edit;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Tests.Visual;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Mania.Tests
{
[Cached(Type = typeof(IManiaHitObjectComposer))]
public abstract class ManiaSelectionBlueprintTestScene : SelectionBlueprintTestScene, IManiaHitObjectComposer
public abstract class ManiaSelectionBlueprintTestScene : SelectionBlueprintTestScene
{
[Cached(Type = typeof(IAdjustableClock))]
private readonly IAdjustableClock clock = new StopwatchClock();

View File

@ -2,23 +2,27 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Input.Events;
using osu.Framework.Timing;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Configuration;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Edit;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Screens.Edit;
using osu.Game.Tests.Visual;
using osuTK;
namespace osu.Game.Rulesets.Mania.Tests
{
[Cached(typeof(IManiaHitObjectComposer))]
public class TestSceneManiaBeatSnapGrid : EditorClockTestScene, IManiaHitObjectComposer
public class TestSceneManiaBeatSnapGrid : EditorClockTestScene
{
[Cached(typeof(IScrollingInfo))]
private ScrollingTestContainer.TestScrollingInfo scrollingInfo = new ScrollingTestContainer.TestScrollingInfo();
@ -50,7 +54,10 @@ namespace osu.Game.Rulesets.Mania.Tests
{
Clock = new FramedClock(new StopwatchClock())
},
beatSnapGrid = new ManiaBeatSnapGrid()
new TestHitObjectComposer(Playfield)
{
Child = beatSnapGrid = new ManiaBeatSnapGrid()
}
};
}
@ -67,4 +74,51 @@ namespace osu.Game.Rulesets.Mania.Tests
public ManiaPlayfield Playfield { get; }
}
public class TestHitObjectComposer : HitObjectComposer
{
public override Playfield Playfield { get; }
public override IEnumerable<DrawableHitObject> HitObjects => Enumerable.Empty<DrawableHitObject>();
public override bool CursorInPlacementArea => false;
public TestHitObjectComposer(Playfield playfield)
{
Playfield = playfield;
}
public Drawable Child
{
set => InternalChild = value;
}
public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition)
{
throw new System.NotImplementedException();
}
public override float GetBeatSnapDistanceAt(double referenceTime)
{
throw new System.NotImplementedException();
}
public override float DurationToDistance(double referenceTime, double duration)
{
throw new System.NotImplementedException();
}
public override double DistanceToDuration(double referenceTime, float distance)
{
throw new System.NotImplementedException();
}
public override double GetSnappedDurationFromDistance(double referenceTime, float distance)
{
throw new System.NotImplementedException();
}
public override float GetSnappedDistanceFromDistance(double referenceTime, float distance)
{
throw new System.NotImplementedException();
}
}
}

View File

@ -8,6 +8,7 @@ using osu.Framework.Input.Events;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Edit.Blueprints.Components;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.UI.Scrolling;
using osuTK;
using osuTK.Input;
@ -20,7 +21,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
private readonly EditNotePiece tailPiece;
[Resolved]
private IManiaHitObjectComposer composer { get; set; }
private IScrollingInfo scrollingInfo { get; set; }
public HoldNotePlacementBlueprint()
: base(new HoldNote())
@ -43,6 +44,19 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
{
headPiece.Y = Parent.ToLocalSpace(Column.ScreenSpacePositionAtTime(HitObject.StartTime)).Y;
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));
@ -78,9 +92,9 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
}
else
{
if (result is ManiaSnapResult maniaResult)
if (result.Playfield != null)
{
headPiece.Width = tailPiece.Width = maniaResult.Column.DrawWidth;
headPiece.Width = tailPiece.Width = result.Playfield.DrawWidth;
headPiece.X = tailPiece.X = ToLocalSpace(result.ScreenSpacePosition).X;
}

View File

@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
base.UpdatePosition(result);
if (!PlacementActive)
Column = (result as ManiaSnapResult)?.Column;
Column = result.Playfield as Column;
}
}
}

View File

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

View File

@ -26,9 +26,9 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
{
base.UpdatePosition(result);
if (result is ManiaSnapResult maniaResult)
if (result.Playfield != null)
{
piece.Width = maniaResult.Column.DrawWidth;
piece.Width = result.Playfield.DrawWidth;
piece.Position = ToLocalSpace(result.ScreenSpacePosition);
}
}

View File

@ -1,12 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Mania.UI;
namespace osu.Game.Rulesets.Mania.Edit
{
public interface IManiaHitObjectComposer
{
ManiaPlayfield Playfield { get; }
}
}

View File

@ -11,6 +11,8 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI.Scrolling;
@ -63,9 +65,9 @@ namespace osu.Game.Rulesets.Mania.Edit
private (double start, double end)? selectionTimeRange;
[BackgroundDependencyLoader]
private void load(IManiaHitObjectComposer composer)
private void load(HitObjectComposer composer)
{
foreach (var stage in composer.Playfield.Stages)
foreach (var stage in ((ManiaPlayfield)composer.Playfield).Stages)
{
foreach (var column in stage.Columns)
{

View File

@ -9,6 +9,7 @@ using System.Collections.Generic;
using System.Linq;
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.Mods;
using osu.Game.Rulesets.Objects;
@ -19,8 +20,7 @@ using osuTK;
namespace osu.Game.Rulesets.Mania.Edit
{
[Cached(Type = typeof(IManiaHitObjectComposer))]
public class ManiaHitObjectComposer : HitObjectComposer<ManiaHitObject>, IManiaHitObjectComposer
public class ManiaHitObjectComposer : HitObjectComposer<ManiaHitObject>
{
private DrawableManiaEditRuleset drawableRuleset;
private ManiaBeatSnapGrid beatSnapGrid;
@ -49,28 +49,35 @@ namespace osu.Game.Rulesets.Mania.Edit
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
=> dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
public ManiaPlayfield Playfield => ((ManiaPlayfield)drawableRuleset.Playfield);
public new ManiaPlayfield Playfield => ((ManiaPlayfield)drawableRuleset.Playfield);
public IScrollingInfo ScrollingInfo => drawableRuleset.ScrollingInfo;
protected override Playfield PlayfieldAtScreenSpacePosition(Vector2 screenSpacePosition) =>
Playfield.GetColumnByPosition(screenSpacePosition);
public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition)
{
var column = Playfield.GetColumnByPosition(screenSpacePosition);
var result = base.SnapScreenSpacePositionToValidTime(screenSpacePosition);
if (column == null)
return new SnapResult(screenSpacePosition, null);
switch (ScrollingInfo.Direction.Value)
{
case ScrollingDirection.Down:
result.ScreenSpacePosition -= new Vector2(0, getNoteHeight() / 2);
break;
double targetTime = column.TimeAtScreenSpacePosition(screenSpacePosition);
case ScrollingDirection.Up:
result.ScreenSpacePosition += new Vector2(0, getNoteHeight() / 2);
break;
}
// apply beat snapping
targetTime = BeatSnapProvider.SnapTime(targetTime);
// convert back to screen space
screenSpacePosition = column.ScreenSpacePositionAtTime(targetTime);
return new ManiaSnapResult(screenSpacePosition, targetTime, column);
return result;
}
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)
{
drawableRuleset = new DrawableManiaEditRuleset(ruleset, beatmap, mods);

View File

@ -4,6 +4,7 @@
using System;
using System.Linq;
using osu.Framework.Allocation;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Edit.Blueprints;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.UI.Scrolling;
@ -17,7 +18,7 @@ namespace osu.Game.Rulesets.Mania.Edit
private IScrollingInfo scrollingInfo { get; set; }
[Resolved]
private IManiaHitObjectComposer composer { get; set; }
private HitObjectComposer composer { get; set; }
public override bool HandleMovement(MoveSelectionEvent moveEvent)
{
@ -31,7 +32,9 @@ namespace osu.Game.Rulesets.Mania.Edit
private void performColumnMovement(int lastColumn, MoveSelectionEvent moveEvent)
{
var currentColumn = composer.Playfield.GetColumnByPosition(moveEvent.ScreenSpacePosition);
var maniaPlayfield = ((ManiaHitObjectComposer)composer).Playfield;
var currentColumn = maniaPlayfield.GetColumnByPosition(moveEvent.ScreenSpacePosition);
if (currentColumn == null)
return;
@ -50,7 +53,7 @@ namespace osu.Game.Rulesets.Mania.Edit
maxColumn = obj.Column;
}
columnDelta = Math.Clamp(columnDelta, -minColumn, composer.Playfield.TotalColumns - 1 - maxColumn);
columnDelta = Math.Clamp(columnDelta, -minColumn, maniaPlayfield.TotalColumns - 1 - maxColumn);
foreach (var obj in SelectedHitObjects.OfType<ManiaHitObject>())
obj.Column += columnDelta;

View File

@ -1,20 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.UI;
using osuTK;
namespace osu.Game.Rulesets.Mania.Edit
{
public class ManiaSnapResult : SnapResult
{
public readonly Column Column;
public ManiaSnapResult(Vector2 screenSpacePosition, double time, Column column)
: base(screenSpacePosition, time)
{
Column = column;
}
}
}

View File

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

View File

@ -17,7 +17,6 @@ using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Skinning;
using osuTK;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces;
namespace osu.Game.Rulesets.Mania.UI
{
@ -143,54 +142,5 @@ namespace osu.Game.Rulesets.Mania.UI
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos)
// This probably shouldn't exist as is, but the columns in the stage are separated by a 1px border
=> DrawRectangle.Inflate(new Vector2(Stage.COLUMN_SPACING / 2, 0)).Contains(ToLocalSpace(screenSpacePos));
/// <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, HitObjectContainer.DrawHeight);
switch (ScrollingInfo.Direction.Value)
{
case 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.
// 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.
pos = HitObjectContainer.DrawHeight - pos;
// 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.
pos -= DefaultNotePiece.NOTE_HEIGHT / 2;
break;
case ScrollingDirection.Up:
pos += DefaultNotePiece.NOTE_HEIGHT / 2;
break;
}
return HitObjectContainer.ToScreenSpace(new Vector2(HitObjectContainer.DrawWidth / 2, pos));
}
/// <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 = HitObjectContainer.ToLocalSpace(screenSpacePosition);
switch (ScrollingInfo.Direction.Value)
{
case ScrollingDirection.Down:
// as above
localPosition.Y = HitObjectContainer.DrawHeight - localPosition.Y;
break;
}
// offset for the fact that blueprints are centered, as above.
localPosition.Y -= DefaultNotePiece.NOTE_HEIGHT / 2;
return ScrollingInfo.Algorithm.TimeAt(localPosition.Y, Time.Current, ScrollingInfo.TimeRange.Value, HitObjectContainer.DrawHeight);
}
}
}

View File

@ -12,6 +12,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints
{
protected new T HitObject => (T)DrawableObject.HitObject;
protected override bool AlwaysShowWhenSelected => true;
protected OsuSelectionBlueprint(DrawableHitObject drawableObject)
: base(drawableObject)
{

View File

@ -4,6 +4,10 @@
using System;
using System.Collections.Generic;
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.Rulesets.Edit;
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.UI;
using osu.Game.Screens.Edit.Compose.Components;
using osuTK;
namespace osu.Game.Rulesets.Osu.Edit
{
@ -32,9 +37,80 @@ namespace osu.Game.Rulesets.Osu.Edit
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 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)
return null;
@ -42,7 +118,8 @@ namespace osu.Game.Rulesets.Osu.Edit
var objects = selectedHitObjects.ToList();
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);
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 osu.Game.Rulesets.Objects;
using System.Linq;
using System.Threading;
using osu.Framework.Caching;
using osu.Game.Audio;
using osu.Game.Beatmaps;
@ -133,12 +134,12 @@ namespace osu.Game.Rulesets.Osu.Objects
TickDistance = scoringDistance / difficulty.SliderTickRate * TickDistanceMultiplier;
}
protected override void CreateNestedHitObjects()
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
{
base.CreateNestedHitObjects();
base.CreateNestedHitObjects(cancellationToken);
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)
{

View File

@ -4,6 +4,7 @@
using osu.Game.Rulesets.Objects.Types;
using System;
using System.Collections.Generic;
using System.Threading;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
@ -73,17 +74,17 @@ namespace osu.Game.Rulesets.Taiko.Objects
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);
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)
return;
@ -92,6 +93,8 @@ namespace osu.Game.Rulesets.Taiko.Objects
for (double t = StartTime; t < EndTime + tickSpacing / 2; t += tickSpacing)
{
cancellationToken.ThrowIfCancellationRequested();
AddNested(new DrumRollTick
{
FirstTick = first,

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Threading;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
@ -29,12 +30,15 @@ namespace osu.Game.Rulesets.Taiko.Objects
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++)
{
cancellationToken.ThrowIfCancellationRequested();
AddNested(new SwellTick());
}
}
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.
// See the LICENCE file in the repository root for full licence text.
using System.Threading;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
@ -32,9 +33,9 @@ namespace osu.Game.Rulesets.Taiko.Objects
/// </summary>
public virtual bool IsStrong { get; set; }
protected override void CreateNestedHitObjects()
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
{
base.CreateNestedHitObjects();
base.CreateNestedHitObjects(cancellationToken);
if (IsStrong)
AddNested(new StrongHitObject { StartTime = this.GetEndTime() });

View File

@ -156,7 +156,7 @@ namespace osu.Game.Tests.Beatmaps.IO
var manager = osu.Dependencies.Get<BeatmapManager>();
// ReSharper disable once AccessToModifiedClosure
manager.ItemAdded.BindValueChanged(_ => Interlocked.Increment(ref itemAddRemoveFireCount));
manager.ItemUpdated.BindValueChanged(_ => Interlocked.Increment(ref itemAddRemoveFireCount));
manager.ItemRemoved.BindValueChanged(_ => Interlocked.Increment(ref itemAddRemoveFireCount));
var imported = await LoadOszIntoOsu(osu);
@ -166,7 +166,7 @@ namespace osu.Game.Tests.Beatmaps.IO
imported.Hash += "-changed";
manager.Update(imported);
Assert.AreEqual(0, itemAddRemoveFireCount -= 2);
Assert.AreEqual(0, itemAddRemoveFireCount -= 1);
checkBeatmapSetCount(osu, 1);
checkBeatmapCount(osu, 12);

View File

@ -16,7 +16,7 @@ namespace osu.Game.Tests.Beatmaps
[Test]
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].Time, Is.EqualTo(start_time));
@ -31,7 +31,7 @@ namespace osu.Game.Tests.Beatmaps
[Test]
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].Time, Is.EqualTo(start_time));
@ -52,7 +52,7 @@ namespace osu.Game.Tests.Beatmaps
[Test]
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].Time, Is.EqualTo(start_time));
@ -85,7 +85,7 @@ namespace osu.Game.Tests.Beatmaps
[Test]
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].Time, Is.EqualTo(900));
@ -97,7 +97,7 @@ namespace osu.Game.Tests.Beatmaps
const double velocity = 5;
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(() =>
{

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

View File

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

View File

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

View File

@ -27,7 +27,6 @@ using osu.Game.Online.API.Requests;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Objects;
using Decoder = osu.Game.Beatmaps.Formats.Decoder;
using ZipArchive = SharpCompress.Archives.Zip.ZipArchive;
namespace osu.Game.Beatmaps
{
@ -66,7 +65,6 @@ namespace osu.Game.Beatmaps
private readonly AudioManager audioManager;
private readonly GameHost host;
private readonly BeatmapOnlineLookupQueue onlineLookupQueue;
private readonly Storage exportStorage;
public BeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, AudioManager audioManager, GameHost host = null,
WorkingBeatmap defaultBeatmap = null)
@ -83,7 +81,6 @@ namespace osu.Game.Beatmaps
beatmaps.BeatmapRestored += b => beatmapRestored.Value = new WeakReference<BeatmapInfo>(b);
onlineLookupQueue = new BeatmapOnlineLookupQueue(api, storage);
exportStorage = storage.GetStorageForDirectory("exports");
}
protected override ArchiveDownloadRequest<BeatmapSetInfo> CreateDownloadRequest(BeatmapSetInfo set, bool minimiseDownloadSize) =>
@ -214,26 +211,6 @@ namespace osu.Game.Beatmaps
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>();
/// <summary>

View File

@ -129,12 +129,19 @@ namespace osu.Game.Beatmaps
processor?.PreProcess();
// Compute default values for hitobjects, including creating nested hitobjects in-case they're needed
foreach (var obj in converted.HitObjects)
try
{
if (cancellationSource.IsCancellationRequested)
throw new BeatmapLoadTimeoutException(BeatmapInfo);
foreach (var obj in converted.HitObjects)
{
if (cancellationSource.IsCancellationRequested)
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>())

View File

@ -22,6 +22,7 @@ using osu.Game.IO.Archives;
using osu.Game.IPC;
using osu.Game.Overlays.Notifications;
using osu.Game.Utils;
using SharpCompress.Archives.Zip;
using SharpCompress.Common;
using FileInfo = osu.Game.IO.FileInfo;
@ -54,12 +55,12 @@ namespace osu.Game.Database
public Action<Notification> PostNotification { protected get; set; }
/// <summary>
/// Fired when a new <typeparamref name="TModel"/> becomes available in the database.
/// Fired when a new or updated <typeparamref name="TModel"/> becomes available in the database.
/// This is not guaranteed to run on the update thread.
/// </summary>
public IBindable<WeakReference<TModel>> ItemAdded => itemAdded;
public IBindable<WeakReference<TModel>> ItemUpdated => itemUpdated;
private readonly Bindable<WeakReference<TModel>> itemAdded = new Bindable<WeakReference<TModel>>();
private readonly Bindable<WeakReference<TModel>> itemUpdated = new Bindable<WeakReference<TModel>>();
/// <summary>
/// Fired when a <typeparamref name="TModel"/> is removed from the database.
@ -82,14 +83,18 @@ namespace osu.Game.Database
// ReSharper disable once NotAccessedField.Local (we should keep a reference to this so it is not finalised)
private ArchiveImportIPCChannel ipc;
private readonly Storage exportStorage;
protected ArchiveModelManager(Storage storage, IDatabaseContextFactory contextFactory, MutableDatabaseBackedStoreWithFileIncludes<TModel, TFileModel> modelStore, IIpcHost importHost = null)
{
ContextFactory = contextFactory;
ModelStore = modelStore;
ModelStore.ItemAdded += item => handleEvent(() => itemAdded.Value = new WeakReference<TModel>(item));
ModelStore.ItemUpdated += item => handleEvent(() => itemUpdated.Value = new WeakReference<TModel>(item));
ModelStore.ItemRemoved += item => handleEvent(() => itemRemoved.Value = new WeakReference<TModel>(item));
exportStorage = storage.GetStorageForDirectory("exports");
Files = new FileStore(contextFactory, storage);
if (importHost != null)
@ -369,6 +374,29 @@ namespace osu.Game.Database
return item;
}, 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)
{
using (var usage = ContextFactory.GetForWrite())
@ -710,5 +738,12 @@ namespace osu.Game.Database
}
#endregion
private string getValidFilename(string filename)
{
foreach (char c in Path.GetInvalidFileNameChars())
filename = filename.Replace(c, '_');
return filename;
}
}
}

View File

@ -13,7 +13,7 @@ namespace osu.Game.Database
public interface IModelManager<TModel>
where TModel : class
{
IBindable<WeakReference<TModel>> ItemAdded { get; }
IBindable<WeakReference<TModel>> ItemUpdated { get; }
IBindable<WeakReference<TModel>> ItemRemoved { get; }
}

View File

@ -16,7 +16,14 @@ namespace osu.Game.Database
public abstract class MutableDatabaseBackedStore<T> : DatabaseBackedStore
where T : class, IHasPrimaryKey, ISoftDelete
{
public event Action<T> ItemAdded;
/// <summary>
/// Fired when an item was added or updated.
/// </summary>
public event Action<T> ItemUpdated;
/// <summary>
/// Fired when an item was removed.
/// </summary>
public event Action<T> ItemRemoved;
protected MutableDatabaseBackedStore(IDatabaseContextFactory contextFactory, Storage storage = null)
@ -41,7 +48,7 @@ namespace osu.Game.Database
context.Attach(item);
}
ItemAdded?.Invoke(item);
ItemUpdated?.Invoke(item);
}
/// <summary>
@ -53,8 +60,7 @@ namespace osu.Game.Database
using (var usage = ContextFactory.GetForWrite())
usage.Context.Update(item);
ItemRemoved?.Invoke(item);
ItemAdded?.Invoke(item);
ItemUpdated?.Invoke(item);
}
/// <summary>
@ -91,7 +97,7 @@ namespace osu.Game.Database
item.DeletePending = false;
}
ItemAdded?.Invoke(item);
ItemUpdated?.Invoke(item);
return true;
}

View File

@ -69,7 +69,7 @@ namespace osu.Game.IO
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)
{

View File

@ -34,7 +34,7 @@ namespace osu.Game.Online
Model.Value = model;
}
private IBindable<WeakReference<TModel>> managerAdded;
private IBindable<WeakReference<TModel>> managedUpdated;
private IBindable<WeakReference<TModel>> managerRemoved;
private IBindable<WeakReference<ArchiveDownloadRequest<TModel>>> managerDownloadBegan;
private IBindable<WeakReference<ArchiveDownloadRequest<TModel>>> managerDownloadFailed;
@ -56,8 +56,8 @@ namespace osu.Game.Online
managerDownloadBegan.BindValueChanged(downloadBegan);
managerDownloadFailed = manager.DownloadFailed.GetBoundCopy();
managerDownloadFailed.BindValueChanged(downloadFailed);
managerAdded = manager.ItemAdded.GetBoundCopy();
managerAdded.BindValueChanged(itemAdded);
managedUpdated = manager.ItemUpdated.GetBoundCopy();
managedUpdated.BindValueChanged(itemUpdated);
managerRemoved = manager.ItemRemoved.GetBoundCopy();
managerRemoved.BindValueChanged(itemRemoved);
}
@ -128,7 +128,7 @@ namespace osu.Game.Online
private void onRequestFailure(Exception e) => Schedule(() => attachDownload(null));
private void itemAdded(ValueChangedEvent<WeakReference<TModel>> weakItem)
private void itemUpdated(ValueChangedEvent<WeakReference<TModel>> weakItem)
{
if (weakItem.NewValue.TryGetTarget(out var item))
setDownloadStateFromManager(item, DownloadState.LocallyAvailable);

View File

@ -192,7 +192,7 @@ namespace osu.Game
ScoreManager.Delete(getBeatmapScores(item), true);
});
BeatmapManager.ItemAdded.BindValueChanged(i =>
BeatmapManager.ItemUpdated.BindValueChanged(i =>
{
if (i.NewValue.TryGetTarget(out var item))
ScoreManager.Undelete(getBeatmapScores(item), true);

View File

@ -60,14 +60,14 @@ namespace osu.Game.Overlays
[Resolved(canBeNull: true)]
private OnScreenDisplay onScreenDisplay { get; set; }
private IBindable<WeakReference<BeatmapSetInfo>> managerAdded;
private IBindable<WeakReference<BeatmapSetInfo>> managerUpdated;
private IBindable<WeakReference<BeatmapSetInfo>> managerRemoved;
[BackgroundDependencyLoader]
private void load()
{
managerAdded = beatmaps.ItemAdded.GetBoundCopy();
managerAdded.BindValueChanged(beatmapAdded);
managerUpdated = beatmaps.ItemUpdated.GetBoundCopy();
managerUpdated.BindValueChanged(beatmapUpdated);
managerRemoved = beatmaps.ItemRemoved.GetBoundCopy();
managerRemoved.BindValueChanged(beatmapRemoved);
@ -98,14 +98,14 @@ namespace osu.Game.Overlays
/// </summary>
public bool IsPlaying => current?.Track.IsRunning ?? false;
private void beatmapAdded(ValueChangedEvent<WeakReference<BeatmapSetInfo>> weakSet)
private void beatmapUpdated(ValueChangedEvent<WeakReference<BeatmapSetInfo>> weakSet)
{
if (weakSet.NewValue.TryGetTarget(out var set))
{
Schedule(() =>
{
if (!beatmapSets.Contains(set))
beatmapSets.Add(set);
beatmapSets.Remove(set);
beatmapSets.Add(set);
});
}
}

View File

@ -7,6 +7,7 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Logging;
using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface;
using osu.Game.Skinning;
@ -31,16 +32,18 @@ namespace osu.Game.Overlays.Settings.Sections
[Resolved]
private SkinManager skins { get; set; }
private IBindable<WeakReference<SkinInfo>> managerAdded;
private IBindable<WeakReference<SkinInfo>> managerUpdated;
private IBindable<WeakReference<SkinInfo>> managerRemoved;
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
{
FlowContent.Spacing = new Vector2(0, 5);
Children = new Drawable[]
{
skinDropdown = new SkinSettingsDropdown(),
new ExportSkinButton(),
new SettingsSlider<float, SizeSlider>
{
LabelText = "Menu cursor size",
@ -70,8 +73,8 @@ namespace osu.Game.Overlays.Settings.Sections
},
};
managerAdded = skins.ItemAdded.GetBoundCopy();
managerAdded.BindValueChanged(itemAdded);
managerUpdated = skins.ItemUpdated.GetBoundCopy();
managerUpdated.BindValueChanged(itemUpdated);
managerRemoved = skins.ItemRemoved.GetBoundCopy();
managerRemoved.BindValueChanged(itemRemoved);
@ -89,10 +92,10 @@ namespace osu.Game.Overlays.Settings.Sections
dropdownBindable.BindValueChanged(skin => configBindable.Value = skin.NewValue.ID);
}
private void itemAdded(ValueChangedEvent<WeakReference<SkinInfo>> weakItem)
private void itemUpdated(ValueChangedEvent<WeakReference<SkinInfo>> weakItem)
{
if (weakItem.NewValue.TryGetTarget(out var item))
Schedule(() => skinDropdown.Items = skinDropdown.Items.Append(item).ToArray());
Schedule(() => skinDropdown.Items = skinDropdown.Items.Where(i => !i.Equals(item)).Append(item).ToArray());
}
private void itemRemoved(ValueChangedEvent<WeakReference<SkinInfo>> weakItem)
@ -117,5 +120,35 @@ namespace osu.Game.Overlays.Settings.Sections
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.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input;
using osu.Framework.Input.Events;
using osu.Framework.Logging;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Configuration;
@ -20,6 +19,7 @@ using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Components.RadioButtons;
using osu.Game.Screens.Edit.Compose;
@ -38,23 +38,21 @@ namespace osu.Game.Rulesets.Edit
protected readonly Ruleset Ruleset;
[Resolved]
protected IFrameBasedClock EditorClock { get; private set; }
protected EditorClock EditorClock { get; private set; }
[Resolved]
protected EditorBeatmap EditorBeatmap { get; private set; }
[Resolved]
private IAdjustableClock adjustableClock { get; set; }
[Resolved]
protected IBeatSnapProvider BeatSnapProvider { get; private set; }
protected ComposeBlueprintContainer BlueprintContainer { get; private set; }
public override Playfield Playfield => drawableRulesetWrapper.Playfield;
private DrawableEditRulesetWrapper<TObject> drawableRulesetWrapper;
private Container distanceSnapGridContainer;
private DistanceSnapGrid distanceSnapGrid;
private readonly List<Container> layerContainers = new List<Container>();
protected readonly Container LayerBelowRuleset = new Container { RelativeSizeAxes = Axes.Both };
private InputManager inputManager;
@ -67,7 +65,7 @@ namespace osu.Game.Rulesets.Edit
}
[BackgroundDependencyLoader]
private void load(IFrameBasedClock framedClock)
private void load()
{
Config = Dependencies.Get<RulesetConfigCache>().GetConfigFor(Ruleset);
@ -75,7 +73,7 @@ namespace osu.Game.Rulesets.Edit
{
drawableRulesetWrapper = new DrawableEditRulesetWrapper<TObject>(CreateDrawableRuleset(Ruleset, EditorBeatmap.PlayableBeatmap))
{
Clock = framedClock,
Clock = EditorClock,
ProcessCustomClock = false
};
}
@ -85,17 +83,6 @@ namespace osu.Game.Rulesets.Edit
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
{
RelativeSizeAxes = Axes.Both,
@ -117,11 +104,19 @@ namespace osu.Game.Rulesets.Edit
{
Name = "Content",
RelativeSizeAxes = Axes.Both,
Masking = true,
Children = new Drawable[]
{
layerBelowRuleset,
// layers below playfield
drawableRulesetWrapper.CreatePlayfieldAdjustmentContainer().WithChildren(new Drawable[]
{
LayerBelowRuleset,
new EditorPlayfieldBorder { RelativeSizeAxes = Axes.Both }
}),
drawableRulesetWrapper,
layerAboveRuleset
// layers above playfield
drawableRulesetWrapper.CreatePlayfieldAdjustmentContainer()
.WithChild(BlueprintContainer = CreateBlueprintContainer())
}
}
},
@ -139,7 +134,7 @@ namespace osu.Game.Rulesets.Edit
setSelectTool();
BlueprintContainer.SelectionChanged += selectionChanged;
EditorBeatmap.SelectedHitObjects.CollectionChanged += selectionChanged;
}
protected override bool OnKeyDown(KeyDownEvent e)
@ -165,42 +160,13 @@ namespace osu.Game.Rulesets.Edit
inputManager = GetContainingInputManager();
}
private double lastGridUpdateTime;
protected override void Update()
private void selectionChanged(object sender, NotifyCollectionChangedEventArgs changedArgs)
{
base.Update();
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())
if (EditorBeatmap.SelectedHitObjects.Any())
{
// ensure in selection mode if a selection is made.
setSelectTool();
showGridFor(hitObjects);
}
else
distanceSnapGridContainer.Hide();
}
private void setSelectTool() => toolboxCollection.Items.First().Select();
@ -209,30 +175,12 @@ namespace osu.Game.Rulesets.Edit
{
BlueprintContainer.CurrentTool = tool;
if (tool is SelectTool)
distanceSnapGridContainer.Hide();
else
{
if (!(tool is SelectTool))
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 bool CursorInPlacementArea => drawableRulesetWrapper.Playfield.ReceivePositionalInputAt(inputManager.CurrentState.Mouse.Position);
protected abstract IReadOnlyList<HitObjectCompositionTool> CompositionTools { get; }
@ -254,23 +202,32 @@ namespace osu.Game.Rulesets.Edit
{
EditorBeatmap.Add(hitObject);
if (adjustableClock.CurrentTime < hitObject.StartTime)
adjustableClock.Seek(hitObject.StartTime);
if (EditorClock.CurrentTime < hitObject.StartTime)
EditorClock.SeekTo(hitObject.StartTime);
}
showGridFor(Enumerable.Empty<HitObject>());
}
public void Delete(HitObject hitObject) => EditorBeatmap.Remove(hitObject);
protected virtual Playfield PlayfieldAtScreenSpacePosition(Vector2 screenSpacePosition) => drawableRulesetWrapper.Playfield;
public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition)
{
if (distanceSnapGrid == null) return new SnapResult(screenSpacePosition, null);
var playfield = PlayfieldAtScreenSpacePosition(screenSpacePosition);
double? targetTime = null;
// TODO: move distance snap grid to OsuHitObjectComposer.
(Vector2 pos, double time) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(screenSpacePosition));
if (playfield is ScrollingPlayfield scrollingPlayfield)
{
targetTime = scrollingPlayfield.TimeAtScreenSpacePosition(screenSpacePosition);
return new SnapResult(distanceSnapGrid.ToScreenSpace(pos), time);
// 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)
@ -306,11 +263,13 @@ namespace osu.Game.Rulesets.Edit
[Cached(typeof(IPositionSnapProvider))]
public abstract class HitObjectComposer : CompositeDrawable, IPositionSnapProvider
{
internal HitObjectComposer()
protected HitObjectComposer()
{
RelativeSizeAxes = Axes.Both;
}
public abstract Playfield Playfield { get; }
/// <summary>
/// All the <see cref="DrawableHitObject"/>s.
/// </summary>
@ -321,14 +280,6 @@ namespace osu.Game.Rulesets.Edit
/// </summary>
public abstract bool CursorInPlacementArea { get; }
/// <summary>
/// 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 SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition);
public abstract float GetBeatSnapDistanceAt(double referenceTime);

View File

@ -15,7 +15,12 @@ namespace osu.Game.Rulesets.Edit
/// </summary>
public readonly DrawableHitObject DrawableObject;
protected override bool ShouldBeAlive => (DrawableObject.IsAlive && DrawableObject.IsPresent) || State == SelectionState.Selected;
/// <summary>
/// Whether the blueprint should be shown even when the <see cref="DrawableObject"/> is not alive.
/// </summary>
protected virtual bool AlwaysShowWhenSelected => false;
protected override bool ShouldBeAlive => (DrawableObject.IsAlive && DrawableObject.IsPresent) || (AlwaysShowWhenSelected && State == SelectionState.Selected);
protected OverlaySelectionBlueprint(DrawableHitObject drawableObject)
: base(drawableObject.HitObject)

View File

@ -1,15 +1,16 @@
// 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.Threading;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Objects;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Compose;
using osuTK;
@ -30,10 +31,13 @@ namespace osu.Game.Rulesets.Edit
/// </summary>
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 Bindable<double> startTimeBindable;
[Resolved]
private IPlacementHandler placementHandler { get; set; }
@ -49,13 +53,12 @@ namespace osu.Game.Rulesets.Edit
}
[BackgroundDependencyLoader]
private void load(IBindable<WorkingBeatmap> beatmap, IAdjustableClock clock)
private void load(IBindable<WorkingBeatmap> beatmap)
{
this.beatmap.BindTo(beatmap);
EditorClock = clock;
ApplyDefaultsToHitObject();
startTimeBindable = HitObject.StartTimeBindable.GetBoundCopy();
startTimeBindable.BindValueChanged(_ => ApplyDefaultsToHitObject(), true);
}
/// <summary>
@ -81,9 +84,6 @@ namespace osu.Game.Rulesets.Edit
PlacementActive = false;
}
[Resolved(canBeNull: true)]
private IFrameBasedClock editorClock { get; set; }
/// <summary>
/// Updates the position of this <see cref="PlacementBlueprint"/> to a new screen-space position.
/// </summary>
@ -91,11 +91,11 @@ namespace osu.Game.Rulesets.Edit
public virtual void UpdatePosition(SnapResult snapResult)
{
if (!PlacementActive)
HitObject.StartTime = snapResult.Time ?? editorClock?.CurrentTime ?? Time.Current;
HitObject.StartTime = snapResult.Time ?? EditorClock?.CurrentTime ?? Time.Current;
}
/// <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"/>.
/// </summary>
protected void ApplyDefaultsToHitObject() => HitObject.ApplyDefaults(beatmap.Value.Beatmap.ControlPointInfo, beatmap.Value.Beatmap.BeatmapInfo.BaseDifficulty);

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.UI;
using osuTK;
namespace osu.Game.Rulesets.Edit
@ -20,10 +21,13 @@ namespace osu.Game.Rulesets.Edit
/// </summary>
public double? Time;
public SnapResult(Vector2 screenSpacePosition, double? time)
public readonly Playfield Playfield;
public SnapResult(Vector2 screenSpacePosition, double? time, Playfield playfield = null)
{
ScreenSpacePosition = screenSpacePosition;
Time = time;
Playfield = playfield;
}
}
}

View File

@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using JetBrains.Annotations;
using Newtonsoft.Json;
using osu.Framework.Bindables;
@ -99,7 +100,8 @@ namespace osu.Game.Rulesets.Objects
/// </summary>
/// <param name="controlPointInfo">The control points.</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);
@ -108,7 +110,7 @@ namespace osu.Game.Rulesets.Objects
nestedHitObjects.Clear();
CreateNestedHitObjects();
CreateNestedHitObjects(cancellationToken);
if (this is IHasComboInformation hasCombo)
{
@ -122,7 +124,7 @@ namespace osu.Game.Rulesets.Objects
nestedHitObjects.Sort((h1, h2) => h1.StartTime.CompareTo(h2.StartTime));
foreach (var h in nestedHitObjects)
h.ApplyDefaults(controlPointInfo, difficulty);
h.ApplyDefaults(controlPointInfo, difficulty, cancellationToken);
DefaultsApplied?.Invoke(this);
}
@ -136,6 +138,14 @@ namespace osu.Game.Rulesets.Objects
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()
{
}

View File

@ -4,13 +4,22 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
namespace osu.Game.Rulesets.Objects
{
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,
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.
// 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 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)
{
@ -108,12 +117,15 @@ namespace osu.Game.Rulesets.Objects
/// <param name="length">The length of the path.</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="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>
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)
{
cancellationToken.ThrowIfCancellationRequested();
if (d >= length - minDistanceFromEnd)
break;

View File

@ -9,6 +9,7 @@ using osu.Framework.Graphics;
using osu.Framework.Layout;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types;
using osuTK;
namespace osu.Game.Rulesets.UI.Scrolling
{
@ -78,6 +79,98 @@ namespace osu.Game.Rulesets.UI.Scrolling
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)
{
// 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.Bindables;
using osu.Game.Rulesets.Objects.Drawables;
using osuTK;
namespace osu.Game.Rulesets.UI.Scrolling
{
@ -23,6 +24,18 @@ namespace osu.Game.Rulesets.UI.Scrolling
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();
}
}

View File

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

View File

@ -5,7 +5,6 @@ using osu.Framework.Graphics;
using osu.Game.Graphics.Sprites;
using System;
using osu.Framework.Allocation;
using osu.Framework.Timing;
using osu.Game.Graphics;
namespace osu.Game.Screens.Edit.Components
@ -15,7 +14,7 @@ namespace osu.Game.Screens.Edit.Components
private readonly OsuSpriteText trackTimer;
[Resolved]
private IAdjustableClock adjustableClock { get; set; }
private EditorClock editorClock { get; set; }
public TimeInfoContainer()
{
@ -35,7 +34,7 @@ namespace osu.Game.Screens.Edit.Components
{
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.Input.Events;
using osu.Framework.Threading;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
@ -20,14 +19,14 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
/// </summary>
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());
}
@ -59,14 +58,14 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
return;
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()
{
base.Update();
marker.X = (float)adjustableClock.CurrentTime;
marker.X = (float)editorClock.CurrentTime;
}
protected override void LoadBeatmap(WorkingBeatmap beatmap)

View File

@ -6,7 +6,6 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Timing;
using osu.Game.Graphics;
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
{
[BackgroundDependencyLoader]
private void load(OsuColour colours, IAdjustableClock adjustableClock)
private void load(OsuColour colours)
{
Children = new Drawable[]
{
new MarkerPart(adjustableClock) { RelativeSizeAxes = Axes.Both },
new MarkerPart { RelativeSizeAxes = Axes.Both },
new ControlPointPart
{
Anchor = Anchor.Centre,

View File

@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Linq;
@ -14,7 +13,6 @@ using osu.Framework.Graphics.Primitives;
using osu.Framework.Input;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Timing;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
@ -29,8 +27,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// </summary>
public abstract class BlueprintContainer : CompositeDrawable, IKeyBindingHandler<PlatformAction>
{
public event Action<IEnumerable<HitObject>> SelectionChanged;
protected DragBox DragBox { get; private set; }
protected Container<SelectionBlueprint> SelectionBlueprints { get; private set; }
@ -41,13 +37,15 @@ namespace osu.Game.Screens.Edit.Compose.Components
private IEditorChangeHandler changeHandler { get; set; }
[Resolved]
private IAdjustableClock adjustableClock { get; set; }
private EditorClock editorClock { get; set; }
[Resolved]
private EditorBeatmap beatmap { get; set; }
private readonly BindableList<HitObject> selectedHitObjects = new BindableList<HitObject>();
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
[Resolved(canBeNull: true)]
private IPositionSnapProvider snapProvider { get; set; }
@ -88,8 +86,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
SelectionBlueprints.FirstOrDefault(b => b.HitObject == o)?.Deselect();
break;
}
SelectionChanged?.Invoke(selectedHitObjects);
};
}
@ -149,7 +145,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
if (clickedBlueprint == null)
return false;
adjustableClock?.Seek(clickedBlueprint.HitObject.StartTime);
editorClock?.SeekTo(clickedBlueprint.HitObject.StartTime);
return true;
}

View File

@ -74,9 +74,16 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// <summary>
/// Handles the selected <see cref="DrawableHitObject"/>s being moved.
/// </summary>
/// <remarks>
/// Just returning true is enough to allow <see cref="HitObject.StartTime"/> updates to take place.
/// Custom implementation is only required if other attributes are to be considered, like changing columns.
/// </remarks>
/// <param name="moveEvent">The move event.</param>
/// <returns>Whether any <see cref="DrawableHitObject"/>s were moved.</returns>
public virtual bool HandleMovement(MoveSelectionEvent moveEvent) => false;
/// <returns>
/// Whether any <see cref="DrawableHitObject"/>s could be moved.
/// Returning true will also propagate StartTime changes provided by the closest <see cref="IPositionSnapProvider.SnapScreenSpacePositionToValidTime"/>.
/// </returns>
public virtual bool HandleMovement(MoveSelectionEvent moveEvent) => true;
public bool OnPressed(PlatformAction action)
{
@ -244,14 +251,21 @@ namespace osu.Game.Screens.Edit.Compose.Components
#region Context Menu
public virtual MenuItem[] ContextMenuItems
public MenuItem[] ContextMenuItems
{
get
{
if (!selectedBlueprints.Any(b => b.IsHovered))
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")
{
@ -263,15 +277,20 @@ namespace osu.Game.Screens.Edit.Compose.Components
}
},
new OsuMenuItem("Delete", MenuItemType.Destructive, deleteSelected),
};
if (selectedBlueprints.Count == 1)
items.AddRange(selectedBlueprints[0].ContextMenuItems);
});
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)
{
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.Audio;
using osu.Framework.Input.Events;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Rulesets.Edit;
@ -25,7 +24,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
public readonly IBindable<WorkingBeatmap> Beatmap = new Bindable<WorkingBeatmap>();
[Resolved]
private IAdjustableClock adjustableClock { get; set; }
private EditorClock editorClock { get; set; }
public Timeline()
{
@ -101,7 +100,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
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
if (adjustableClock.IsRunning)
if (editorClock.IsRunning)
scrollToTrackTime();
}
@ -111,21 +110,21 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
if (handlingDragInput)
seekTrackToCurrent();
else if (!adjustableClock.IsRunning)
else if (!editorClock.IsRunning)
{
// 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
// 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
if (Current != lastScrollPosition && adjustableClock.CurrentTime == lastTrackTime)
if (Current != lastScrollPosition && editorClock.CurrentTime == lastTrackTime)
seekTrackToCurrent();
else
scrollToTrackTime();
}
lastScrollPosition = Current;
lastTrackTime = adjustableClock.CurrentTime;
lastTrackTime = editorClock.CurrentTime;
}
private void seekTrackToCurrent()
@ -133,7 +132,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
if (!track.IsLoaded)
return;
adjustableClock.Seek(Current / Content.DrawWidth * track.Length);
editorClock.Seek(Current / Content.DrawWidth * track.Length);
}
private void scrollToTrackTime()
@ -141,7 +140,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
if (!track.IsLoaded || track.Length == 0)
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)
@ -164,15 +163,15 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
private void beginUserDrag()
{
handlingDragInput = true;
trackWasPlaying = adjustableClock.IsRunning;
adjustableClock.Stop();
trackWasPlaying = editorClock.IsRunning;
editorClock.Stop();
}
private void endUserDrag()
{
handlingDragInput = false;
if (trackWasPlaying)
adjustableClock.Start();
editorClock.Start();
}
[Resolved]

View File

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

View File

@ -3,6 +3,8 @@
using System;
using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Transforms;
using osu.Framework.Utils;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
@ -13,7 +15,7 @@ namespace osu.Game.Screens.Edit
/// <summary>
/// A decoupled clock which adds editor-specific functionality, such as snapping to a user-defined beat divisor.
/// </summary>
public class EditorClock : DecoupleableInterpolatingFramedClock
public class EditorClock : Component, IFrameBasedClock, IAdjustableClock, ISourceChangeableClock
{
public readonly double TrackLength;
@ -21,12 +23,11 @@ namespace osu.Game.Screens.Edit
private readonly BindableBeatDivisor beatDivisor;
public EditorClock(WorkingBeatmap beatmap, BindableBeatDivisor beatDivisor)
{
this.beatDivisor = beatDivisor;
private readonly DecoupleableInterpolatingFramedClock underlyingClock;
ControlPointInfo = beatmap.Beatmap.ControlPointInfo;
TrackLength = beatmap.Track.Length;
public EditorClock(WorkingBeatmap beatmap, BindableBeatDivisor beatDivisor)
: this(beatmap.Beatmap.ControlPointInfo, beatmap.Track.Length, beatDivisor)
{
}
public EditorClock(ControlPointInfo controlPointInfo, double trackLength, BindableBeatDivisor beatDivisor)
@ -35,6 +36,13 @@ namespace osu.Game.Screens.Edit
ControlPointInfo = controlPointInfo;
TrackLength = trackLength;
underlyingClock = new DecoupleableInterpolatingFramedClock();
}
public EditorClock()
: this(new ControlPointInfo(), 1000, new BindableBeatDivisor())
{
}
/// <summary>
@ -79,20 +87,22 @@ namespace osu.Game.Screens.Edit
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));
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
timingPoint = ControlPointInfo.TimingPointAt(CurrentTime - 1);
timingPoint = ControlPointInfo.TimingPointAt(current - 1);
double seekAmount = timingPoint.BeatLength / beatDivisor.Value * amount;
double seekTime = CurrentTime + seekAmount * direction;
double seekTime = current + seekAmount * direction;
if (!snapped || ControlPointInfo.TimingPoints.Count == 0)
{
Seek(seekTime);
SeekTo(seekTime);
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.
// 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;
seekTime = timingPoint.Time + closestBeat * seekAmount;
@ -125,7 +135,97 @@ namespace osu.Game.Screens.Edit
// Ensure the sought point is within the boundaries
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.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics;
@ -23,7 +22,7 @@ namespace osu.Game.Screens.Edit.Timing
private Bindable<ControlPointGroup> selectedGroup = new Bindable<ControlPointGroup>();
[Resolved]
private IAdjustableClock clock { get; set; }
private EditorClock clock { get; set; }
protected override Drawable CreateMainContent() => new GridContainer
{
@ -50,7 +49,7 @@ namespace osu.Game.Screens.Edit.Timing
selectedGroup.BindValueChanged(selected =>
{
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;
[Resolved]
private IFrameBasedClock clock { get; set; }
private EditorClock clock { get; set; }
[Resolved]
protected IBindable<WorkingBeatmap> Beatmap { get; private set; }

View File

@ -32,14 +32,14 @@ namespace osu.Game.Screens.Multi.Match.Components
Text = "Start";
}
private IBindable<WeakReference<BeatmapSetInfo>> managerAdded;
private IBindable<WeakReference<BeatmapSetInfo>> managerUpdated;
private IBindable<WeakReference<BeatmapSetInfo>> managerRemoved;
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
managerAdded = beatmaps.ItemAdded.GetBoundCopy();
managerAdded.BindValueChanged(beatmapAdded);
managerUpdated = beatmaps.ItemUpdated.GetBoundCopy();
managerUpdated.BindValueChanged(beatmapUpdated);
managerRemoved = beatmaps.ItemRemoved.GetBoundCopy();
managerRemoved.BindValueChanged(beatmapRemoved);
@ -61,7 +61,7 @@ namespace osu.Game.Screens.Multi.Match.Components
hasBeatmap = beatmaps.QueryBeatmap(b => b.OnlineBeatmapID == beatmapId) != null;
}
private void beatmapAdded(ValueChangedEvent<WeakReference<BeatmapSetInfo>> weakSet)
private void beatmapUpdated(ValueChangedEvent<WeakReference<BeatmapSetInfo>> weakSet)
{
if (weakSet.NewValue.TryGetTarget(out var set))
{

View File

@ -50,7 +50,7 @@ namespace osu.Game.Screens.Multi.Match
private LeaderboardChatDisplay leaderboardChatDisplay;
private MatchSettingsOverlay settingsOverlay;
private IBindable<WeakReference<BeatmapSetInfo>> managerAdded;
private IBindable<WeakReference<BeatmapSetInfo>> managerUpdated;
public MatchSubScreen(Room room)
{
@ -183,8 +183,8 @@ namespace osu.Game.Screens.Multi.Match
SelectedItem.BindValueChanged(_ => Scheduler.AddOnce(selectedItemChanged));
SelectedItem.Value = playlist.FirstOrDefault();
managerAdded = beatmapManager.ItemAdded.GetBoundCopy();
managerAdded.BindValueChanged(beatmapAdded);
managerUpdated = beatmapManager.ItemUpdated.GetBoundCopy();
managerUpdated.BindValueChanged(beatmapUpdated);
}
public override bool OnExiting(IScreen next)
@ -217,7 +217,7 @@ namespace osu.Game.Screens.Multi.Match
Beatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap);
}
private void beatmapAdded(ValueChangedEvent<WeakReference<BeatmapSetInfo>> weakSet)
private void beatmapUpdated(ValueChangedEvent<WeakReference<BeatmapSetInfo>> weakSet)
{
Schedule(() =>
{

View File

@ -131,7 +131,7 @@ namespace osu.Game.Screens.Select
private CarouselRoot root;
private IBindable<WeakReference<BeatmapSetInfo>> itemAdded;
private IBindable<WeakReference<BeatmapSetInfo>> itemUpdated;
private IBindable<WeakReference<BeatmapSetInfo>> itemRemoved;
private IBindable<WeakReference<BeatmapInfo>> itemHidden;
private IBindable<WeakReference<BeatmapInfo>> itemRestored;
@ -166,8 +166,8 @@ namespace osu.Game.Screens.Select
RightClickScrollingEnabled.ValueChanged += enabled => scroll.RightMouseScrollbar = enabled.NewValue;
RightClickScrollingEnabled.TriggerChange();
itemAdded = beatmaps.ItemAdded.GetBoundCopy();
itemAdded.BindValueChanged(beatmapAdded);
itemUpdated = beatmaps.ItemUpdated.GetBoundCopy();
itemUpdated.BindValueChanged(beatmapUpdated);
itemRemoved = beatmaps.ItemRemoved.GetBoundCopy();
itemRemoved.BindValueChanged(beatmapRemoved);
itemHidden = beatmaps.BeatmapHidden.GetBoundCopy();
@ -582,7 +582,7 @@ namespace osu.Game.Screens.Select
RemoveBeatmapSet(item);
}
private void beatmapAdded(ValueChangedEvent<WeakReference<BeatmapSetInfo>> weakItem)
private void beatmapUpdated(ValueChangedEvent<WeakReference<BeatmapSetInfo>> weakItem)
{
if (weakItem.NewValue.TryGetTarget(out var item))
UpdateBeatmapSet(item);

View File

@ -28,7 +28,7 @@ namespace osu.Game.Screens.Select.Carousel
[Resolved]
private IAPIProvider api { get; set; }
private IBindable<WeakReference<ScoreInfo>> itemAdded;
private IBindable<WeakReference<ScoreInfo>> itemUpdated;
private IBindable<WeakReference<ScoreInfo>> itemRemoved;
public TopLocalRank(BeatmapInfo beatmap)
@ -40,8 +40,8 @@ namespace osu.Game.Screens.Select.Carousel
[BackgroundDependencyLoader]
private void load()
{
itemAdded = scores.ItemAdded.GetBoundCopy();
itemAdded.BindValueChanged(scoreChanged);
itemUpdated = scores.ItemUpdated.GetBoundCopy();
itemUpdated.BindValueChanged(scoreChanged);
itemRemoved = scores.ItemRemoved.GetBoundCopy();
itemRemoved.BindValueChanged(scoreChanged);

View File

@ -24,8 +24,6 @@ namespace osu.Game.Skinning
public bool DeletePending { get; set; }
public string FullName => $"\"{Name}\" by {Creator}";
public static SkinInfo Default { get; } = new SkinInfo
{
Name = "osu!lazer",
@ -34,6 +32,10 @@ namespace osu.Game.Skinning
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));
dependencies.Cache(BeatDivisor);
dependencies.CacheAs<IFrameBasedClock>(Clock);
dependencies.CacheAs<IAdjustableClock>(Clock);
dependencies.CacheAs(Clock);
return dependencies;
}

View File

@ -8,6 +8,7 @@ using osu.Framework.Timing;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Compose;
namespace osu.Game.Tests.Visual
@ -32,7 +33,7 @@ namespace osu.Game.Tests.Visual
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
dependencies.CacheAs<IAdjustableClock>(new StopwatchClock());
dependencies.CacheAs(new EditorClock());
return dependencies;
}

View File

@ -19,15 +19,15 @@
</ItemGroup>
<ItemGroup Label="Package References">
<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="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="ppy.osu.Framework" Version="2020.519.0" />
<PackageReference Include="ppy.osu.Framework" Version="2020.528.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.512.0" />
<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="System.ComponentModel.Annotations" Version="4.7.0" />
</ItemGroup>

View File

@ -70,18 +70,18 @@
<Reference Include="System.Net.Http" />
</ItemGroup>
<ItemGroup Label="Package References">
<PackageReference Include="ppy.osu.Framework.iOS" Version="2020.519.0" />
<PackageReference Include="ppy.osu.Framework.iOS" Version="2020.528.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.512.0" />
</ItemGroup>
<!-- Xamarin.iOS does not automatically handle transitive dependencies from NuGet packages. -->
<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="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="ppy.osu.Framework" Version="2020.519.0" />
<PackageReference Include="SharpCompress" Version="0.25.0" />
<PackageReference Include="ppy.osu.Framework" Version="2020.528.0" />
<PackageReference Include="SharpCompress" Version="0.25.1" />
<PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="SharpRaven" Version="2.4.0" />
<PackageReference Include="System.ComponentModel.Annotations" Version="4.7.0" />