diff --git a/osu.Android.props b/osu.Android.props
index 85766665a9..c5714caf4c 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -62,6 +62,6 @@
-
+
diff --git a/osu.Android/OsuGameActivity.cs b/osu.Android/OsuGameActivity.cs
index 762a9c418d..2e5fa59d20 100644
--- a/osu.Android/OsuGameActivity.cs
+++ b/osu.Android/OsuGameActivity.cs
@@ -16,6 +16,11 @@ namespace osu.Android
protected override void OnCreate(Bundle savedInstanceState)
{
+ // The default current directory on android is '/'.
+ // On some devices '/' maps to the app data directory. On others it maps to the root of the internal storage.
+ // In order to have a consistent current directory on all devices the full path of the app data directory is set as the current directory.
+ System.Environment.CurrentDirectory = System.Environment.GetFolderPath(System.Environment.SpecialFolder.Personal);
+
base.OnCreate(savedInstanceState);
Window.AddFlags(WindowManagerFlags.Fullscreen);
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableBananaShower.cs b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableBananaShower.cs
index 42646851d7..ea415e18fa 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableBananaShower.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableBananaShower.cs
@@ -2,35 +2,50 @@
// See the LICENCE file in the repository root for full licence text.
using System;
-using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
namespace osu.Game.Rulesets.Catch.Objects.Drawable
{
public class DrawableBananaShower : DrawableCatchHitObject
{
+ private readonly Func> createDrawableRepresentation;
private readonly Container bananaContainer;
public DrawableBananaShower(BananaShower s, Func> createDrawableRepresentation = null)
: base(s)
{
+ this.createDrawableRepresentation = createDrawableRepresentation;
RelativeSizeAxes = Axes.X;
Origin = Anchor.BottomLeft;
X = 0;
AddInternal(bananaContainer = new Container { RelativeSizeAxes = Axes.Both });
-
- foreach (var b in s.NestedHitObjects.Cast())
- AddNested(createDrawableRepresentation?.Invoke(b));
}
- protected override void AddNested(DrawableHitObject h)
+ protected override void AddNestedHitObject(DrawableHitObject hitObject)
{
- ((DrawableCatchHitObject)h).CheckPosition = o => CheckPosition?.Invoke(o) ?? false;
- bananaContainer.Add(h);
- base.AddNested(h);
+ base.AddNestedHitObject(hitObject);
+ bananaContainer.Add(hitObject);
+ }
+
+ protected override void ClearNestedHitObjects()
+ {
+ base.ClearNestedHitObjects();
+ bananaContainer.Clear();
+ }
+
+ protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject)
+ {
+ switch (hitObject)
+ {
+ case Banana banana:
+ return createDrawableRepresentation?.Invoke(banana)?.With(o => ((DrawableCatchHitObject)o).CheckPosition = p => CheckPosition?.Invoke(p) ?? false);
+ }
+
+ return base.CreateNestedHitObject(hitObject);
}
}
}
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableJuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableJuiceStream.cs
index 9e5e9f6a04..a24821b3ce 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableJuiceStream.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableJuiceStream.cs
@@ -2,38 +2,50 @@
// See the LICENCE file in the repository root for full licence text.
using System;
-using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
namespace osu.Game.Rulesets.Catch.Objects.Drawable
{
public class DrawableJuiceStream : DrawableCatchHitObject
{
+ private readonly Func> createDrawableRepresentation;
private readonly Container dropletContainer;
public DrawableJuiceStream(JuiceStream s, Func> createDrawableRepresentation = null)
: base(s)
{
+ this.createDrawableRepresentation = createDrawableRepresentation;
RelativeSizeAxes = Axes.Both;
Origin = Anchor.BottomLeft;
X = 0;
AddInternal(dropletContainer = new Container { RelativeSizeAxes = Axes.Both, });
-
- foreach (var o in s.NestedHitObjects.Cast())
- AddNested(createDrawableRepresentation?.Invoke(o));
}
- protected override void AddNested(DrawableHitObject h)
+ protected override void AddNestedHitObject(DrawableHitObject hitObject)
{
- var catchObject = (DrawableCatchHitObject)h;
+ base.AddNestedHitObject(hitObject);
+ dropletContainer.Add(hitObject);
+ }
- catchObject.CheckPosition = o => CheckPosition?.Invoke(o) ?? false;
+ protected override void ClearNestedHitObjects()
+ {
+ base.ClearNestedHitObjects();
+ dropletContainer.Clear();
+ }
- dropletContainer.Add(h);
- base.AddNested(h);
+ protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject)
+ {
+ switch (hitObject)
+ {
+ case CatchHitObject catchObject:
+ return createDrawableRepresentation?.Invoke(catchObject)?.With(o => ((DrawableCatchHitObject)o).CheckPosition = p => CheckPosition?.Invoke(p) ?? false);
+ }
+
+ return base.CreateNestedHitObject(hitObject);
}
}
}
diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs
index d64c5dbc6a..3a9eb1f043 100644
--- a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs
@@ -16,45 +16,51 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
{
public class HoldNoteSelectionBlueprint : ManiaSelectionBlueprint
{
- public new DrawableHoldNote HitObject => (DrawableHoldNote)base.HitObject;
+ public new DrawableHoldNote DrawableObject => (DrawableHoldNote)base.DrawableObject;
private readonly IBindable direction = new Bindable();
- private readonly BodyPiece body;
+ [Resolved]
+ private OsuColour colours { get; set; }
public HoldNoteSelectionBlueprint(DrawableHoldNote hold)
: base(hold)
{
- InternalChildren = new Drawable[]
- {
- new HoldNoteNoteSelectionBlueprint(hold.Head),
- new HoldNoteNoteSelectionBlueprint(hold.Tail),
- body = new BodyPiece
- {
- AccentColour = Color4.Transparent
- },
- };
}
[BackgroundDependencyLoader]
- private void load(OsuColour colours, IScrollingInfo scrollingInfo)
+ private void load(IScrollingInfo scrollingInfo)
{
- body.BorderColour = colours.Yellow;
-
direction.BindTo(scrollingInfo.Direction);
}
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ InternalChildren = new Drawable[]
+ {
+ new HoldNoteNoteSelectionBlueprint(DrawableObject.Head),
+ new HoldNoteNoteSelectionBlueprint(DrawableObject.Tail),
+ new BodyPiece
+ {
+ AccentColour = Color4.Transparent,
+ BorderColour = colours.Yellow
+ },
+ };
+ }
+
protected override void Update()
{
base.Update();
- Size = HitObject.DrawSize + new Vector2(0, HitObject.Tail.DrawHeight);
+ Size = DrawableObject.DrawSize + new Vector2(0, DrawableObject.Tail.DrawHeight);
// This is a side-effect of not matching the hitobject's anchors/origins, which is kinda hard to do
// When scrolling upwards our origin is already at the top of the head note (which is the intended location),
// but when scrolling downwards our origin is at the _bottom_ of the tail note (where we need to be at the _top_ of the tail note)
if (direction.Value == ScrollingDirection.Down)
- Y -= HitObject.Tail.DrawHeight;
+ Y -= DrawableObject.Tail.DrawHeight;
}
public override Quad SelectionQuad => ScreenSpaceDrawQuad;
@@ -71,10 +77,10 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
{
base.Update();
- Anchor = HitObject.Anchor;
- Origin = HitObject.Origin;
+ Anchor = DrawableObject.Anchor;
+ Origin = DrawableObject.Origin;
- Position = HitObject.DrawPosition;
+ Position = DrawableObject.DrawPosition;
}
// Todo: This is temporary, since the note masks don't do anything special yet. In the future they will handle input.
diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs
index 3142f22fcd..b28d8bb0e6 100644
--- a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs
@@ -49,10 +49,8 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
if (Column == null)
return base.OnMouseDown(e);
- HitObject.StartTime = TimeAt(e.ScreenSpaceMousePosition);
HitObject.Column = Column.Index;
-
- BeginPlacement();
+ BeginPlacement(TimeAt(e.ScreenSpaceMousePosition));
return true;
}
diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs
index cc50459a0c..3bd7fb2d49 100644
--- a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs
@@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
public Vector2 ScreenSpaceDragPosition { get; private set; }
public Vector2 DragPosition { get; private set; }
- public new DrawableManiaHitObject HitObject => (DrawableManiaHitObject)base.HitObject;
+ public new DrawableManiaHitObject DrawableObject => (DrawableManiaHitObject)base.DrawableObject;
protected IClock EditorClock { get; private set; }
@@ -28,8 +28,8 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
[Resolved]
private IManiaHitObjectComposer composer { get; set; }
- public ManiaSelectionBlueprint(DrawableHitObject hitObject)
- : base(hitObject)
+ public ManiaSelectionBlueprint(DrawableHitObject drawableObject)
+ : base(drawableObject)
{
RelativeSizeAxes = Axes.None;
}
@@ -44,13 +44,13 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
{
base.Update();
- Position = Parent.ToLocalSpace(HitObject.ToScreenSpace(Vector2.Zero));
+ Position = Parent.ToLocalSpace(DrawableObject.ToScreenSpace(Vector2.Zero));
}
protected override bool OnMouseDown(MouseDownEvent e)
{
ScreenSpaceDragPosition = e.ScreenSpaceMousePosition;
- DragPosition = HitObject.ToLocalSpace(e.ScreenSpaceMousePosition);
+ DragPosition = DrawableObject.ToLocalSpace(e.ScreenSpaceMousePosition);
return base.OnMouseDown(e);
}
@@ -60,20 +60,20 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
var result = base.OnDrag(e);
ScreenSpaceDragPosition = e.ScreenSpaceMousePosition;
- DragPosition = HitObject.ToLocalSpace(e.ScreenSpaceMousePosition);
+ DragPosition = DrawableObject.ToLocalSpace(e.ScreenSpaceMousePosition);
return result;
}
public override void Show()
{
- HitObject.AlwaysAlive = true;
+ DrawableObject.AlwaysAlive = true;
base.Show();
}
public override void Hide()
{
- HitObject.AlwaysAlive = false;
+ DrawableObject.AlwaysAlive = false;
base.Hide();
}
}
diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/NoteSelectionBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/NoteSelectionBlueprint.cs
index d345b14e84..b83c4aa9aa 100644
--- a/osu.Game.Rulesets.Mania/Edit/Blueprints/NoteSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/NoteSelectionBlueprint.cs
@@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
{
base.Update();
- Size = HitObject.DrawSize;
+ Size = DrawableObject.DrawSize;
}
}
}
diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs b/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs
index f576c43e52..732231b0d9 100644
--- a/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs
+++ b/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs
@@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Mania.Edit
public override void HandleMovement(MoveSelectionEvent moveEvent)
{
var maniaBlueprint = (ManiaSelectionBlueprint)moveEvent.Blueprint;
- int lastColumn = maniaBlueprint.HitObject.HitObject.Column;
+ int lastColumn = maniaBlueprint.DrawableObject.HitObject.Column;
adjustOrigins(maniaBlueprint);
performDragMovement(moveEvent);
@@ -48,41 +48,44 @@ namespace osu.Game.Rulesets.Mania.Edit
/// The that received the drag event.
private void adjustOrigins(ManiaSelectionBlueprint reference)
{
- var referenceParent = (HitObjectContainer)reference.HitObject.Parent;
+ var referenceParent = (HitObjectContainer)reference.DrawableObject.Parent;
- float offsetFromReferenceOrigin = reference.DragPosition.Y - reference.HitObject.OriginPosition.Y;
+ float offsetFromReferenceOrigin = reference.DragPosition.Y - reference.DrawableObject.OriginPosition.Y;
float targetPosition = referenceParent.ToLocalSpace(reference.ScreenSpaceDragPosition).Y - offsetFromReferenceOrigin;
// Flip the vertical coordinate space when scrolling downwards
if (scrollingInfo.Direction.Value == ScrollingDirection.Down)
targetPosition = targetPosition - referenceParent.DrawHeight;
- float movementDelta = targetPosition - reference.HitObject.Position.Y;
+ float movementDelta = targetPosition - reference.DrawableObject.Position.Y;
foreach (var b in SelectedBlueprints.OfType())
- b.HitObject.Y += movementDelta;
+ b.DrawableObject.Y += movementDelta;
}
private void performDragMovement(MoveSelectionEvent moveEvent)
{
+ float delta = moveEvent.InstantDelta.Y;
+
+ // When scrolling downwards the anchor position is at the bottom of the screen, however the movement event assumes the anchor is at the top of the screen.
+ // This causes the delta to assume a positive hitobject position, and which can be corrected for by subtracting the parent height.
+ if (scrollingInfo.Direction.Value == ScrollingDirection.Down)
+ delta -= moveEvent.Blueprint.DrawableObject.Parent.DrawHeight;
+
foreach (var b in SelectedBlueprints)
{
- var hitObject = b.HitObject;
-
+ var hitObject = b.DrawableObject;
var objectParent = (HitObjectContainer)hitObject.Parent;
- // Using the hitobject position is required since AdjustPosition can be invoked multiple times per frame
- // without the position having been updated by the parenting ScrollingHitObjectContainer
- hitObject.Y += moveEvent.InstantDelta.Y;
+ // StartTime could be used to adjust the position if only one movement event was received per frame.
+ // However this is not the case and ScrollingHitObjectContainer performs movement in UpdateAfterChildren() so the position must also be updated to be valid for further movement events
+ hitObject.Y += delta;
- float targetPosition;
+ float targetPosition = hitObject.Position.Y;
- // If we're scrolling downwards, a position of 0 is actually further away from the hit target
- // so we need to flip the vertical coordinate in the hitobject container's space
+ // The scrolling algorithm always assumes an anchor at the top of the screen, so the position must be flipped when scrolling downwards to reflect a top anchor
if (scrollingInfo.Direction.Value == ScrollingDirection.Down)
- targetPosition = -hitObject.Position.Y;
- else
- targetPosition = hitObject.Position.Y;
+ targetPosition = -targetPosition;
objectParent.Remove(hitObject);
diff --git a/osu.Game.Rulesets.Mania/Edit/Masks/ManiaSelectionBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Masks/ManiaSelectionBlueprint.cs
index 30b0f09a94..ff8882124f 100644
--- a/osu.Game.Rulesets.Mania/Edit/Masks/ManiaSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Mania/Edit/Masks/ManiaSelectionBlueprint.cs
@@ -9,8 +9,8 @@ namespace osu.Game.Rulesets.Mania.Edit.Masks
{
public abstract class ManiaSelectionBlueprint : SelectionBlueprint
{
- protected ManiaSelectionBlueprint(DrawableHitObject hitObject)
- : base(hitObject)
+ protected ManiaSelectionBlueprint(DrawableHitObject drawableObject)
+ : base(drawableObject)
{
RelativeSizeAxes = Axes.None;
}
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
index c5c157608f..87b9633c80 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
@@ -2,13 +2,12 @@
// See the LICENCE file in the repository root for full licence text.
using System.Diagnostics;
-using System.Linq;
using osu.Framework.Bindables;
-using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Bindings;
+using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI.Scrolling;
@@ -22,8 +21,12 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
{
public override bool DisplayResult => false;
- public readonly DrawableNote Head;
- public readonly DrawableNote Tail;
+ public DrawableNote Head => headContainer.Child;
+ public DrawableNote Tail => tailContainer.Child;
+
+ private readonly Container headContainer;
+ private readonly Container tailContainer;
+ private readonly Container tickContainer;
private readonly BodyPiece bodyPiece;
@@ -40,50 +43,81 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
public DrawableHoldNote(HoldNote hitObject)
: base(hitObject)
{
- Container tickContainer;
RelativeSizeAxes = Axes.X;
AddRangeInternal(new Drawable[]
{
- bodyPiece = new BodyPiece
- {
- RelativeSizeAxes = Axes.X,
- },
- tickContainer = new Container
- {
- RelativeSizeAxes = Axes.Both,
- ChildrenEnumerable = HitObject.NestedHitObjects.OfType().Select(tick => new DrawableHoldNoteTick(tick)
- {
- HoldStartTime = () => holdStartTime
- })
- },
- Head = new DrawableHeadNote(this)
- {
- Anchor = Anchor.TopCentre,
- Origin = Anchor.TopCentre
- },
- Tail = new DrawableTailNote(this)
- {
- Anchor = Anchor.TopCentre,
- Origin = Anchor.TopCentre
- }
+ bodyPiece = new BodyPiece { RelativeSizeAxes = Axes.X },
+ tickContainer = new Container { RelativeSizeAxes = Axes.Both },
+ headContainer = new Container { RelativeSizeAxes = Axes.Both },
+ tailContainer = new Container { RelativeSizeAxes = Axes.Both },
});
- foreach (var tick in tickContainer)
- AddNested(tick);
-
- AddNested(Head);
- AddNested(Tail);
-
AccentColour.BindValueChanged(colour =>
{
bodyPiece.AccentColour = colour.NewValue;
- Head.AccentColour.Value = colour.NewValue;
- Tail.AccentColour.Value = colour.NewValue;
- tickContainer.ForEach(t => t.AccentColour.Value = colour.NewValue);
}, true);
}
+ protected override void AddNestedHitObject(DrawableHitObject hitObject)
+ {
+ base.AddNestedHitObject(hitObject);
+
+ switch (hitObject)
+ {
+ case DrawableHeadNote head:
+ headContainer.Child = head;
+ break;
+
+ case DrawableTailNote tail:
+ tailContainer.Child = tail;
+ break;
+
+ case DrawableHoldNoteTick tick:
+ tickContainer.Add(tick);
+ break;
+ }
+ }
+
+ protected override void ClearNestedHitObjects()
+ {
+ base.ClearNestedHitObjects();
+ headContainer.Clear();
+ tailContainer.Clear();
+ tickContainer.Clear();
+ }
+
+ protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject)
+ {
+ switch (hitObject)
+ {
+ case TailNote _:
+ return new DrawableTailNote(this)
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ AccentColour = { BindTarget = AccentColour }
+ };
+
+ case Note _:
+ return new DrawableHeadNote(this)
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ AccentColour = { BindTarget = AccentColour }
+ };
+
+ case HoldNoteTick tick:
+ return new DrawableHoldNoteTick(tick)
+ {
+ HoldStartTime = () => holdStartTime,
+ AccentColour = { BindTarget = AccentColour }
+ };
+ }
+
+ return base.CreateNestedHitObject(hitObject);
+ }
+
protected override void OnDirectionChanged(ValueChangedEvent e)
{
base.OnDirectionChanged(e);
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleSelectionBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleSelectionBlueprint.cs
index d4cdabdb07..0ecce42e88 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleSelectionBlueprint.cs
@@ -52,12 +52,19 @@ namespace osu.Game.Rulesets.Osu.Tests
AddAssert("blueprint positioned over hitobject", () => blueprint.CirclePiece.Position == hitCircle.Position);
}
+ [Test]
+ public void TestStackedHitObject()
+ {
+ AddStep("set stacking", () => hitCircle.StackHeight = 5);
+ AddAssert("blueprint positioned over hitobject", () => blueprint.CirclePiece.Position == hitCircle.StackedPosition);
+ }
+
private class TestBlueprint : HitCircleSelectionBlueprint
{
public new HitCirclePiece CirclePiece => base.CirclePiece;
- public TestBlueprint(DrawableHitCircle hitCircle)
- : base(hitCircle)
+ public TestBlueprint(DrawableHitCircle drawableCircle)
+ : base(drawableCircle)
{
}
}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuDistanceSnapGrid.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuDistanceSnapGrid.cs
new file mode 100644
index 0000000000..da7708081b
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuDistanceSnapGrid.cs
@@ -0,0 +1,210 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Input.Events;
+using osu.Framework.MathUtils;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Rulesets.Osu.Beatmaps;
+using osu.Game.Rulesets.Osu.Edit;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Screens.Edit;
+using osu.Game.Tests.Visual;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Osu.Tests
+{
+ public class TestSceneOsuDistanceSnapGrid : ManualInputManagerTestScene
+ {
+ private const double beat_length = 100;
+ private static readonly Vector2 grid_position = new Vector2(512, 384);
+
+ [Cached(typeof(IEditorBeatmap))]
+ private readonly EditorBeatmap editorBeatmap;
+
+ [Cached]
+ private readonly BindableBeatDivisor beatDivisor = new BindableBeatDivisor();
+
+ private TestOsuDistanceSnapGrid grid;
+
+ public TestSceneOsuDistanceSnapGrid()
+ {
+ editorBeatmap = new EditorBeatmap(new OsuBeatmap());
+
+ createGrid();
+ }
+
+ [SetUp]
+ public void Setup() => Schedule(() =>
+ {
+ Clear();
+
+ editorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = 1;
+ editorBeatmap.ControlPointInfo.DifficultyPoints.Clear();
+ editorBeatmap.ControlPointInfo.TimingPoints.Clear();
+ editorBeatmap.ControlPointInfo.TimingPoints.Add(new TimingControlPoint { BeatLength = beat_length });
+
+ beatDivisor.Value = 1;
+ });
+
+ [TestCase(1)]
+ [TestCase(2)]
+ [TestCase(3)]
+ [TestCase(4)]
+ [TestCase(6)]
+ [TestCase(8)]
+ [TestCase(12)]
+ [TestCase(16)]
+ public void TestBeatDivisor(int divisor)
+ {
+ AddStep($"set beat divisor = {divisor}", () => beatDivisor.Value = divisor);
+ createGrid();
+ }
+
+ [TestCase(100, 100)]
+ [TestCase(200, 100)]
+ public void TestBeatLength(float beatLength, float expectedSpacing)
+ {
+ AddStep($"set beat length = {beatLength}", () =>
+ {
+ editorBeatmap.ControlPointInfo.TimingPoints.Clear();
+ editorBeatmap.ControlPointInfo.TimingPoints.Add(new TimingControlPoint { BeatLength = beatLength });
+ });
+
+ createGrid();
+ AddAssert($"spacing = {expectedSpacing}", () => Precision.AlmostEquals(expectedSpacing, grid.DistanceSpacing));
+ }
+
+ [TestCase(0.5f, 50)]
+ [TestCase(1, 100)]
+ [TestCase(1.5f, 150)]
+ public void TestSpeedMultiplier(float multiplier, float expectedSpacing)
+ {
+ AddStep($"set speed multiplier = {multiplier}", () =>
+ {
+ editorBeatmap.ControlPointInfo.DifficultyPoints.Clear();
+ editorBeatmap.ControlPointInfo.DifficultyPoints.Add(new DifficultyControlPoint { SpeedMultiplier = multiplier });
+ });
+
+ createGrid();
+ AddAssert($"spacing = {expectedSpacing}", () => Precision.AlmostEquals(expectedSpacing, grid.DistanceSpacing));
+ }
+
+ [TestCase(0.5f, 50)]
+ [TestCase(1, 100)]
+ [TestCase(1.5f, 150)]
+ public void TestSliderMultiplier(float multiplier, float expectedSpacing)
+ {
+ AddStep($"set speed multiplier = {multiplier}", () => editorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = multiplier);
+ createGrid();
+ AddAssert($"spacing = {expectedSpacing}", () => Precision.AlmostEquals(expectedSpacing, grid.DistanceSpacing));
+ }
+
+ [Test]
+ public void TestCursorInCentre()
+ {
+ createGrid();
+
+ AddStep("move mouse to centre", () => InputManager.MoveMouseTo(grid.ToScreenSpace(grid_position)));
+ assertSnappedDistance((float)beat_length);
+ }
+
+ [Test]
+ public void TestCursorBeforeMovementPoint()
+ {
+ createGrid();
+
+ AddStep("move mouse to just before movement point", () => InputManager.MoveMouseTo(grid.ToScreenSpace(grid_position + new Vector2((float)beat_length, 0) * 1.49f)));
+ assertSnappedDistance((float)beat_length);
+ }
+
+ [Test]
+ public void TestCursorAfterMovementPoint()
+ {
+ createGrid();
+
+ AddStep("move mouse to just after movement point", () => InputManager.MoveMouseTo(grid.ToScreenSpace(grid_position + new Vector2((float)beat_length, 0) * 1.51f)));
+ assertSnappedDistance((float)beat_length * 2);
+ }
+
+ private void assertSnappedDistance(float expectedDistance) => AddAssert($"snap distance = {expectedDistance}", () =>
+ {
+ Vector2 snappedPosition = grid.GetSnapPosition(grid.ToLocalSpace(InputManager.CurrentState.Mouse.Position));
+ float distance = Vector2.Distance(snappedPosition, grid_position);
+
+ return Precision.AlmostEquals(expectedDistance, distance);
+ });
+
+ private void createGrid()
+ {
+ AddStep("create grid", () =>
+ {
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = Color4.SlateGray
+ },
+ grid = new TestOsuDistanceSnapGrid(new HitCircle { Position = grid_position }),
+ new SnappingCursorContainer { GetSnapPosition = v => grid.GetSnapPosition(grid.ToLocalSpace(v)) }
+ };
+ });
+ }
+
+ private class SnappingCursorContainer : CompositeDrawable
+ {
+ public Func GetSnapPosition;
+
+ private readonly Drawable cursor;
+
+ public SnappingCursorContainer()
+ {
+ RelativeSizeAxes = Axes.Both;
+
+ InternalChild = cursor = new Circle
+ {
+ Origin = Anchor.Centre,
+ Size = new Vector2(50),
+ Colour = Color4.Red
+ };
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ updatePosition(GetContainingInputManager().CurrentState.Mouse.Position);
+ }
+
+ protected override bool OnMouseMove(MouseMoveEvent e)
+ {
+ base.OnMouseMove(e);
+
+ updatePosition(e.ScreenSpaceMousePosition);
+ return true;
+ }
+
+ private void updatePosition(Vector2 screenSpacePosition)
+ {
+ cursor.Position = GetSnapPosition.Invoke(screenSpacePosition);
+ }
+ }
+
+ private class TestOsuDistanceSnapGrid : OsuDistanceSnapGrid
+ {
+ public new float DistanceSpacing => base.DistanceSpacing;
+
+ public TestOsuDistanceSnapGrid(OsuHitObject hitObject)
+ : base(hitObject)
+ {
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/BlueprintPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/BlueprintPiece.cs
index 95e926fdfa..b9c77d3f56 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/BlueprintPiece.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/BlueprintPiece.cs
@@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints
/// The to reference properties from.
public virtual void UpdateFrom(T hitObject)
{
- Position = hitObject.Position;
+ Position = hitObject.StackedPosition;
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs
index 6c08990ad6..bb47c7e464 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs
@@ -30,7 +30,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles
protected override bool OnClick(ClickEvent e)
{
- HitObject.StartTime = EditorClock.CurrentTime;
EndPlacement();
return true;
}
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCircleSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCircleSelectionBlueprint.cs
index a191dba8ff..093bae854e 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCircleSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCircleSelectionBlueprint.cs
@@ -1,18 +1,22 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using osu.Framework.Graphics.Primitives;
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
+using osuTK;
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles
{
public class HitCircleSelectionBlueprint : OsuSelectionBlueprint
{
+ protected new DrawableHitCircle DrawableObject => (DrawableHitCircle)base.DrawableObject;
+
protected readonly HitCirclePiece CirclePiece;
- public HitCircleSelectionBlueprint(DrawableHitCircle hitCircle)
- : base(hitCircle)
+ public HitCircleSelectionBlueprint(DrawableHitCircle drawableCircle)
+ : base(drawableCircle)
{
InternalChild = CirclePiece = new HitCirclePiece();
}
@@ -23,5 +27,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles
CirclePiece.UpdateFrom(HitObject);
}
+
+ public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => DrawableObject.HitArea.ReceivePositionalInputAt(screenSpacePos);
+
+ public override Quad SelectionQuad => DrawableObject.HitArea.ScreenSpaceDrawQuad;
}
}
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/OsuSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/OsuSelectionBlueprint.cs
index 2e4b990db8..a864257274 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/OsuSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/OsuSelectionBlueprint.cs
@@ -10,10 +10,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints
public abstract class OsuSelectionBlueprint : SelectionBlueprint
where T : OsuHitObject
{
- protected new T HitObject => (T)base.HitObject.HitObject;
+ protected T HitObject => (T)DrawableObject.HitObject;
- protected OsuSelectionBlueprint(DrawableHitObject hitObject)
- : base(hitObject)
+ protected OsuSelectionBlueprint(DrawableHitObject drawableObject)
+ : base(drawableObject)
{
}
}
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs
index fc074ef8af..2fb18bf8ba 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs
@@ -104,8 +104,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private void beginCurve()
{
BeginPlacement();
-
- HitObject.StartTime = EditorClock.CurrentTime;
setState(PlacementState.Body);
}
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerPlacementBlueprint.cs
index 8319f49cbc..5525b8936e 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerPlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerPlacementBlueprint.cs
@@ -41,8 +41,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners
}
else
{
- HitObject.StartTime = EditorClock.CurrentTime;
-
isPlacingEnd = true;
piece.FadeTo(1f, 150, Easing.OutQuint);
diff --git a/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapGrid.cs b/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapGrid.cs
new file mode 100644
index 0000000000..bc0f76f000
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapGrid.cs
@@ -0,0 +1,29 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Screens.Edit.Compose.Components;
+
+namespace osu.Game.Rulesets.Osu.Edit
+{
+ public class OsuDistanceSnapGrid : CircularDistanceSnapGrid
+ {
+ public OsuDistanceSnapGrid(OsuHitObject hitObject)
+ : base(hitObject, hitObject.StackedEndPosition)
+ {
+ Masking = true;
+ }
+
+ protected override float GetVelocity(double time, ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
+ {
+ TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(time);
+ DifficultyControlPoint difficultyPoint = controlPointInfo.DifficultyPointAt(time);
+
+ double scoringDistance = OsuHitObject.BASE_SCORING_DISTANCE * difficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier;
+
+ return (float)(scoringDistance / timingPoint.BeatLength);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
index 1c040e9dee..fcf2772219 100644
--- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
+++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
@@ -2,10 +2,12 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
+using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders;
@@ -52,5 +54,31 @@ namespace osu.Game.Rulesets.Osu.Edit
return base.CreateBlueprintFor(hitObject);
}
+
+ protected override DistanceSnapGrid CreateDistanceSnapGrid(IEnumerable selectedHitObjects)
+ {
+ var objects = selectedHitObjects.ToList();
+
+ if (objects.Count == 0)
+ {
+ var lastObject = EditorBeatmap.HitObjects.LastOrDefault(h => h.StartTime <= EditorClock.CurrentTime);
+
+ if (lastObject == null)
+ return null;
+
+ return new OsuDistanceSnapGrid(lastObject);
+ }
+ else
+ {
+ double minTime = objects.Min(h => h.StartTime);
+
+ var lastObject = EditorBeatmap.HitObjects.LastOrDefault(h => h.StartTime < minTime);
+
+ if (lastObject == null)
+ return null;
+
+ return new OsuDistanceSnapGrid(lastObject);
+ }
+ }
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
index bb227d76df..f74f2d7bc5 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
@@ -24,14 +24,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private readonly IBindable stackHeightBindable = new Bindable();
private readonly IBindable scaleBindable = new Bindable();
- public OsuAction? HitAction => hitArea.HitAction;
+ public OsuAction? HitAction => HitArea.HitAction;
+ public readonly HitReceptor HitArea;
+ public readonly SkinnableDrawable CirclePiece;
private readonly Container scaleContainer;
- private readonly HitArea hitArea;
-
- public SkinnableDrawable CirclePiece { get; }
-
public DrawableHitCircle(HitCircle h)
: base(h)
{
@@ -48,7 +46,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Anchor = Anchor.Centre,
Children = new Drawable[]
{
- hitArea = new HitArea
+ HitArea = new HitReceptor
{
Hit = () =>
{
@@ -69,7 +67,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
},
};
- Size = hitArea.DrawSize;
+ Size = HitArea.DrawSize;
}
[BackgroundDependencyLoader]
@@ -153,7 +151,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Expire(true);
- hitArea.HitAction = null;
+ HitArea.HitAction = null;
break;
case ArmedState.Miss:
@@ -172,7 +170,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public Drawable ProxiedLayer => ApproachCircle;
- private class HitArea : Drawable, IKeyBindingHandler
+ public class HitReceptor : Drawable, IKeyBindingHandler
{
// IsHovered is used
public override bool HandlePositionalInput => true;
@@ -181,7 +179,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public OsuAction? HitAction;
- public HitArea()
+ public HitReceptor()
{
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
index 9e8ad9851c..6d45bb9ac4 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
@@ -5,7 +5,6 @@ using osuTK;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces;
-using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@@ -21,15 +20,19 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
public class DrawableSlider : DrawableOsuHitObject, IDrawableHitObjectWithProxiedApproach
{
- private readonly Slider slider;
- private readonly List components = new List();
-
- public readonly DrawableHitCircle HeadCircle;
- public readonly DrawableSliderTail TailCircle;
+ public DrawableSliderHead HeadCircle => headContainer.Child;
+ public DrawableSliderTail TailCircle => tailContainer.Child;
public readonly SnakingSliderBody Body;
public readonly SliderBall Ball;
+ private readonly Container headContainer;
+ private readonly Container tailContainer;
+ private readonly Container tickContainer;
+ private readonly Container repeatContainer;
+
+ private readonly Slider slider;
+
private readonly IBindable positionBindable = new Bindable();
private readonly IBindable scaleBindable = new Bindable();
private readonly IBindable pathBindable = new Bindable();
@@ -44,14 +47,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Position = s.StackedPosition;
- Container ticks;
- Container repeatPoints;
-
InternalChildren = new Drawable[]
{
Body = new SnakingSliderBody(s),
- ticks = new Container { RelativeSizeAxes = Axes.Both },
- repeatPoints = new Container { RelativeSizeAxes = Axes.Both },
+ tickContainer = new Container { RelativeSizeAxes = Axes.Both },
+ repeatContainer = new Container { RelativeSizeAxes = Axes.Both },
Ball = new SliderBall(s, this)
{
GetInitialHitAction = () => HeadCircle.HitAction,
@@ -60,45 +60,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
AlwaysPresent = true,
Alpha = 0
},
- HeadCircle = new DrawableSliderHead(s, s.HeadCircle)
- {
- OnShake = Shake
- },
- TailCircle = new DrawableSliderTail(s, s.TailCircle)
+ headContainer = new Container { RelativeSizeAxes = Axes.Both },
+ tailContainer = new Container { RelativeSizeAxes = Axes.Both },
};
-
- components.Add(Body);
- components.Add(Ball);
-
- AddNested(HeadCircle);
-
- AddNested(TailCircle);
- components.Add(TailCircle);
-
- foreach (var tick in s.NestedHitObjects.OfType())
- {
- var drawableTick = new DrawableSliderTick(tick) { Position = tick.Position - s.Position };
-
- ticks.Add(drawableTick);
- components.Add(drawableTick);
- AddNested(drawableTick);
- }
-
- foreach (var repeatPoint in s.NestedHitObjects.OfType())
- {
- var drawableRepeatPoint = new DrawableRepeatPoint(repeatPoint, this) { Position = repeatPoint.Position - s.Position };
-
- repeatPoints.Add(drawableRepeatPoint);
- components.Add(drawableRepeatPoint);
- AddNested(drawableRepeatPoint);
- }
- }
-
- protected override void UpdateInitialTransforms()
- {
- base.UpdateInitialTransforms();
-
- Body.FadeInFromZero(HitObject.TimeFadeIn);
}
[BackgroundDependencyLoader]
@@ -129,6 +93,67 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
}, true);
}
+ protected override void AddNestedHitObject(DrawableHitObject hitObject)
+ {
+ base.AddNestedHitObject(hitObject);
+
+ switch (hitObject)
+ {
+ case DrawableSliderHead head:
+ headContainer.Child = head;
+ break;
+
+ case DrawableSliderTail tail:
+ tailContainer.Child = tail;
+ break;
+
+ case DrawableSliderTick tick:
+ tickContainer.Add(tick);
+ break;
+
+ case DrawableRepeatPoint repeat:
+ repeatContainer.Add(repeat);
+ break;
+ }
+ }
+
+ protected override void ClearNestedHitObjects()
+ {
+ base.ClearNestedHitObjects();
+
+ headContainer.Clear();
+ tailContainer.Clear();
+ repeatContainer.Clear();
+ tickContainer.Clear();
+ }
+
+ protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject)
+ {
+ switch (hitObject)
+ {
+ case SliderTailCircle tail:
+ return new DrawableSliderTail(slider, tail);
+
+ case HitCircle head:
+ return new DrawableSliderHead(slider, head) { OnShake = Shake };
+
+ case SliderTick tick:
+ return new DrawableSliderTick(tick) { Position = tick.Position - slider.Position };
+
+ case RepeatPoint repeat:
+ return new DrawableRepeatPoint(repeat, this) { Position = repeat.Position - slider.Position };
+ }
+
+ return base.CreateNestedHitObject(hitObject);
+ }
+
+ protected override void UpdateInitialTransforms()
+ {
+ base.UpdateInitialTransforms();
+
+ Body.FadeInFromZero(HitObject.TimeFadeIn);
+ }
+
public readonly Bindable Tracking = new Bindable();
protected override void Update()
@@ -139,9 +164,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
double completionProgress = MathHelper.Clamp((Time.Current - slider.StartTime) / slider.Duration, 0, 1);
- foreach (var c in components.OfType()) c.UpdateProgress(completionProgress);
- foreach (var c in components.OfType()) c.UpdateSnakingPosition(slider.Path.PositionAt(Body.SnakedStart ?? 0), slider.Path.PositionAt(Body.SnakedEnd ?? 0));
- foreach (var t in components.OfType()) t.Tracking = Ball.Tracking;
+ Ball.UpdateProgress(completionProgress);
+ Body.UpdateProgress(completionProgress);
+
+ foreach (DrawableHitObject hitObject in NestedHitObjects)
+ {
+ if (hitObject is ITrackSnaking s) s.UpdateSnakingPosition(slider.Path.PositionAt(Body.SnakedStart ?? 0), slider.Path.PositionAt(Body.SnakedEnd ?? 0));
+ if (hitObject is IRequireTracking t) t.Tracking = Ball.Tracking;
+ }
Size = Body.Size;
OriginPosition = Body.PathOffset;
@@ -187,7 +217,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
ApplyResult(r =>
{
- var judgementsCount = NestedHitObjects.Count();
+ var judgementsCount = NestedHitObjects.Count;
var judgementsHit = NestedHitObjects.Count(h => h.IsHit);
var hitFraction = (double)judgementsHit / judgementsCount;
@@ -228,7 +258,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
}
}
- public Drawable ProxiedLayer => HeadCircle.ApproachCircle;
+ public Drawable ProxiedLayer => HeadCircle.ProxiedLayer;
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Body.ReceivePositionalInputAt(screenSpacePos);
}
diff --git a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs
index 80e013fe68..b506c1f918 100644
--- a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs
+++ b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs
@@ -14,8 +14,16 @@ namespace osu.Game.Rulesets.Osu.Objects
{
public abstract class OsuHitObject : HitObject, IHasComboInformation, IHasPosition
{
+ ///
+ /// The radius of hit objects (ie. the radius of a ).
+ ///
public const float OBJECT_RADIUS = 64;
+ ///
+ /// Scoring distance with a speed-adjusted beat length of 1 second (ie. the speed slider balls move through their track).
+ ///
+ internal const float BASE_SCORING_DISTANCE = 100;
+
public double TimePreempt = 600;
public double TimeFadeIn = 400;
diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs
index 9bed123465..d98d72331a 100644
--- a/osu.Game.Rulesets.Osu/Objects/Slider.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs
@@ -19,11 +19,6 @@ namespace osu.Game.Rulesets.Osu.Objects
{
public class Slider : OsuHitObject, IHasCurve
{
- ///
- /// Scoring distance with a speed-adjusted beat length of 1 second.
- ///
- private const float base_scoring_distance = 100;
-
public double EndTime => StartTime + this.SpanCount() * Path.Distance / Velocity;
public double Duration => EndTime - StartTime;
@@ -123,7 +118,7 @@ namespace osu.Game.Rulesets.Osu.Objects
TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime);
DifficultyControlPoint difficultyPoint = controlPointInfo.DifficultyPointAt(StartTime);
- double scoringDistance = base_scoring_distance * difficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier;
+ double scoringDistance = BASE_SCORING_DISTANCE * difficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier;
Velocity = scoringDistance / timingPoint.BeatLength;
TickDistance = scoringDistance / difficulty.SliderTickRate * TickDistanceMultiplier;
diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs
index 41a02deaca..0aa8661fd3 100644
--- a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs
+++ b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs
@@ -2,14 +2,11 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
-using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
-using osu.Game.Beatmaps;
-using osu.Game.Configuration;
using osu.Game.Rulesets.Osu.Skinning;
using osu.Game.Skinning;
using osuTK;
@@ -23,12 +20,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
private bool cursorExpand;
- private Bindable cursorScale;
- private Bindable autoCursorScale;
- private readonly IBindable beatmap = new Bindable();
-
private Container expandTarget;
- private Drawable scaleTarget;
public OsuCursor()
{
@@ -43,43 +35,19 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
}
[BackgroundDependencyLoader]
- private void load(OsuConfigManager config, IBindable beatmap)
+ private void load()
{
InternalChild = expandTarget = new Container
{
RelativeSizeAxes = Axes.Both,
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
- Child = scaleTarget = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.Cursor), _ => new DefaultCursor(), confineMode: ConfineMode.NoScaling)
+ Child = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.Cursor), _ => new DefaultCursor(), confineMode: ConfineMode.NoScaling)
{
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
}
};
-
- this.beatmap.BindTo(beatmap);
- this.beatmap.ValueChanged += _ => calculateScale();
-
- cursorScale = config.GetBindable(OsuSetting.GameplayCursorSize);
- cursorScale.ValueChanged += _ => calculateScale();
-
- autoCursorScale = config.GetBindable(OsuSetting.AutoCursorSize);
- autoCursorScale.ValueChanged += _ => calculateScale();
-
- calculateScale();
- }
-
- private void calculateScale()
- {
- float scale = cursorScale.Value;
-
- if (autoCursorScale.Value && beatmap.Value != null)
- {
- // if we have a beatmap available, let's get its circle size to figure out an automatic cursor scale modifier.
- scale *= 1f - 0.7f * (1f + beatmap.Value.BeatmapInfo.BaseDifficulty.CircleSize - BeatmapDifficulty.DEFAULT_DIFFICULTY) / BeatmapDifficulty.DEFAULT_DIFFICULTY;
- }
-
- scaleTarget.Scale = new Vector2(scale);
}
private const float pressed_scale = 1.2f;
diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs
index 6dbdf0114d..6433ced624 100644
--- a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs
+++ b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs
@@ -8,6 +8,8 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Textures;
using osu.Framework.Input.Bindings;
+using osu.Game.Beatmaps;
+using osu.Game.Configuration;
using osu.Game.Rulesets.Osu.Configuration;
using osu.Game.Rulesets.UI;
using osu.Game.Skinning;
@@ -27,6 +29,11 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
private readonly Drawable cursorTrail;
+ public Bindable CursorScale;
+ private Bindable userCursorScale;
+ private Bindable autoCursorScale;
+ private readonly IBindable beatmap = new Bindable();
+
public OsuCursorContainer()
{
InternalChild = fadeContainer = new Container
@@ -37,9 +44,36 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
}
[BackgroundDependencyLoader(true)]
- private void load(OsuRulesetConfigManager config)
+ private void load(OsuConfigManager config, OsuRulesetConfigManager rulesetConfig, IBindable beatmap)
{
- config?.BindWith(OsuRulesetSetting.ShowCursorTrail, showTrail);
+ rulesetConfig?.BindWith(OsuRulesetSetting.ShowCursorTrail, showTrail);
+
+ this.beatmap.BindTo(beatmap);
+ this.beatmap.ValueChanged += _ => calculateScale();
+
+ userCursorScale = config.GetBindable(OsuSetting.GameplayCursorSize);
+ userCursorScale.ValueChanged += _ => calculateScale();
+
+ autoCursorScale = config.GetBindable(OsuSetting.AutoCursorSize);
+ autoCursorScale.ValueChanged += _ => calculateScale();
+
+ CursorScale = new Bindable();
+ CursorScale.ValueChanged += e => ActiveCursor.Scale = cursorTrail.Scale = new Vector2(e.NewValue);
+
+ calculateScale();
+ }
+
+ private void calculateScale()
+ {
+ float scale = userCursorScale.Value;
+
+ if (autoCursorScale.Value && beatmap.Value != null)
+ {
+ // if we have a beatmap available, let's get its circle size to figure out an automatic cursor scale modifier.
+ scale *= 1f - 0.7f * (1f + beatmap.Value.BeatmapInfo.BaseDifficulty.CircleSize - BeatmapDifficulty.DEFAULT_DIFFICULTY) / BeatmapDifficulty.DEFAULT_DIFFICULTY;
+ }
+
+ CursorScale.Value = scale;
}
protected override void LoadComplete()
@@ -95,13 +129,13 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
protected override void PopIn()
{
fadeContainer.FadeTo(1, 300, Easing.OutQuint);
- ActiveCursor.ScaleTo(1, 400, Easing.OutQuint);
+ ActiveCursor.ScaleTo(CursorScale.Value, 400, Easing.OutQuint);
}
protected override void PopOut()
{
fadeContainer.FadeTo(0.05f, 450, Easing.OutQuint);
- ActiveCursor.ScaleTo(0.8f, 450, Easing.OutQuint);
+ ActiveCursor.ScaleTo(CursorScale.Value * 0.8f, 450, Easing.OutQuint);
}
private class DefaultCursorTrail : CursorTrail
diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
index d1757de445..69e53d6eea 100644
--- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
+++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
@@ -57,21 +57,15 @@ namespace osu.Game.Rulesets.Osu.UI
public override void Add(DrawableHitObject h)
{
h.OnNewResult += onNewResult;
-
- if (h is IDrawableHitObjectWithProxiedApproach c)
+ h.OnLoadComplete += d =>
{
- var original = c.ProxiedLayer;
-
- // Hitobjects only have lifetimes set on LoadComplete. For nested hitobjects (e.g. SliderHeads), this only happens when the parenting slider becomes visible.
- // This delegation is required to make sure that the approach circles for those not-yet-loaded objects aren't added prematurely.
- original.OnLoadComplete += addApproachCircleProxy;
- }
+ if (d is IDrawableHitObjectWithProxiedApproach c)
+ approachCircles.Add(c.ProxiedLayer.CreateProxy());
+ };
base.Add(h);
}
- private void addApproachCircleProxy(Drawable d) => approachCircles.Add(d.CreateProxy());
-
public override void PostProcess()
{
connectionLayer.HitObjects = HitObjectContainer.Objects.Select(d => d.HitObject).OfType();
diff --git a/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs b/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs
index 9e5df0d6b1..8347d255fa 100644
--- a/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs
+++ b/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs
@@ -1,15 +1,15 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation;
+using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Osu.UI.Cursor;
-using osu.Game.Rulesets.UI;
using osu.Game.Screens.Play;
using osuTK;
using osuTK.Graphics;
@@ -18,9 +18,11 @@ namespace osu.Game.Rulesets.Osu.UI
{
public class OsuResumeOverlay : ResumeOverlay
{
+ private Container cursorScaleContainer;
private OsuClickToResumeCursor clickToResumeCursor;
- private GameplayCursorContainer localCursorContainer;
+ private OsuCursorContainer localCursorContainer;
+ private Bindable localCursorScale;
public override CursorContainer LocalCursor => State.Value == Visibility.Visible ? localCursorContainer : null;
@@ -29,22 +31,35 @@ namespace osu.Game.Rulesets.Osu.UI
[BackgroundDependencyLoader]
private void load()
{
- Add(clickToResumeCursor = new OsuClickToResumeCursor { ResumeRequested = Resume });
+ Add(cursorScaleContainer = new Container
+ {
+ RelativePositionAxes = Axes.Both,
+ Child = clickToResumeCursor = new OsuClickToResumeCursor { ResumeRequested = Resume }
+ });
}
public override void Show()
{
base.Show();
- clickToResumeCursor.ShowAt(GameplayCursor.ActiveCursor.Position);
+ GameplayCursor.ActiveCursor.Hide();
+ cursorScaleContainer.MoveTo(GameplayCursor.ActiveCursor.Position);
+ clickToResumeCursor.Appear();
if (localCursorContainer == null)
+ {
Add(localCursorContainer = new OsuCursorContainer());
+
+ localCursorScale = new Bindable();
+ localCursorScale.BindTo(localCursorContainer.CursorScale);
+ localCursorScale.BindValueChanged(scale => cursorScaleContainer.Scale = new Vector2(scale.NewValue), true);
+ }
}
public override void Hide()
{
localCursorContainer?.Expire();
localCursorContainer = null;
+ GameplayCursor.ActiveCursor.Show();
base.Hide();
}
@@ -82,7 +97,7 @@ namespace osu.Game.Rulesets.Osu.UI
case OsuAction.RightButton:
if (!IsHovered) return false;
- this.ScaleTo(new Vector2(2), TRANSITION_TIME, Easing.OutQuint);
+ this.ScaleTo(2, TRANSITION_TIME, Easing.OutQuint);
ResumeRequested?.Invoke();
return true;
@@ -93,11 +108,10 @@ namespace osu.Game.Rulesets.Osu.UI
public bool OnReleased(OsuAction action) => false;
- public void ShowAt(Vector2 activeCursorPosition) => Schedule(() =>
+ public void Appear() => Schedule(() =>
{
updateColour();
- this.MoveTo(activeCursorPosition);
- this.ScaleTo(new Vector2(4)).Then().ScaleTo(Vector2.One, 1000, Easing.OutQuint);
+ this.ScaleTo(4).Then().ScaleTo(1, 1000, Easing.OutQuint);
});
private void updateColour()
diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoPlayfield.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoPlayfield.cs
index cbbf5b0c09..eaa8ca7ebb 100644
--- a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoPlayfield.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoPlayfield.cs
@@ -239,7 +239,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
private class TestStrongNestedHit : DrawableStrongNestedHit
{
public TestStrongNestedHit(DrawableHitObject mainObject)
- : base(null, mainObject)
+ : base(new StrongHitObject { StartTime = mainObject.HitObject.StartTime }, mainObject)
{
}
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs
index 8e16a21199..cc0d6829ba 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs
@@ -12,6 +12,7 @@ using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
@@ -28,31 +29,18 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
///
private int rollingHits;
+ private readonly Container tickContainer;
+
+ private Color4 colourIdle;
+ private Color4 colourEngaged;
+
public DrawableDrumRoll(DrumRoll drumRoll)
: base(drumRoll)
{
RelativeSizeAxes = Axes.Y;
-
- Container tickContainer;
MainPiece.Add(tickContainer = new Container { RelativeSizeAxes = Axes.Both });
-
- foreach (var tick in drumRoll.NestedHitObjects.OfType())
- {
- var newTick = new DrawableDrumRollTick(tick);
- newTick.OnNewResult += onNewTickResult;
-
- AddNested(newTick);
- tickContainer.Add(newTick);
- }
}
- protected override TaikoPiece CreateMainPiece() => new ElongatedCirclePiece();
-
- public override bool OnPressed(TaikoAction action) => false;
-
- private Color4 colourIdle;
- private Color4 colourEngaged;
-
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
@@ -60,8 +48,51 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
colourEngaged = colours.YellowDarker;
}
- private void onNewTickResult(DrawableHitObject obj, JudgementResult result)
+ protected override void LoadComplete()
{
+ base.LoadComplete();
+
+ OnNewResult += onNewResult;
+ }
+
+ protected override void AddNestedHitObject(DrawableHitObject hitObject)
+ {
+ base.AddNestedHitObject(hitObject);
+
+ switch (hitObject)
+ {
+ case DrawableDrumRollTick tick:
+ tickContainer.Add(tick);
+ break;
+ }
+ }
+
+ protected override void ClearNestedHitObjects()
+ {
+ base.ClearNestedHitObjects();
+ tickContainer.Clear();
+ }
+
+ protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject)
+ {
+ switch (hitObject)
+ {
+ case DrumRollTick tick:
+ return new DrawableDrumRollTick(tick);
+ }
+
+ return base.CreateNestedHitObject(hitObject);
+ }
+
+ protected override TaikoPiece CreateMainPiece() => new ElongatedCirclePiece();
+
+ public override bool OnPressed(TaikoAction action) => false;
+
+ private void onNewResult(DrawableHitObject obj, JudgementResult result)
+ {
+ if (!(obj is DrawableDrumRollTick))
+ return;
+
if (result.Type > HitResult.Miss)
rollingHits++;
else
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs
index 07af7fe7e0..9c9dfc5f9e 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs
@@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System;
-using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
@@ -14,6 +13,7 @@ using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces;
using osuTK;
using osuTK.Graphics;
using osu.Framework.Graphics.Shapes;
+using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
@@ -30,8 +30,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
///
private const double ring_appear_offset = 100;
- private readonly List ticks = new List();
-
+ private readonly Container ticks;
private readonly Container bodyContainer;
private readonly CircularContainer targetRing;
private readonly CircularContainer expandingRing;
@@ -108,16 +107,9 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
}
});
+ AddInternal(ticks = new Container { RelativeSizeAxes = Axes.Both });
+
MainPiece.Add(symbol = new SwellSymbolPiece());
-
- foreach (var tick in HitObject.NestedHitObjects.OfType())
- {
- var vis = new DrawableSwellTick(tick);
-
- ticks.Add(vis);
- AddInternal(vis);
- AddNested(vis);
- }
}
[BackgroundDependencyLoader]
@@ -136,11 +128,49 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
Width *= Parent.RelativeChildSize.X;
}
+ protected override void AddNestedHitObject(DrawableHitObject hitObject)
+ {
+ base.AddNestedHitObject(hitObject);
+
+ switch (hitObject)
+ {
+ case DrawableSwellTick tick:
+ ticks.Add(tick);
+ break;
+ }
+ }
+
+ protected override void ClearNestedHitObjects()
+ {
+ base.ClearNestedHitObjects();
+ ticks.Clear();
+ }
+
+ protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject)
+ {
+ switch (hitObject)
+ {
+ case SwellTick tick:
+ return new DrawableSwellTick(tick);
+ }
+
+ return base.CreateNestedHitObject(hitObject);
+ }
+
protected override void CheckForResult(bool userTriggered, double timeOffset)
{
if (userTriggered)
{
- var nextTick = ticks.Find(j => !j.IsHit);
+ DrawableSwellTick nextTick = null;
+
+ foreach (var t in ticks)
+ {
+ if (!t.IsHit)
+ {
+ nextTick = t;
+ break;
+ }
+ }
nextTick?.TriggerResult(HitResult.Great);
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs
index 423f65b2d3..0db6498c12 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs
@@ -11,6 +11,7 @@ using osu.Game.Audio;
using System.Collections.Generic;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
+using osu.Game.Rulesets.Objects;
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{
@@ -109,11 +110,12 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{
public override Vector2 OriginPosition => new Vector2(DrawHeight / 2);
- protected readonly Vector2 BaseSize;
+ public new TaikoHitType HitObject;
+ protected readonly Vector2 BaseSize;
protected readonly TaikoPiece MainPiece;
- public new TaikoHitType HitObject;
+ private readonly Container strongHitContainer;
protected DrawableTaikoHitObject(TaikoHitType hitObject)
: base(hitObject)
@@ -129,17 +131,38 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
Content.Add(MainPiece = CreateMainPiece());
MainPiece.KiaiMode = HitObject.Kiai;
- var strongObject = HitObject.NestedHitObjects.OfType().FirstOrDefault();
+ AddInternal(strongHitContainer = new Container());
+ }
- if (strongObject != null)
+ protected override void AddNestedHitObject(DrawableHitObject hitObject)
+ {
+ base.AddNestedHitObject(hitObject);
+
+ switch (hitObject)
{
- var strongHit = CreateStrongHit(strongObject);
-
- AddNested(strongHit);
- AddInternal(strongHit);
+ case DrawableStrongNestedHit strong:
+ strongHitContainer.Add(strong);
+ break;
}
}
+ protected override void ClearNestedHitObjects()
+ {
+ base.ClearNestedHitObjects();
+ strongHitContainer.Clear();
+ }
+
+ protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject)
+ {
+ switch (hitObject)
+ {
+ case StrongHitObject strong:
+ return CreateStrongHit(strong);
+ }
+
+ return base.CreateNestedHitObject(hitObject);
+ }
+
// Normal and clap samples are handled by the drum
protected override IEnumerable GetSamples() => HitObject.Samples.Where(s => s.Name != HitSampleInfo.HIT_NORMAL && s.Name != HitSampleInfo.HIT_CLAP);
diff --git a/osu.Game.Tests/Gameplay/TestSceneHitObjectAccentColour.cs b/osu.Game.Tests/Gameplay/TestSceneHitObjectAccentColour.cs
new file mode 100644
index 0000000000..6d7159a825
--- /dev/null
+++ b/osu.Game.Tests/Gameplay/TestSceneHitObjectAccentColour.cs
@@ -0,0 +1,143 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using NUnit.Framework;
+using osu.Framework.Audio.Sample;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Textures;
+using osu.Framework.Testing;
+using osu.Game.Audio;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Objects.Types;
+using osu.Game.Skinning;
+using osu.Game.Tests.Visual;
+using osuTK.Graphics;
+
+namespace osu.Game.Tests.Gameplay
+{
+ [HeadlessTest]
+ public class TestSceneHitObjectAccentColour : OsuTestScene
+ {
+ private Container skinContainer;
+
+ [SetUp]
+ public void Setup() => Schedule(() => Child = skinContainer = new SkinProvidingContainer(new TestSkin()));
+
+ [Test]
+ public void TestChangeComboIndexBeforeLoad()
+ {
+ TestDrawableHitObject hitObject = null;
+
+ AddStep("set combo and add hitobject", () =>
+ {
+ hitObject = new TestDrawableHitObject();
+ hitObject.HitObject.ComboIndex = 1;
+
+ skinContainer.Add(hitObject);
+ });
+
+ AddAssert("combo colour is green", () => hitObject.AccentColour.Value == Color4.Green);
+ }
+
+ [Test]
+ public void TestChangeComboIndexDuringLoad()
+ {
+ TestDrawableHitObject hitObject = null;
+
+ AddStep("add hitobject and set combo", () =>
+ {
+ skinContainer.Add(hitObject = new TestDrawableHitObject());
+ hitObject.HitObject.ComboIndex = 1;
+ });
+
+ AddAssert("combo colour is green", () => hitObject.AccentColour.Value == Color4.Green);
+ }
+
+ [Test]
+ public void TestChangeComboIndexAfterLoad()
+ {
+ TestDrawableHitObject hitObject = null;
+
+ AddStep("add hitobject", () => skinContainer.Add(hitObject = new TestDrawableHitObject()));
+ AddAssert("combo colour is red", () => hitObject.AccentColour.Value == Color4.Red);
+
+ AddStep("change combo", () => hitObject.HitObject.ComboIndex = 1);
+ AddAssert("combo colour is green", () => hitObject.AccentColour.Value == Color4.Green);
+ }
+
+ private class TestDrawableHitObject : DrawableHitObject
+ {
+ public TestDrawableHitObject()
+ : base(new TestHitObjectWithCombo())
+ {
+ }
+ }
+
+ private class TestHitObjectWithCombo : HitObject, IHasComboInformation
+ {
+ public bool NewCombo { get; } = false;
+ public int ComboOffset { get; } = 0;
+
+ public Bindable IndexInCurrentComboBindable { get; } = new Bindable();
+
+ public int IndexInCurrentCombo
+ {
+ get => IndexInCurrentComboBindable.Value;
+ set => IndexInCurrentComboBindable.Value = value;
+ }
+
+ public Bindable ComboIndexBindable { get; } = new Bindable();
+
+ public int ComboIndex
+ {
+ get => ComboIndexBindable.Value;
+ set => ComboIndexBindable.Value = value;
+ }
+
+ public Bindable LastInComboBindable { get; } = new Bindable();
+
+ public bool LastInCombo
+ {
+ get => LastInComboBindable.Value;
+ set => LastInComboBindable.Value = value;
+ }
+ }
+
+ private class TestSkin : ISkin
+ {
+ public readonly List ComboColours = new List
+ {
+ Color4.Red,
+ Color4.Green
+ };
+
+ public Drawable GetDrawableComponent(ISkinComponent component) => throw new NotImplementedException();
+
+ public Texture GetTexture(string componentName) => throw new NotImplementedException();
+
+ public SampleChannel GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException();
+
+ public IBindable GetConfig(TLookup lookup)
+ {
+ switch (lookup)
+ {
+ case GlobalSkinConfiguration global:
+ switch (global)
+ {
+ case GlobalSkinConfiguration.ComboColours:
+ return SkinUtils.As(new Bindable>(ComboColours));
+ }
+
+ break;
+ }
+
+ throw new NotImplementedException();
+ }
+ }
+ }
+}
diff --git a/osu.Game.Tests/NonVisual/BeatmapSetInfoEqualityTest.cs b/osu.Game.Tests/NonVisual/BeatmapSetInfoEqualityTest.cs
new file mode 100644
index 0000000000..42a3b4cf43
--- /dev/null
+++ b/osu.Game.Tests/NonVisual/BeatmapSetInfoEqualityTest.cs
@@ -0,0 +1,48 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Game.Beatmaps;
+
+namespace osu.Game.Tests.NonVisual
+{
+ [TestFixture]
+ public class BeatmapSetInfoEqualityTest
+ {
+ [Test]
+ public void TestOnlineWithOnline()
+ {
+ var ourInfo = new BeatmapSetInfo { OnlineBeatmapSetID = 123 };
+ var otherInfo = new BeatmapSetInfo { OnlineBeatmapSetID = 123 };
+
+ Assert.AreEqual(ourInfo, otherInfo);
+ }
+
+ [Test]
+ public void TestDatabasedWithDatabased()
+ {
+ var ourInfo = new BeatmapSetInfo { ID = 123 };
+ var otherInfo = new BeatmapSetInfo { ID = 123 };
+
+ Assert.AreEqual(ourInfo, otherInfo);
+ }
+
+ [Test]
+ public void TestDatabasedWithOnline()
+ {
+ var ourInfo = new BeatmapSetInfo { ID = 123, OnlineBeatmapSetID = 12 };
+ var otherInfo = new BeatmapSetInfo { OnlineBeatmapSetID = 12 };
+
+ Assert.AreEqual(ourInfo, otherInfo);
+ }
+
+ [Test]
+ public void TestCheckNullID()
+ {
+ var ourInfo = new BeatmapSetInfo { Status = BeatmapSetOnlineStatus.Loved };
+ var otherInfo = new BeatmapSetInfo { Status = BeatmapSetOnlineStatus.Approved };
+
+ Assert.AreNotEqual(ourInfo, otherInfo);
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Background/TestSceneUserDimContainer.cs b/osu.Game.Tests/Visual/Background/TestSceneUserDimContainer.cs
index 3061a3a542..f858174ff2 100644
--- a/osu.Game.Tests/Visual/Background/TestSceneUserDimContainer.cs
+++ b/osu.Game.Tests/Visual/Background/TestSceneUserDimContainer.cs
@@ -285,6 +285,12 @@ namespace osu.Game.Tests.Visual.Background
});
}
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+ rulesets?.Dispose();
+ }
+
private class DummySongSelect : PlaySongSelect
{
protected override BackgroundScreen CreateBackground()
diff --git a/osu.Game.Tests/Visual/Editor/TestSceneBeatSnapGrid.cs b/osu.Game.Tests/Visual/Editor/TestSceneDistanceSnapGrid.cs
similarity index 93%
rename from osu.Game.Tests/Visual/Editor/TestSceneBeatSnapGrid.cs
rename to osu.Game.Tests/Visual/Editor/TestSceneDistanceSnapGrid.cs
index 073cec7315..a9e5930478 100644
--- a/osu.Game.Tests/Visual/Editor/TestSceneBeatSnapGrid.cs
+++ b/osu.Game.Tests/Visual/Editor/TestSceneDistanceSnapGrid.cs
@@ -19,7 +19,7 @@ using osuTK.Graphics;
namespace osu.Game.Tests.Visual.Editor
{
- public class TestSceneBeatSnapGrid : EditorClockTestScene
+ public class TestSceneDistanceSnapGrid : EditorClockTestScene
{
private const double beat_length = 100;
private static readonly Vector2 grid_position = new Vector2(512, 384);
@@ -27,9 +27,9 @@ namespace osu.Game.Tests.Visual.Editor
[Cached(typeof(IEditorBeatmap))]
private readonly EditorBeatmap editorBeatmap;
- private TestBeatSnapGrid grid;
+ private TestDistanceSnapGrid grid;
- public TestSceneBeatSnapGrid()
+ public TestSceneDistanceSnapGrid()
{
editorBeatmap = new EditorBeatmap(new OsuBeatmap());
editorBeatmap.ControlPointInfo.TimingPoints.Add(new TimingControlPoint { BeatLength = beat_length });
@@ -112,7 +112,7 @@ namespace osu.Game.Tests.Visual.Editor
AddAssert("snap time is now 0.5 beats away", () => Precision.AlmostEquals(beat_length / 2, grid.GetSnapTime(snapPosition), 0.01));
}
- private void createGrid(Action func = null, string description = null)
+ private void createGrid(Action func = null, string description = null)
{
AddStep($"create grid {description ?? string.Empty}", () =>
{
@@ -123,20 +123,20 @@ namespace osu.Game.Tests.Visual.Editor
RelativeSizeAxes = Axes.Both,
Colour = Color4.SlateGray
},
- grid = new TestBeatSnapGrid(new HitObject(), grid_position)
+ grid = new TestDistanceSnapGrid(new HitObject(), grid_position)
};
func?.Invoke(grid);
});
}
- private class TestBeatSnapGrid : BeatSnapGrid
+ private class TestDistanceSnapGrid : DistanceSnapGrid
{
public new float Velocity = 1;
public new float DistanceSpacing => base.DistanceSpacing;
- public TestBeatSnapGrid(HitObject hitObject, Vector2 centrePosition)
+ public TestDistanceSnapGrid(HitObject hitObject, Vector2 centrePosition)
: base(hitObject, centrePosition)
{
}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneCommentsContainer.cs b/osu.Game.Tests/Visual/Online/TestSceneCommentsContainer.cs
new file mode 100644
index 0000000000..436e80d6f5
--- /dev/null
+++ b/osu.Game.Tests/Visual/Online/TestSceneCommentsContainer.cs
@@ -0,0 +1,58 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using NUnit.Framework;
+using osu.Game.Online.API.Requests;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics;
+using osu.Game.Overlays.Comments;
+
+namespace osu.Game.Tests.Visual.Online
+{
+ [TestFixture]
+ public class TestSceneCommentsContainer : OsuTestScene
+ {
+ public override IReadOnlyList RequiredTypes => new[]
+ {
+ typeof(CommentsContainer),
+ typeof(CommentsHeader),
+ typeof(DrawableComment),
+ typeof(HeaderButton),
+ typeof(SortTabControl),
+ typeof(ShowChildrenButton),
+ typeof(DeletedChildrenPlaceholder)
+ };
+
+ protected override bool UseOnlineAPI => true;
+
+ public TestSceneCommentsContainer()
+ {
+ BasicScrollContainer scrollFlow;
+
+ Add(scrollFlow = new BasicScrollContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ });
+
+ AddStep("Big Black comments", () =>
+ {
+ scrollFlow.Clear();
+ scrollFlow.Add(new CommentsContainer(CommentableType.Beatmapset, 41823));
+ });
+
+ AddStep("Airman comments", () =>
+ {
+ scrollFlow.Clear();
+ scrollFlow.Add(new CommentsContainer(CommentableType.Beatmapset, 24313));
+ });
+
+ AddStep("lazer build comments", () =>
+ {
+ scrollFlow.Clear();
+ scrollFlow.Add(new CommentsContainer(CommentableType.Build, 4772));
+ });
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneShowMoreButton.cs b/osu.Game.Tests/Visual/Online/TestSceneShowMoreButton.cs
index bccb263600..b9fbbfef6b 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneShowMoreButton.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneShowMoreButton.cs
@@ -1,10 +1,12 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using osu.Game.Overlays.Profile.Sections;
using System;
using System.Collections.Generic;
using osu.Framework.Graphics;
+using osu.Game.Graphics.UserInterface;
+using osu.Framework.Allocation;
+using osu.Game.Graphics;
namespace osu.Game.Tests.Visual.Online
{
@@ -17,11 +19,11 @@ namespace osu.Game.Tests.Visual.Online
public TestSceneShowMoreButton()
{
- ShowMoreButton button = null;
+ TestButton button = null;
int fireCount = 0;
- Add(button = new ShowMoreButton
+ Add(button = new TestButton
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -51,5 +53,16 @@ namespace osu.Game.Tests.Visual.Online
AddAssert("action fired twice", () => fireCount == 2);
AddAssert("is in loading state", () => button.IsLoading);
}
+
+ private class TestButton : ShowMoreButton
+ {
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colors)
+ {
+ IdleColour = colors.YellowDark;
+ HoverColour = colors.Yellow;
+ ChevronIconColour = colors.Red;
+ }
+ }
}
}
diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
index a7020b6534..efe7fee5e4 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
@@ -349,5 +349,11 @@ namespace osu.Game.Tests.Visual.SongSelect
DateAdded = DateTimeOffset.UtcNow,
};
}
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+ rulesets?.Dispose();
+ }
}
}
diff --git a/osu.Game/Beatmaps/BeatmapSetInfo.cs b/osu.Game/Beatmaps/BeatmapSetInfo.cs
index 03bc7c7312..a8b83dca38 100644
--- a/osu.Game/Beatmaps/BeatmapSetInfo.cs
+++ b/osu.Game/Beatmaps/BeatmapSetInfo.cs
@@ -63,6 +63,21 @@ namespace osu.Game.Beatmaps
public bool Protected { get; set; }
- public bool Equals(BeatmapSetInfo other) => OnlineBeatmapSetID == other?.OnlineBeatmapSetID;
+ public bool Equals(BeatmapSetInfo other)
+ {
+ if (other == null)
+ return false;
+
+ if (ID != 0 && other.ID != 0)
+ return ID == other.ID;
+
+ if (OnlineBeatmapSetID.HasValue && other.OnlineBeatmapSetID.HasValue)
+ return OnlineBeatmapSetID == other.OnlineBeatmapSetID;
+
+ if (!string.IsNullOrEmpty(Hash) && !string.IsNullOrEmpty(other.Hash))
+ return Hash == other.Hash;
+
+ return ReferenceEquals(this, other);
+ }
}
}
diff --git a/osu.Game/Overlays/Profile/Sections/ShowMoreButton.cs b/osu.Game/Graphics/UserInterface/ShowMoreButton.cs
similarity index 80%
rename from osu.Game/Overlays/Profile/Sections/ShowMoreButton.cs
rename to osu.Game/Graphics/UserInterface/ShowMoreButton.cs
index cf4e1c0dde..5296b9dd7f 100644
--- a/osu.Game/Overlays/Profile/Sections/ShowMoreButton.cs
+++ b/osu.Game/Graphics/UserInterface/ShowMoreButton.cs
@@ -1,30 +1,36 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
-using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
-using osu.Game.Graphics.UserInterface;
using osuTK;
+using osuTK.Graphics;
using System.Collections.Generic;
-namespace osu.Game.Overlays.Profile.Sections
+namespace osu.Game.Graphics.UserInterface
{
public class ShowMoreButton : OsuHoverContainer
{
private const float fade_duration = 200;
- private readonly Box background;
- private readonly LoadingAnimation loading;
- private readonly FillFlowContainer content;
+ private Color4 chevronIconColour;
- protected override IEnumerable EffectTargets => new[] { background };
+ protected Color4 ChevronIconColour
+ {
+ get => chevronIconColour;
+ set => chevronIconColour = leftChevron.Colour = rightChevron.Colour = value;
+ }
+
+ public string Text
+ {
+ get => text.Text;
+ set => text.Text = value;
+ }
private bool isLoading;
@@ -33,26 +39,32 @@ namespace osu.Game.Overlays.Profile.Sections
get => isLoading;
set
{
- if (isLoading == value)
- return;
-
isLoading = value;
Enabled.Value = !isLoading;
if (value)
{
- loading.FadeIn(fade_duration, Easing.OutQuint);
+ loading.Show();
content.FadeOut(fade_duration, Easing.OutQuint);
}
else
{
- loading.FadeOut(fade_duration, Easing.OutQuint);
+ loading.Hide();
content.FadeIn(fade_duration, Easing.OutQuint);
}
}
}
+ private readonly Box background;
+ private readonly LoadingAnimation loading;
+ private readonly FillFlowContainer content;
+ private readonly ChevronIcon leftChevron;
+ private readonly ChevronIcon rightChevron;
+ private readonly SpriteText text;
+
+ protected override IEnumerable EffectTargets => new[] { background };
+
public ShowMoreButton()
{
AutoSizeAxes = Axes.Both;
@@ -77,15 +89,15 @@ namespace osu.Game.Overlays.Profile.Sections
Spacing = new Vector2(7),
Children = new Drawable[]
{
- new ChevronIcon(),
- new OsuSpriteText
+ leftChevron = new ChevronIcon(),
+ text = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold),
Text = "show more".ToUpper(),
},
- new ChevronIcon(),
+ rightChevron = new ChevronIcon(),
}
},
loading = new LoadingAnimation
@@ -99,13 +111,6 @@ namespace osu.Game.Overlays.Profile.Sections
};
}
- [BackgroundDependencyLoader]
- private void load(OsuColour colors)
- {
- IdleColour = colors.GreySeafoamDark;
- HoverColour = colors.GreySeafoam;
- }
-
protected override bool OnClick(ClickEvent e)
{
if (!Enabled.Value)
@@ -133,12 +138,6 @@ namespace osu.Game.Overlays.Profile.Sections
Size = new Vector2(icon_size);
Icon = FontAwesome.Solid.ChevronDown;
}
-
- [BackgroundDependencyLoader]
- private void load(OsuColour colors)
- {
- Colour = colors.Yellow;
- }
}
}
}
diff --git a/osu.Game/Online/API/Requests/GetCommentsRequest.cs b/osu.Game/Online/API/Requests/GetCommentsRequest.cs
new file mode 100644
index 0000000000..7763501860
--- /dev/null
+++ b/osu.Game/Online/API/Requests/GetCommentsRequest.cs
@@ -0,0 +1,47 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.IO.Network;
+using Humanizer;
+using osu.Game.Online.API.Requests.Responses;
+using osu.Game.Overlays.Comments;
+
+namespace osu.Game.Online.API.Requests
+{
+ public class GetCommentsRequest : APIRequest
+ {
+ private readonly long id;
+ private readonly int page;
+ private readonly CommentableType type;
+ private readonly CommentsSortCriteria sort;
+
+ public GetCommentsRequest(CommentableType type, long id, CommentsSortCriteria sort = CommentsSortCriteria.New, int page = 1)
+ {
+ this.type = type;
+ this.sort = sort;
+ this.id = id;
+ this.page = page;
+ }
+
+ protected override WebRequest CreateWebRequest()
+ {
+ var req = base.CreateWebRequest();
+
+ req.AddParameter("commentable_type", type.ToString().Underscore().ToLowerInvariant());
+ req.AddParameter("commentable_id", id.ToString());
+ req.AddParameter("sort", sort.ToString().ToLowerInvariant());
+ req.AddParameter("page", page.ToString());
+
+ return req;
+ }
+
+ protected override string Target => "comments";
+ }
+
+ public enum CommentableType
+ {
+ Build,
+ Beatmapset,
+ NewsPost
+ }
+}
diff --git a/osu.Game/Online/API/Requests/Responses/Comment.cs b/osu.Game/Online/API/Requests/Responses/Comment.cs
new file mode 100644
index 0000000000..29abaa74e5
--- /dev/null
+++ b/osu.Game/Online/API/Requests/Responses/Comment.cs
@@ -0,0 +1,79 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using Newtonsoft.Json;
+using osu.Game.Users;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Text.RegularExpressions;
+
+namespace osu.Game.Online.API.Requests.Responses
+{
+ public class Comment
+ {
+ [JsonProperty(@"id")]
+ public long Id { get; set; }
+
+ [JsonProperty(@"parent_id")]
+ public long? ParentId { get; set; }
+
+ public readonly List ChildComments = new List();
+
+ public Comment ParentComment { get; set; }
+
+ [JsonProperty(@"user_id")]
+ public long? UserId { get; set; }
+
+ public User User { get; set; }
+
+ [JsonProperty(@"message")]
+ public string Message { get; set; }
+
+ [JsonProperty(@"message_html")]
+ public string MessageHtml { get; set; }
+
+ [JsonProperty(@"replies_count")]
+ public int RepliesCount { get; set; }
+
+ [JsonProperty(@"votes_count")]
+ public int VotesCount { get; set; }
+
+ [JsonProperty(@"commenatble_type")]
+ public string CommentableType { get; set; }
+
+ [JsonProperty(@"commentable_id")]
+ public int CommentableId { get; set; }
+
+ [JsonProperty(@"legacy_name")]
+ public string LegacyName { get; set; }
+
+ [JsonProperty(@"created_at")]
+ public DateTimeOffset CreatedAt { get; set; }
+
+ [JsonProperty(@"updated_at")]
+ public DateTimeOffset? UpdatedAt { get; set; }
+
+ [JsonProperty(@"deleted_at")]
+ public DateTimeOffset? DeletedAt { get; set; }
+
+ [JsonProperty(@"edited_at")]
+ public DateTimeOffset? EditedAt { get; set; }
+
+ [JsonProperty(@"edited_by_id")]
+ public long? EditedById { get; set; }
+
+ public User EditedUser { get; set; }
+
+ public bool IsTopLevel => !ParentId.HasValue;
+
+ public bool IsDeleted => DeletedAt.HasValue;
+
+ public bool HasMessage => !string.IsNullOrEmpty(MessageHtml);
+
+ public string GetMessage => HasMessage ? WebUtility.HtmlDecode(Regex.Replace(MessageHtml, @"<(.|\n)*?>", string.Empty)) : string.Empty;
+
+ public int DeletedChildrenCount => ChildComments.Count(c => c.IsDeleted);
+ }
+}
diff --git a/osu.Game/Online/API/Requests/Responses/CommentBundle.cs b/osu.Game/Online/API/Requests/Responses/CommentBundle.cs
new file mode 100644
index 0000000000..7063581605
--- /dev/null
+++ b/osu.Game/Online/API/Requests/Responses/CommentBundle.cs
@@ -0,0 +1,80 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using Newtonsoft.Json;
+using osu.Game.Users;
+using System.Collections.Generic;
+
+namespace osu.Game.Online.API.Requests.Responses
+{
+ public class CommentBundle
+ {
+ private List comments;
+
+ [JsonProperty(@"comments")]
+ public List Comments
+ {
+ get => comments;
+ set
+ {
+ comments = value;
+ comments.ForEach(child =>
+ {
+ if (child.ParentId != null)
+ {
+ comments.ForEach(parent =>
+ {
+ if (parent.Id == child.ParentId)
+ {
+ parent.ChildComments.Add(child);
+ child.ParentComment = parent;
+ }
+ });
+ }
+ });
+ }
+ }
+
+ [JsonProperty(@"has_more")]
+ public bool HasMore { get; set; }
+
+ [JsonProperty(@"has_more_id")]
+ public long? HasMoreId { get; set; }
+
+ [JsonProperty(@"user_follow")]
+ public bool UserFollow { get; set; }
+
+ [JsonProperty(@"included_comments")]
+ public List IncludedComments { get; set; }
+
+ private List users;
+
+ [JsonProperty(@"users")]
+ public List Users
+ {
+ get => users;
+ set
+ {
+ users = value;
+
+ value.ForEach(u =>
+ {
+ Comments.ForEach(c =>
+ {
+ if (c.UserId == u.Id)
+ c.User = u;
+
+ if (c.EditedById == u.Id)
+ c.EditedUser = u;
+ });
+ });
+ }
+ }
+
+ [JsonProperty(@"total")]
+ public int Total { get; set; }
+
+ [JsonProperty(@"top_level_count")]
+ public int TopLevelCount { get; set; }
+ }
+}
diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs
index 8578517a17..194a439b06 100644
--- a/osu.Game/OsuGameBase.cs
+++ b/osu.Game/OsuGameBase.cs
@@ -298,6 +298,12 @@ namespace osu.Game
public string[] HandledExtensions => fileImporters.SelectMany(i => i.HandledExtensions).ToArray();
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+ RulesetStore?.Dispose();
+ }
+
private class OsuUserInputManager : UserInputManager
{
protected override MouseButtonEventManager CreateButtonManagerFor(MouseButton button)
diff --git a/osu.Game/Overlays/AccountCreation/ScreenWarning.cs b/osu.Game/Overlays/AccountCreation/ScreenWarning.cs
index be417f4aac..f91d2e3323 100644
--- a/osu.Game/Overlays/AccountCreation/ScreenWarning.cs
+++ b/osu.Game/Overlays/AccountCreation/ScreenWarning.cs
@@ -121,7 +121,7 @@ namespace osu.Game.Overlays.AccountCreation
multiAccountExplanationText.AddText("? osu! has a policy of ");
multiAccountExplanationText.AddText("one account per person!", cp => cp.Colour = colours.Yellow);
multiAccountExplanationText.AddText(" Please be aware that creating more than one account per person may result in ");
- multiAccountExplanationText.AddText("permanent deactivation of accounts", cp => cp.Colour = colours.Yellow);
+ multiAccountExplanationText.AddText("permanent deactivation of accounts", cp => cp.Colour = colours.Yellow);
multiAccountExplanationText.AddText(".");
furtherAssistance.AddText("Need further assistance? Contact us via our ");
diff --git a/osu.Game/Overlays/Comments/CommentsContainer.cs b/osu.Game/Overlays/Comments/CommentsContainer.cs
new file mode 100644
index 0000000000..abc1b7233d
--- /dev/null
+++ b/osu.Game/Overlays/Comments/CommentsContainer.cs
@@ -0,0 +1,197 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Online.API;
+using osu.Game.Online.API.Requests;
+using osu.Framework.Graphics;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Graphics;
+using osu.Game.Online.API.Requests.Responses;
+using System.Threading;
+using System.Linq;
+using osu.Framework.Extensions.IEnumerableExtensions;
+
+namespace osu.Game.Overlays.Comments
+{
+ public class CommentsContainer : CompositeDrawable
+ {
+ private readonly CommentableType type;
+ private readonly long id;
+
+ public readonly Bindable Sort = new Bindable();
+ public readonly BindableBool ShowDeleted = new BindableBool();
+
+ [Resolved]
+ private IAPIProvider api { get; set; }
+
+ [Resolved]
+ private OsuColour colours { get; set; }
+
+ private GetCommentsRequest request;
+ private CancellationTokenSource loadCancellation;
+ private int currentPage;
+
+ private readonly Box background;
+ private readonly FillFlowContainer content;
+ private readonly DeletedChildrenPlaceholder deletedChildrenPlaceholder;
+ private readonly CommentsShowMoreButton moreButton;
+
+ public CommentsContainer(CommentableType type, long id)
+ {
+ this.type = type;
+ this.id = id;
+
+ RelativeSizeAxes = Axes.X;
+ AutoSizeAxes = Axes.Y;
+ AddRangeInternal(new Drawable[]
+ {
+ background = new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ },
+ new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Direction = FillDirection.Vertical,
+ Children = new Drawable[]
+ {
+ new CommentsHeader
+ {
+ Sort = { BindTarget = Sort },
+ ShowDeleted = { BindTarget = ShowDeleted }
+ },
+ content = new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Direction = FillDirection.Vertical,
+ },
+ new Container
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = OsuColour.Gray(0.2f)
+ },
+ new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Direction = FillDirection.Vertical,
+ Children = new Drawable[]
+ {
+ deletedChildrenPlaceholder = new DeletedChildrenPlaceholder
+ {
+ ShowDeleted = { BindTarget = ShowDeleted }
+ },
+ new Container
+ {
+ AutoSizeAxes = Axes.Y,
+ RelativeSizeAxes = Axes.X,
+ Child = moreButton = new CommentsShowMoreButton
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Margin = new MarginPadding(5),
+ Action = getComments
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ });
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ background.Colour = colours.Gray2;
+ }
+
+ protected override void LoadComplete()
+ {
+ Sort.BindValueChanged(onSortChanged, true);
+ base.LoadComplete();
+ }
+
+ private void onSortChanged(ValueChangedEvent sort)
+ {
+ clearComments();
+ getComments();
+ }
+
+ private void getComments()
+ {
+ request?.Cancel();
+ loadCancellation?.Cancel();
+ request = new GetCommentsRequest(type, id, Sort.Value, currentPage++);
+ request.Success += onSuccess;
+ api.Queue(request);
+ }
+
+ private void clearComments()
+ {
+ currentPage = 1;
+ deletedChildrenPlaceholder.DeletedCount.Value = 0;
+ moreButton.IsLoading = true;
+ content.Clear();
+ }
+
+ private void onSuccess(CommentBundle response)
+ {
+ loadCancellation = new CancellationTokenSource();
+
+ FillFlowContainer page = new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Direction = FillDirection.Vertical,
+ };
+
+ foreach (var c in response.Comments)
+ {
+ if (c.IsTopLevel)
+ page.Add(new DrawableComment(c)
+ {
+ ShowDeleted = { BindTarget = ShowDeleted }
+ });
+ }
+
+ LoadComponentAsync(page, loaded =>
+ {
+ content.Add(loaded);
+
+ deletedChildrenPlaceholder.DeletedCount.Value += response.Comments.Count(c => c.IsDeleted && c.IsTopLevel);
+
+ if (response.HasMore)
+ {
+ int loadedTopLevelComments = 0;
+ content.Children.OfType().ForEach(p => loadedTopLevelComments += p.Children.OfType().Count());
+
+ moreButton.Current.Value = response.TopLevelCount - loadedTopLevelComments;
+ moreButton.IsLoading = false;
+ }
+
+ moreButton.FadeTo(response.HasMore ? 1 : 0);
+ }, loadCancellation.Token);
+ }
+
+ protected override void Dispose(bool isDisposing)
+ {
+ request?.Cancel();
+ loadCancellation?.Cancel();
+ base.Dispose(isDisposing);
+ }
+ }
+}
diff --git a/osu.Game/Overlays/Comments/CommentsShowMoreButton.cs b/osu.Game/Overlays/Comments/CommentsShowMoreButton.cs
new file mode 100644
index 0000000000..b0174e7b1a
--- /dev/null
+++ b/osu.Game/Overlays/Comments/CommentsShowMoreButton.cs
@@ -0,0 +1,32 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Bindables;
+using osu.Game.Graphics;
+using osu.Game.Graphics.UserInterface;
+
+namespace osu.Game.Overlays.Comments
+{
+ public class CommentsShowMoreButton : ShowMoreButton
+ {
+ public readonly BindableInt Current = new BindableInt();
+
+ public CommentsShowMoreButton()
+ {
+ IdleColour = OsuColour.Gray(0.3f);
+ HoverColour = OsuColour.Gray(0.4f);
+ ChevronIconColour = OsuColour.Gray(0.5f);
+ }
+
+ protected override void LoadComplete()
+ {
+ Current.BindValueChanged(onCurrentChanged, true);
+ base.LoadComplete();
+ }
+
+ private void onCurrentChanged(ValueChangedEvent count)
+ {
+ Text = $@"Show More ({count.NewValue})".ToUpper();
+ }
+ }
+}
diff --git a/osu.Game/Overlays/Comments/DeletedChildrenPlaceholder.cs b/osu.Game/Overlays/Comments/DeletedChildrenPlaceholder.cs
new file mode 100644
index 0000000000..e849691597
--- /dev/null
+++ b/osu.Game/Overlays/Comments/DeletedChildrenPlaceholder.cs
@@ -0,0 +1,61 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics;
+using osu.Game.Graphics;
+using osu.Framework.Graphics.Sprites;
+using osuTK;
+using osu.Framework.Bindables;
+using Humanizer;
+
+namespace osu.Game.Overlays.Comments
+{
+ public class DeletedChildrenPlaceholder : FillFlowContainer
+ {
+ public readonly BindableBool ShowDeleted = new BindableBool();
+ public readonly BindableInt DeletedCount = new BindableInt();
+
+ private readonly SpriteText countText;
+
+ public DeletedChildrenPlaceholder()
+ {
+ AutoSizeAxes = Axes.Both;
+ Direction = FillDirection.Horizontal;
+ Spacing = new Vector2(3, 0);
+ Margin = new MarginPadding { Vertical = 10, Left = 80 };
+ Children = new Drawable[]
+ {
+ new SpriteIcon
+ {
+ Icon = FontAwesome.Solid.Trash,
+ Size = new Vector2(14),
+ },
+ countText = new SpriteText
+ {
+ Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold, italics: true),
+ }
+ };
+ }
+
+ protected override void LoadComplete()
+ {
+ DeletedCount.BindValueChanged(_ => updateDisplay(), true);
+ ShowDeleted.BindValueChanged(_ => updateDisplay(), true);
+ base.LoadComplete();
+ }
+
+ private void updateDisplay()
+ {
+ if (DeletedCount.Value != 0)
+ {
+ countText.Text = @"deleted comment".ToQuantity(DeletedCount.Value);
+ this.FadeTo(ShowDeleted.Value ? 0 : 1);
+ }
+ else
+ {
+ Hide();
+ }
+ }
+ }
+}
diff --git a/osu.Game/Overlays/Comments/DrawableComment.cs b/osu.Game/Overlays/Comments/DrawableComment.cs
new file mode 100644
index 0000000000..89abda92cf
--- /dev/null
+++ b/osu.Game/Overlays/Comments/DrawableComment.cs
@@ -0,0 +1,363 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics;
+using osu.Game.Graphics;
+using osu.Framework.Graphics.Sprites;
+using osuTK;
+using osu.Game.Online.API.Requests.Responses;
+using osu.Game.Users.Drawables;
+using osu.Game.Graphics.Containers;
+using osu.Game.Utils;
+using osu.Framework.Graphics.Cursor;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics.Shapes;
+using System.Linq;
+using osu.Game.Online.Chat;
+
+namespace osu.Game.Overlays.Comments
+{
+ public class DrawableComment : CompositeDrawable
+ {
+ private const int avatar_size = 40;
+ private const int margin = 10;
+
+ public readonly BindableBool ShowDeleted = new BindableBool();
+
+ private readonly BindableBool childrenExpanded = new BindableBool(true);
+
+ private readonly FillFlowContainer childCommentsVisibilityContainer;
+ private readonly Comment comment;
+
+ public DrawableComment(Comment comment)
+ {
+ LinkFlowContainer username;
+ FillFlowContainer childCommentsContainer;
+ DeletedChildrenPlaceholder deletedChildrenPlaceholder;
+ FillFlowContainer info;
+ LinkFlowContainer message;
+ GridContainer content;
+ VotePill votePill;
+
+ this.comment = comment;
+
+ RelativeSizeAxes = Axes.X;
+ AutoSizeAxes = Axes.Y;
+ InternalChild = new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Direction = FillDirection.Vertical,
+ Children = new Drawable[]
+ {
+ new Container
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Padding = new MarginPadding(margin),
+ Child = content = new GridContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ ColumnDimensions = new[]
+ {
+ new Dimension(GridSizeMode.AutoSize),
+ new Dimension(),
+ },
+ RowDimensions = new[]
+ {
+ new Dimension(GridSizeMode.AutoSize)
+ },
+ Content = new[]
+ {
+ new Drawable[]
+ {
+ new FillFlowContainer
+ {
+ AutoSizeAxes = Axes.Both,
+ Margin = new MarginPadding { Horizontal = margin },
+ Direction = FillDirection.Horizontal,
+ Spacing = new Vector2(5, 0),
+ Children = new Drawable[]
+ {
+ votePill = new VotePill(comment.VotesCount)
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ AlwaysPresent = true,
+ },
+ new UpdateableAvatar(comment.User)
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Size = new Vector2(avatar_size),
+ Masking = true,
+ CornerRadius = avatar_size / 2f,
+ },
+ }
+ },
+ new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Spacing = new Vector2(0, 3),
+ Children = new Drawable[]
+ {
+ new FillFlowContainer
+ {
+ AutoSizeAxes = Axes.Both,
+ Direction = FillDirection.Horizontal,
+ Spacing = new Vector2(7, 0),
+ Children = new Drawable[]
+ {
+ username = new LinkFlowContainer(s => s.Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold, italics: true))
+ {
+ AutoSizeAxes = Axes.Both,
+ },
+ new ParentUsername(comment),
+ new SpriteText
+ {
+ Alpha = comment.IsDeleted ? 1 : 0,
+ Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold, italics: true),
+ Text = @"deleted",
+ }
+ }
+ },
+ message = new LinkFlowContainer(s => s.Font = OsuFont.GetFont(size: 14))
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Padding = new MarginPadding { Right = 40 }
+ },
+ info = new FillFlowContainer
+ {
+ AutoSizeAxes = Axes.Both,
+ Direction = FillDirection.Horizontal,
+ Spacing = new Vector2(10, 0),
+ Colour = OsuColour.Gray(0.7f),
+ Children = new Drawable[]
+ {
+ new SpriteText
+ {
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
+ Font = OsuFont.GetFont(size: 12),
+ Text = HumanizerUtils.Humanize(comment.CreatedAt)
+ },
+ new RepliesButton(comment.RepliesCount)
+ {
+ Expanded = { BindTarget = childrenExpanded }
+ },
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ childCommentsVisibilityContainer = new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Direction = FillDirection.Vertical,
+ Children = new Drawable[]
+ {
+ childCommentsContainer = new FillFlowContainer
+ {
+ Padding = new MarginPadding { Left = 20 },
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Direction = FillDirection.Vertical
+ },
+ deletedChildrenPlaceholder = new DeletedChildrenPlaceholder
+ {
+ ShowDeleted = { BindTarget = ShowDeleted }
+ }
+ }
+ }
+ }
+ };
+
+ deletedChildrenPlaceholder.DeletedCount.Value = comment.DeletedChildrenCount;
+
+ if (comment.UserId.HasValue)
+ username.AddUserLink(comment.User);
+ else
+ username.AddText(comment.LegacyName);
+
+ if (comment.EditedAt.HasValue)
+ {
+ info.Add(new SpriteText
+ {
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
+ Font = OsuFont.GetFont(size: 12),
+ Text = $@"edited {HumanizerUtils.Humanize(comment.EditedAt.Value)} by {comment.EditedUser.Username}"
+ });
+ }
+
+ if (comment.HasMessage)
+ {
+ var formattedSource = MessageFormatter.FormatText(comment.GetMessage);
+ message.AddLinks(formattedSource.Text, formattedSource.Links);
+ }
+
+ if (comment.IsDeleted)
+ {
+ content.FadeColour(OsuColour.Gray(0.5f));
+ votePill.Hide();
+ }
+
+ if (comment.IsTopLevel)
+ {
+ AddInternal(new Container
+ {
+ RelativeSizeAxes = Axes.X,
+ Height = 1.5f,
+ Anchor = Anchor.BottomCentre,
+ Origin = Anchor.BottomCentre,
+ Child = new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = OsuColour.Gray(0.1f)
+ }
+ });
+
+ if (comment.ChildComments.Any())
+ {
+ AddInternal(new ChevronButton(comment)
+ {
+ Anchor = Anchor.TopRight,
+ Origin = Anchor.TopRight,
+ Margin = new MarginPadding { Right = 30, Top = margin },
+ Expanded = { BindTarget = childrenExpanded }
+ });
+ }
+ }
+
+ comment.ChildComments.ForEach(c => childCommentsContainer.Add(new DrawableComment(c)
+ {
+ ShowDeleted = { BindTarget = ShowDeleted }
+ }));
+ }
+
+ protected override void LoadComplete()
+ {
+ ShowDeleted.BindValueChanged(show =>
+ {
+ if (comment.IsDeleted)
+ this.FadeTo(show.NewValue ? 1 : 0);
+ }, true);
+ childrenExpanded.BindValueChanged(expanded => childCommentsVisibilityContainer.FadeTo(expanded.NewValue ? 1 : 0), true);
+ base.LoadComplete();
+ }
+
+ private class ChevronButton : ShowChildrenButton
+ {
+ private readonly SpriteIcon icon;
+
+ public ChevronButton(Comment comment)
+ {
+ Alpha = comment.IsTopLevel && comment.ChildComments.Any() ? 1 : 0;
+ Child = icon = new SpriteIcon
+ {
+ Size = new Vector2(12),
+ Colour = OsuColour.Gray(0.7f)
+ };
+ }
+
+ protected override void OnExpandedChanged(ValueChangedEvent expanded)
+ {
+ icon.Icon = expanded.NewValue ? FontAwesome.Solid.ChevronUp : FontAwesome.Solid.ChevronDown;
+ }
+ }
+
+ private class RepliesButton : ShowChildrenButton
+ {
+ private readonly SpriteText text;
+ private readonly int count;
+
+ public RepliesButton(int count)
+ {
+ this.count = count;
+
+ Alpha = count == 0 ? 0 : 1;
+ Child = text = new SpriteText
+ {
+ Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold),
+ };
+ }
+
+ protected override void OnExpandedChanged(ValueChangedEvent expanded)
+ {
+ text.Text = $@"{(expanded.NewValue ? "[+]" : "[-]")} replies ({count})";
+ }
+ }
+
+ private class ParentUsername : FillFlowContainer, IHasTooltip
+ {
+ public string TooltipText => getParentMessage();
+
+ private readonly Comment parentComment;
+
+ public ParentUsername(Comment comment)
+ {
+ parentComment = comment.ParentComment;
+
+ AutoSizeAxes = Axes.Both;
+ Direction = FillDirection.Horizontal;
+ Spacing = new Vector2(3, 0);
+ Alpha = comment.ParentId == null ? 0 : 1;
+ Children = new Drawable[]
+ {
+ new SpriteIcon
+ {
+ Icon = FontAwesome.Solid.Reply,
+ Size = new Vector2(14),
+ },
+ new SpriteText
+ {
+ Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold, italics: true),
+ Text = parentComment?.User?.Username ?? parentComment?.LegacyName
+ }
+ };
+ }
+
+ private string getParentMessage()
+ {
+ if (parentComment == null)
+ return string.Empty;
+
+ return parentComment.HasMessage ? parentComment.GetMessage : parentComment.IsDeleted ? @"deleted" : string.Empty;
+ }
+ }
+
+ private class VotePill : CircularContainer
+ {
+ public VotePill(int count)
+ {
+ AutoSizeAxes = Axes.X;
+ Height = 20;
+ Masking = true;
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = OsuColour.Gray(0.05f)
+ },
+ new SpriteText
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Margin = new MarginPadding { Horizontal = margin },
+ Font = OsuFont.GetFont(size: 14),
+ Text = $"+{count}"
+ }
+ };
+ }
+ }
+ }
+}
diff --git a/osu.Game/Overlays/Comments/ShowChildrenButton.cs b/osu.Game/Overlays/Comments/ShowChildrenButton.cs
new file mode 100644
index 0000000000..be04b6e5de
--- /dev/null
+++ b/osu.Game/Overlays/Comments/ShowChildrenButton.cs
@@ -0,0 +1,34 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Graphics;
+using osu.Game.Graphics.Containers;
+using osu.Framework.Input.Events;
+using osu.Framework.Bindables;
+
+namespace osu.Game.Overlays.Comments
+{
+ public abstract class ShowChildrenButton : OsuHoverContainer
+ {
+ public readonly BindableBool Expanded = new BindableBool(true);
+
+ protected ShowChildrenButton()
+ {
+ AutoSizeAxes = Axes.Both;
+ }
+
+ protected override void LoadComplete()
+ {
+ Expanded.BindValueChanged(OnExpandedChanged, true);
+ base.LoadComplete();
+ }
+
+ protected abstract void OnExpandedChanged(ValueChangedEvent expanded);
+
+ protected override bool OnClick(ClickEvent e)
+ {
+ Expanded.Value = !Expanded.Value;
+ return true;
+ }
+ }
+}
diff --git a/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs b/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs
index bb221bd43a..dc1a847b14 100644
--- a/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs
+++ b/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs
@@ -19,7 +19,7 @@ namespace osu.Game.Overlays.Profile.Sections
{
public abstract class PaginatedContainer : FillFlowContainer
{
- private readonly ShowMoreButton moreButton;
+ private readonly ProfileShowMoreButton moreButton;
private readonly OsuSpriteText missingText;
private APIRequest> retrievalRequest;
private CancellationTokenSource loadCancellation;
@@ -56,7 +56,7 @@ namespace osu.Game.Overlays.Profile.Sections
RelativeSizeAxes = Axes.X,
Spacing = new Vector2(0, 2),
},
- moreButton = new ShowMoreButton
+ moreButton = new ProfileShowMoreButton
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
diff --git a/osu.Game/Overlays/Profile/Sections/ProfileShowMoreButton.cs b/osu.Game/Overlays/Profile/Sections/ProfileShowMoreButton.cs
new file mode 100644
index 0000000000..28486cc743
--- /dev/null
+++ b/osu.Game/Overlays/Profile/Sections/ProfileShowMoreButton.cs
@@ -0,0 +1,20 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Game.Graphics;
+using osu.Game.Graphics.UserInterface;
+
+namespace osu.Game.Overlays.Profile.Sections
+{
+ public class ProfileShowMoreButton : ShowMoreButton
+ {
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colors)
+ {
+ IdleColour = colors.GreySeafoamDark;
+ HoverColour = colors.GreySeafoam;
+ ChevronIconColour = colors.Yellow;
+ }
+ }
+}
diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs
index a267d7c44d..51155ce3fd 100644
--- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs
+++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs
@@ -4,12 +4,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input;
using osu.Framework.Logging;
+using osu.Framework.Threading;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Configuration;
@@ -22,6 +24,7 @@ using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Components.RadioButtons;
using osu.Game.Screens.Edit.Compose;
using osu.Game.Screens.Edit.Compose.Components;
+using osuTK;
namespace osu.Game.Rulesets.Edit
{
@@ -30,16 +33,20 @@ namespace osu.Game.Rulesets.Edit
where TObject : HitObject
{
protected IRulesetConfigManager Config { get; private set; }
-
+ protected EditorBeatmap EditorBeatmap { get; private set; }
protected readonly Ruleset Ruleset;
+ [Resolved]
+ protected IFrameBasedClock EditorClock { get; private set; }
+
private IWorkingBeatmap workingBeatmap;
private Beatmap playableBeatmap;
- private EditorBeatmap editorBeatmap;
private IBeatmapProcessor beatmapProcessor;
private DrawableEditRulesetWrapper drawableRulesetWrapper;
private BlueprintContainer blueprintContainer;
+ private Container distanceSnapGridContainer;
+ private DistanceSnapGrid distanceSnapGrid;
private readonly List layerContainers = new List();
private InputManager inputManager;
@@ -57,7 +64,8 @@ namespace osu.Game.Rulesets.Edit
{
drawableRulesetWrapper = new DrawableEditRulesetWrapper(CreateDrawableRuleset(Ruleset, workingBeatmap, Array.Empty()))
{
- Clock = framedClock
+ Clock = framedClock,
+ ProcessCustomClock = false
};
}
catch (Exception e)
@@ -66,11 +74,13 @@ namespace osu.Game.Rulesets.Edit
return;
}
- var layerBelowRuleset = drawableRulesetWrapper.CreatePlayfieldAdjustmentContainer();
- layerBelowRuleset.Child = new EditorPlayfieldBorder { RelativeSizeAxes = Axes.Both };
+ var layerBelowRuleset = drawableRulesetWrapper.CreatePlayfieldAdjustmentContainer().WithChildren(new Drawable[]
+ {
+ distanceSnapGridContainer = new Container { RelativeSizeAxes = Axes.Both },
+ new EditorPlayfieldBorder { RelativeSizeAxes = Axes.Both }
+ });
- var layerAboveRuleset = drawableRulesetWrapper.CreatePlayfieldAdjustmentContainer();
- layerAboveRuleset.Child = blueprintContainer = new BlueprintContainer();
+ var layerAboveRuleset = drawableRulesetWrapper.CreatePlayfieldAdjustmentContainer().WithChild(blueprintContainer = new BlueprintContainer());
layerContainers.Add(layerBelowRuleset);
layerContainers.Add(layerAboveRuleset);
@@ -113,11 +123,13 @@ namespace osu.Game.Rulesets.Edit
};
toolboxCollection.Items =
- CompositionTools.Select(t => new RadioButton(t.Name, () => blueprintContainer.CurrentTool = t))
- .Prepend(new RadioButton("Select", () => blueprintContainer.CurrentTool = null))
+ CompositionTools.Select(t => new RadioButton(t.Name, () => selectTool(t)))
+ .Prepend(new RadioButton("Select", () => selectTool(null)))
.ToList();
toolboxCollection.Items[0].Select();
+
+ blueprintContainer.SelectionChanged += selectionChanged;
}
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
@@ -129,14 +141,14 @@ namespace osu.Game.Rulesets.Edit
beatmapProcessor = Ruleset.CreateBeatmapProcessor(playableBeatmap);
- editorBeatmap = new EditorBeatmap(playableBeatmap);
- editorBeatmap.HitObjectAdded += addHitObject;
- editorBeatmap.HitObjectRemoved += removeHitObject;
- editorBeatmap.StartTimeChanged += updateHitObject;
+ EditorBeatmap = new EditorBeatmap(playableBeatmap);
+ EditorBeatmap.HitObjectAdded += addHitObject;
+ EditorBeatmap.HitObjectRemoved += removeHitObject;
+ EditorBeatmap.StartTimeChanged += updateHitObject;
var dependencies = new DependencyContainer(parent);
- dependencies.CacheAs(editorBeatmap);
- dependencies.CacheAs>(editorBeatmap);
+ dependencies.CacheAs(EditorBeatmap);
+ dependencies.CacheAs>(EditorBeatmap);
Config = dependencies.Get().GetConfigFor(Ruleset);
@@ -150,6 +162,14 @@ namespace osu.Game.Rulesets.Edit
inputManager = GetContainingInputManager();
}
+ protected override void Update()
+ {
+ base.Update();
+
+ if (EditorClock.ElapsedFrameTime != 0 && blueprintContainer.CurrentTool != null)
+ showGridFor(Enumerable.Empty());
+ }
+
protected override void UpdateAfterChildren()
{
base.UpdateAfterChildren();
@@ -163,19 +183,53 @@ namespace osu.Game.Rulesets.Edit
});
}
- private void addHitObject(HitObject hitObject) => updateHitObject(hitObject);
-
- private void removeHitObject(HitObject hitObject)
+ private void selectionChanged(IEnumerable selectedHitObjects)
{
- beatmapProcessor?.PreProcess();
- beatmapProcessor?.PostProcess();
+ var hitObjects = selectedHitObjects.ToArray();
+
+ if (!hitObjects.Any())
+ distanceSnapGridContainer.Hide();
+ else
+ showGridFor(hitObjects);
}
- private void updateHitObject(HitObject hitObject)
+ private void selectTool(HitObjectCompositionTool tool)
{
- beatmapProcessor?.PreProcess();
- hitObject.ApplyDefaults(playableBeatmap.ControlPointInfo, playableBeatmap.BeatmapInfo.BaseDifficulty);
- beatmapProcessor?.PostProcess();
+ blueprintContainer.CurrentTool = tool;
+
+ if (tool == null)
+ distanceSnapGridContainer.Hide();
+ else
+ showGridFor(Enumerable.Empty());
+ }
+
+ private void showGridFor(IEnumerable selectedHitObjects)
+ {
+ distanceSnapGridContainer.Clear();
+ distanceSnapGrid = CreateDistanceSnapGrid(selectedHitObjects);
+
+ if (distanceSnapGrid != null)
+ {
+ distanceSnapGridContainer.Child = distanceSnapGrid;
+ distanceSnapGridContainer.Show();
+ }
+ }
+
+ private ScheduledDelegate scheduledUpdate;
+
+ private void addHitObject(HitObject hitObject) => updateHitObject(hitObject);
+
+ private void removeHitObject(HitObject hitObject) => updateHitObject(null);
+
+ private void updateHitObject([CanBeNull] HitObject hitObject)
+ {
+ scheduledUpdate?.Cancel();
+ scheduledUpdate = Schedule(() =>
+ {
+ beatmapProcessor?.PreProcess();
+ hitObject?.ApplyDefaults(playableBeatmap.ControlPointInfo, playableBeatmap.BeatmapInfo.BaseDifficulty);
+ beatmapProcessor?.PostProcess();
+ });
}
public override IEnumerable HitObjects => drawableRulesetWrapper.Playfield.AllHitObjects;
@@ -187,20 +241,30 @@ namespace osu.Game.Rulesets.Edit
public void BeginPlacement(HitObject hitObject)
{
+ if (distanceSnapGrid != null)
+ hitObject.StartTime = GetSnappedTime(hitObject.StartTime, distanceSnapGrid.ToLocalSpace(inputManager.CurrentState.Mouse.Position));
}
- public void EndPlacement(HitObject hitObject) => editorBeatmap.Add(hitObject);
+ public void EndPlacement(HitObject hitObject)
+ {
+ EditorBeatmap.Add(hitObject);
+ showGridFor(Enumerable.Empty());
+ }
- public void Delete(HitObject hitObject) => editorBeatmap.Remove(hitObject);
+ public void Delete(HitObject hitObject) => EditorBeatmap.Remove(hitObject);
+
+ public override Vector2 GetSnappedPosition(Vector2 position) => distanceSnapGrid?.GetSnapPosition(position) ?? position;
+
+ public override double GetSnappedTime(double startTime, Vector2 position) => distanceSnapGrid?.GetSnapTime(position) ?? startTime;
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
- if (editorBeatmap != null)
+ if (EditorBeatmap != null)
{
- editorBeatmap.HitObjectAdded -= addHitObject;
- editorBeatmap.HitObjectRemoved -= removeHitObject;
+ EditorBeatmap.HitObjectAdded -= addHitObject;
+ EditorBeatmap.HitObjectRemoved -= removeHitObject;
}
}
}
@@ -233,5 +297,17 @@ namespace osu.Game.Rulesets.Edit
/// Creates a which outlines s and handles movement of selections.
///
public virtual SelectionHandler CreateSelectionHandler() => new SelectionHandler();
+
+ ///
+ /// Creates the applicable for a selection.
+ ///
+ /// The selection.
+ /// The for .
+ [CanBeNull]
+ protected virtual DistanceSnapGrid CreateDistanceSnapGrid([NotNull] IEnumerable selectedHitObjects) => null;
+
+ public abstract Vector2 GetSnappedPosition(Vector2 position);
+
+ public abstract double GetSnappedTime(double startTime, Vector2 screenSpacePosition);
}
}
diff --git a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs
index 290fd8d27d..07283d2245 100644
--- a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs
+++ b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs
@@ -91,8 +91,10 @@ namespace osu.Game.Rulesets.Edit
///
/// Signals that the placement of has started.
///
- protected void BeginPlacement()
+ /// The start time of at the placement point. If null, the current clock time is used.
+ protected void BeginPlacement(double? startTime = null)
{
+ HitObject.StartTime = startTime ?? EditorClock.CurrentTime;
placementHandler.BeginPlacement(HitObject);
PlacementBegun = true;
}
diff --git a/osu.Game/Rulesets/Edit/SelectionBlueprint.cs b/osu.Game/Rulesets/Edit/SelectionBlueprint.cs
index 838984b223..3076ad081a 100644
--- a/osu.Game/Rulesets/Edit/SelectionBlueprint.cs
+++ b/osu.Game/Rulesets/Edit/SelectionBlueprint.cs
@@ -43,20 +43,20 @@ namespace osu.Game.Rulesets.Edit
///
/// The which this applies to.
///
- public readonly DrawableHitObject HitObject;
+ public readonly DrawableHitObject DrawableObject;
///
- /// The screen-space position of prior to handling a movement event.
+ /// The screen-space position of prior to handling a movement event.
///
internal Vector2 ScreenSpaceMovementStartPosition { get; private set; }
- protected override bool ShouldBeAlive => (HitObject.IsAlive && HitObject.IsPresent) || State == SelectionState.Selected;
+ protected override bool ShouldBeAlive => (DrawableObject.IsAlive && DrawableObject.IsPresent) || State == SelectionState.Selected;
public override bool HandlePositionalInput => ShouldBeAlive;
public override bool RemoveWhenNotAlive => false;
- protected SelectionBlueprint(DrawableHitObject hitObject)
+ protected SelectionBlueprint(DrawableHitObject drawableObject)
{
- HitObject = hitObject;
+ DrawableObject = drawableObject;
RelativeSizeAxes = Axes.Both;
@@ -107,7 +107,7 @@ namespace osu.Game.Rulesets.Edit
public bool IsSelected => State == SelectionState.Selected;
- public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => HitObject.ReceivePositionalInputAt(screenSpacePos);
+ public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => DrawableObject.ReceivePositionalInputAt(screenSpacePos);
private bool selectionRequested;
@@ -138,7 +138,7 @@ namespace osu.Game.Rulesets.Edit
protected override bool OnDragStart(DragStartEvent e)
{
- ScreenSpaceMovementStartPosition = HitObject.ToScreenSpace(HitObject.OriginPosition);
+ ScreenSpaceMovementStartPosition = DrawableObject.ToScreenSpace(DrawableObject.OriginPosition);
return true;
}
@@ -151,11 +151,11 @@ namespace osu.Game.Rulesets.Edit
///
/// The screen-space point that causes this to be selected.
///
- public virtual Vector2 SelectionPoint => HitObject.ScreenSpaceDrawQuad.Centre;
+ public virtual Vector2 SelectionPoint => DrawableObject.ScreenSpaceDrawQuad.Centre;
///
/// The screen-space quad that outlines this for selections.
///
- public virtual Quad SelectionQuad => HitObject.ScreenSpaceDrawQuad;
+ public virtual Quad SelectionQuad => DrawableObject.ScreenSpaceDrawQuad;
}
}
diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
index 7f3bfd3b5c..1d9b66f88d 100644
--- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
+++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
@@ -4,6 +4,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Reflection;
+using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.TypeExtensions;
@@ -37,7 +39,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
protected virtual IEnumerable GetSamples() => HitObject.Samples;
private readonly Lazy> nestedHitObjects = new Lazy>();
- public IEnumerable NestedHitObjects => nestedHitObjects.IsValueCreated ? nestedHitObjects.Value : Enumerable.Empty();
+ public IReadOnlyList NestedHitObjects => nestedHitObjects.IsValueCreated ? nestedHitObjects.Value : (IReadOnlyList)Array.Empty();
///
/// Invoked when a has been applied by this or a nested .
@@ -76,6 +78,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
///
public JudgementResult Result { get; private set; }
+ private Bindable startTimeBindable;
private Bindable comboIndexBindable;
public override bool RemoveWhenNotAlive => false;
@@ -88,9 +91,9 @@ namespace osu.Game.Rulesets.Objects.Drawables
public IBindable State => state;
- protected DrawableHitObject(HitObject hitObject)
+ protected DrawableHitObject([NotNull] HitObject hitObject)
{
- HitObject = hitObject;
+ HitObject = hitObject ?? throw new ArgumentNullException(nameof(hitObject));
}
[BackgroundDependencyLoader]
@@ -125,13 +128,83 @@ namespace osu.Game.Rulesets.Objects.Drawables
{
base.LoadComplete();
+ HitObject.DefaultsApplied += onDefaultsApplied;
+
+ startTimeBindable = HitObject.StartTimeBindable.GetBoundCopy();
+ startTimeBindable.BindValueChanged(_ => updateState(ArmedState.Idle, true));
+
if (HitObject is IHasComboInformation combo)
{
comboIndexBindable = combo.ComboIndexBindable.GetBoundCopy();
- comboIndexBindable.BindValueChanged(_ => updateAccentColour());
+ comboIndexBindable.BindValueChanged(_ => updateAccentColour(), true);
}
updateState(ArmedState.Idle, true);
+ onDefaultsApplied();
+ }
+
+ private void onDefaultsApplied() => apply(HitObject);
+
+ private void apply(HitObject hitObject)
+ {
+#pragma warning disable 618 // can be removed 20200417
+ if (GetType().GetMethod(nameof(AddNested), BindingFlags.NonPublic | BindingFlags.Instance)?.DeclaringType != typeof(DrawableHitObject))
+ return;
+#pragma warning restore 618
+
+ if (nestedHitObjects.IsValueCreated)
+ {
+ nestedHitObjects.Value.Clear();
+ ClearNestedHitObjects();
+ }
+
+ foreach (var h in hitObject.NestedHitObjects)
+ {
+ var drawableNested = CreateNestedHitObject(h) ?? throw new InvalidOperationException($"{nameof(CreateNestedHitObject)} returned null for {h.GetType().ReadableName()}.");
+
+ addNested(drawableNested);
+ AddNestedHitObject(drawableNested);
+ }
+ }
+
+ ///
+ /// Invoked by the base to add nested s to the hierarchy.
+ ///
+ /// The to be added.
+ protected virtual void AddNestedHitObject(DrawableHitObject hitObject)
+ {
+ }
+
+ ///
+ /// Adds a nested . This should not be used except for legacy nested usages.
+ ///
+ ///
+ [Obsolete("Use AddNestedHitObject() / ClearNestedHitObjects() / CreateNestedHitObject() instead.")] // can be removed 20200417
+ protected virtual void AddNested(DrawableHitObject h) => addNested(h);
+
+ ///
+ /// Invoked by the base to remove all previously-added nested s.
+ ///
+ protected virtual void ClearNestedHitObjects()
+ {
+ }
+
+ ///
+ /// Creates the drawable representation for a nested .
+ ///
+ /// The .
+ /// The drawable representation for .
+ protected virtual DrawableHitObject CreateNestedHitObject(HitObject hitObject) => null;
+
+ private void addNested(DrawableHitObject hitObject)
+ {
+ // Todo: Exists for legacy purposes, can be removed 20200417
+
+ hitObject.OnNewResult += (d, r) => OnNewResult?.Invoke(d, r);
+ hitObject.OnRevertResult += (d, r) => OnRevertResult?.Invoke(d, r);
+ hitObject.ApplyCustomUpdateState += (d, j) => ApplyCustomUpdateState?.Invoke(d, j);
+
+ nestedHitObjects.Value.Add(hitObject);
}
#region State / Transform Management
@@ -356,15 +429,6 @@ namespace osu.Game.Rulesets.Objects.Drawables
UpdateResult(false);
}
- protected virtual void AddNested(DrawableHitObject h)
- {
- h.OnNewResult += (d, r) => OnNewResult?.Invoke(d, r);
- h.OnRevertResult += (d, r) => OnRevertResult?.Invoke(d, r);
- h.ApplyCustomUpdateState += (d, j) => ApplyCustomUpdateState?.Invoke(d, j);
-
- nestedHitObjects.Value.Add(h);
- }
-
///
/// Applies the of this , notifying responders such as
/// the of the .
@@ -437,6 +501,12 @@ namespace osu.Game.Rulesets.Objects.Drawables
///
/// The that provides the scoring information.
protected virtual JudgementResult CreateResult(Judgement judgement) => new JudgementResult(HitObject, judgement);
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+ HitObject.DefaultsApplied -= onDefaultsApplied;
+ }
}
public abstract class DrawableHitObject : DrawableHitObject
diff --git a/osu.Game/Rulesets/Objects/HitObject.cs b/osu.Game/Rulesets/Objects/HitObject.cs
index eb8652443f..6211fe50e6 100644
--- a/osu.Game/Rulesets/Objects/HitObject.cs
+++ b/osu.Game/Rulesets/Objects/HitObject.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
@@ -28,6 +29,11 @@ namespace osu.Game.Rulesets.Objects
///
private const double control_point_leniency = 1;
+ ///
+ /// Invoked after has completed on this .
+ ///
+ public event Action DefaultsApplied;
+
public readonly Bindable StartTimeBindable = new Bindable();
///
@@ -113,6 +119,8 @@ namespace osu.Game.Rulesets.Objects
foreach (var h in nestedHitObjects)
h.ApplyDefaults(controlPointInfo, difficulty);
+
+ DefaultsApplied?.Invoke();
}
protected virtual void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
diff --git a/osu.Game/Rulesets/RulesetStore.cs b/osu.Game/Rulesets/RulesetStore.cs
index 47aad43966..23988ff0ff 100644
--- a/osu.Game/Rulesets/RulesetStore.cs
+++ b/osu.Game/Rulesets/RulesetStore.cs
@@ -11,28 +11,22 @@ using osu.Game.Database;
namespace osu.Game.Rulesets
{
- ///
- /// Todo: All of this needs to be moved to a RulesetStore.
- ///
- public class RulesetStore : DatabaseBackedStore
+ public class RulesetStore : DatabaseBackedStore, IDisposable
{
- private static readonly Dictionary loaded_assemblies = new Dictionary();
+ private const string ruleset_library_prefix = "osu.Game.Rulesets";
- static RulesetStore()
- {
- AppDomain.CurrentDomain.AssemblyResolve += currentDomain_AssemblyResolve;
-
- // On android in release configuration assemblies are loaded from the apk directly into memory.
- // We cannot read assemblies from cwd, so should check loaded assemblies instead.
- loadFromAppDomain();
-
- loadFromDisk();
- }
+ private readonly Dictionary loadedAssemblies = new Dictionary();
public RulesetStore(IDatabaseContextFactory factory)
: base(factory)
{
+ // On android in release configuration assemblies are loaded from the apk directly into memory.
+ // We cannot read assemblies from cwd, so should check loaded assemblies instead.
+ loadFromAppDomain();
+ loadFromDisk();
addMissingRulesets();
+
+ AppDomain.CurrentDomain.AssemblyResolve += resolveRulesetAssembly;
}
///
@@ -54,9 +48,7 @@ namespace osu.Game.Rulesets
///
public IEnumerable AvailableRulesets { get; private set; }
- private static Assembly currentDomain_AssemblyResolve(object sender, ResolveEventArgs args) => loaded_assemblies.Keys.FirstOrDefault(a => a.FullName == args.Name);
-
- private const string ruleset_library_prefix = "osu.Game.Rulesets";
+ private Assembly resolveRulesetAssembly(object sender, ResolveEventArgs args) => loadedAssemblies.Keys.FirstOrDefault(a => a.FullName == args.Name);
private void addMissingRulesets()
{
@@ -64,7 +56,7 @@ namespace osu.Game.Rulesets
{
var context = usage.Context;
- var instances = loaded_assemblies.Values.Select(r => (Ruleset)Activator.CreateInstance(r, (RulesetInfo)null)).ToList();
+ var instances = loadedAssemblies.Values.Select(r => (Ruleset)Activator.CreateInstance(r, (RulesetInfo)null)).ToList();
//add all legacy modes in correct order
foreach (var r in instances.Where(r => r.LegacyID != null).OrderBy(r => r.LegacyID))
@@ -113,7 +105,7 @@ namespace osu.Game.Rulesets
}
}
- private static void loadFromAppDomain()
+ private void loadFromAppDomain()
{
foreach (var ruleset in AppDomain.CurrentDomain.GetAssemblies())
{
@@ -126,7 +118,7 @@ namespace osu.Game.Rulesets
}
}
- private static void loadFromDisk()
+ private void loadFromDisk()
{
try
{
@@ -141,11 +133,11 @@ namespace osu.Game.Rulesets
}
}
- private static void loadRulesetFromFile(string file)
+ private void loadRulesetFromFile(string file)
{
var filename = Path.GetFileNameWithoutExtension(file);
- if (loaded_assemblies.Values.Any(t => t.Namespace == filename))
+ if (loadedAssemblies.Values.Any(t => t.Namespace == filename))
return;
try
@@ -158,19 +150,30 @@ namespace osu.Game.Rulesets
}
}
- private static void addRuleset(Assembly assembly)
+ private void addRuleset(Assembly assembly)
{
- if (loaded_assemblies.ContainsKey(assembly))
+ if (loadedAssemblies.ContainsKey(assembly))
return;
try
{
- loaded_assemblies[assembly] = assembly.GetTypes().First(t => t.IsPublic && t.IsSubclassOf(typeof(Ruleset)));
+ loadedAssemblies[assembly] = assembly.GetTypes().First(t => t.IsPublic && t.IsSubclassOf(typeof(Ruleset)));
}
catch (Exception e)
{
Logger.Error(e, $"Failed to add ruleset {assembly}");
}
}
+
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ AppDomain.CurrentDomain.AssemblyResolve -= resolveRulesetAssembly;
+ }
}
}
diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs
index 2de5ecf633..4001a0f33a 100644
--- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
@@ -14,20 +15,20 @@ using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
+using osuTK;
namespace osu.Game.Screens.Edit.Compose.Components
{
public class BlueprintContainer : CompositeDrawable
{
- private SelectionBlueprintContainer selectionBlueprints;
+ public event Action> SelectionChanged;
+ private SelectionBlueprintContainer selectionBlueprints;
private Container placementBlueprintContainer;
private PlacementBlueprint currentPlacement;
private SelectionHandler selectionHandler;
private InputManager inputManager;
- private IEnumerable selections => selectionBlueprints.Children.Where(c => c.IsAlive);
-
[Resolved]
private HitObjectComposer composer { get; set; }
@@ -101,7 +102,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
private void removeBlueprintFor(HitObject hitObject)
{
- var blueprint = selectionBlueprints.Single(m => m.HitObject.HitObject == hitObject);
+ var blueprint = selectionBlueprints.Single(m => m.DrawableObject.HitObject == hitObject);
if (blueprint == null)
return;
@@ -143,7 +144,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
{
if (currentPlacement != null)
{
- currentPlacement.UpdatePosition(e.ScreenSpaceMousePosition);
+ updatePlacementPosition(e.ScreenSpaceMousePosition);
return true;
}
@@ -178,19 +179,27 @@ namespace osu.Game.Screens.Edit.Compose.Components
placementBlueprintContainer.Child = currentPlacement = blueprint;
// Fixes a 1-frame position discrepancy due to the first mouse move event happening in the next frame
- blueprint.UpdatePosition(inputManager.CurrentState.Mouse.Position);
+ updatePlacementPosition(inputManager.CurrentState.Mouse.Position);
}
}
+ private void updatePlacementPosition(Vector2 screenSpacePosition)
+ {
+ Vector2 snappedGridPosition = composer.GetSnappedPosition(ToLocalSpace(screenSpacePosition));
+ Vector2 snappedScreenSpacePosition = ToScreenSpace(snappedGridPosition);
+
+ currentPlacement.UpdatePosition(snappedScreenSpacePosition);
+ }
+
///
/// Select all masks in a given rectangle selection area.
///
/// The rectangle to perform a selection on in screen-space coordinates.
private void select(RectangleF rect)
{
- foreach (var blueprint in selections.ToList())
+ foreach (var blueprint in selectionBlueprints)
{
- if (blueprint.IsPresent && rect.Contains(blueprint.SelectionPoint))
+ if (blueprint.IsAlive && blueprint.IsPresent && rect.Contains(blueprint.SelectionPoint))
blueprint.Select();
else
blueprint.Deselect();
@@ -200,27 +209,40 @@ namespace osu.Game.Screens.Edit.Compose.Components
///
/// Deselects all selected s.
///
- private void deselectAll() => selections.ToList().ForEach(m => m.Deselect());
+ private void deselectAll() => selectionHandler.SelectedBlueprints.ToList().ForEach(m => m.Deselect());
private void onBlueprintSelected(SelectionBlueprint blueprint)
{
selectionHandler.HandleSelected(blueprint);
selectionBlueprints.ChangeChildDepth(blueprint, 1);
+
+ SelectionChanged?.Invoke(selectionHandler.SelectedHitObjects);
}
private void onBlueprintDeselected(SelectionBlueprint blueprint)
{
selectionHandler.HandleDeselected(blueprint);
selectionBlueprints.ChangeChildDepth(blueprint, 0);
+
+ SelectionChanged?.Invoke(selectionHandler.SelectedHitObjects);
}
private void onSelectionRequested(SelectionBlueprint blueprint, InputState state) => selectionHandler.HandleSelectionRequested(blueprint, state);
private void onDragRequested(SelectionBlueprint blueprint, DragEvent dragEvent)
{
- var movePosition = blueprint.ScreenSpaceMovementStartPosition + dragEvent.ScreenSpaceMousePosition - dragEvent.ScreenSpaceMouseDownPosition;
+ HitObject draggedObject = blueprint.DrawableObject.HitObject;
- selectionHandler.HandleMovement(new MoveSelectionEvent(blueprint, blueprint.ScreenSpaceMovementStartPosition, movePosition));
+ Vector2 movePosition = blueprint.ScreenSpaceMovementStartPosition + dragEvent.ScreenSpaceMousePosition - dragEvent.ScreenSpaceMouseDownPosition;
+ Vector2 snappedPosition = composer.GetSnappedPosition(ToLocalSpace(movePosition));
+
+ // Move the hitobjects
+ selectionHandler.HandleMovement(new MoveSelectionEvent(blueprint, blueprint.ScreenSpaceMovementStartPosition, ToScreenSpace(snappedPosition)));
+
+ // Apply the start time at the newly snapped-to position
+ double offset = composer.GetSnappedTime(draggedObject.StartTime, snappedPosition) - draggedObject.StartTime;
+ foreach (HitObject obj in selectionHandler.SelectedHitObjects)
+ obj.StartTime += offset;
}
protected override void Dispose(bool isDisposing)
@@ -252,7 +274,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
return d;
// Put earlier hitobjects towards the end of the list, so they handle input first
- int i = y.HitObject.HitObject.StartTime.CompareTo(x.HitObject.HitObject.StartTime);
+ int i = y.DrawableObject.HitObject.StartTime.CompareTo(x.DrawableObject.HitObject.StartTime);
return i == 0 ? CompareReverseChildID(x, y) : i;
}
}
diff --git a/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs
new file mode 100644
index 0000000000..3cbf926d4f
--- /dev/null
+++ b/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs
@@ -0,0 +1,59 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.UserInterface;
+using osu.Game.Rulesets.Objects;
+using osuTK;
+
+namespace osu.Game.Screens.Edit.Compose.Components
+{
+ public abstract class CircularDistanceSnapGrid : DistanceSnapGrid
+ {
+ protected CircularDistanceSnapGrid(HitObject hitObject, Vector2 centrePosition)
+ : base(hitObject, centrePosition)
+ {
+ }
+
+ protected override void CreateContent(Vector2 centrePosition)
+ {
+ float dx = Math.Max(centrePosition.X, DrawWidth - centrePosition.X);
+ float dy = Math.Max(centrePosition.Y, DrawHeight - centrePosition.Y);
+ float maxDistance = new Vector2(dx, dy).Length;
+
+ int requiredCircles = (int)(maxDistance / DistanceSpacing);
+
+ for (int i = 0; i < requiredCircles; i++)
+ {
+ float radius = (i + 1) * DistanceSpacing * 2;
+
+ AddInternal(new CircularProgress
+ {
+ Origin = Anchor.Centre,
+ Position = centrePosition,
+ Current = { Value = 1 },
+ Size = new Vector2(radius),
+ InnerRadius = 4 * 1f / radius,
+ Colour = GetColourForBeatIndex(i)
+ });
+ }
+ }
+
+ public override Vector2 GetSnapPosition(Vector2 position)
+ {
+ Vector2 direction = position - CentrePosition;
+
+ if (direction == Vector2.Zero)
+ direction = new Vector2(0.001f, 0.001f);
+
+ float distance = direction.Length;
+
+ float radius = DistanceSpacing;
+ int radialCount = Math.Max(1, (int)Math.Round(distance / radius));
+
+ Vector2 normalisedDirection = direction * new Vector2(1f / distance);
+ return CentrePosition + normalisedDirection * radialCount * radius;
+ }
+ }
+}
diff --git a/osu.Game/Screens/Edit/Compose/Components/BeatSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs
similarity index 92%
rename from osu.Game/Screens/Edit/Compose/Components/BeatSnapGrid.cs
rename to osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs
index 9040843144..299e78b7c0 100644
--- a/osu.Game/Screens/Edit/Compose/Components/BeatSnapGrid.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs
@@ -15,7 +15,10 @@ using osuTK;
namespace osu.Game.Screens.Edit.Compose.Components
{
- public abstract class BeatSnapGrid : CompositeDrawable
+ ///
+ /// A grid which takes user input and returns a quantized ("snapped") position and time.
+ ///
+ public abstract class DistanceSnapGrid : CompositeDrawable
{
///
/// The velocity of the beatmap at the point of placement in pixels per millisecond.
@@ -48,7 +51,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
private double startTime;
private double beatLength;
- protected BeatSnapGrid(HitObject hitObject, Vector2 centrePosition)
+ protected DistanceSnapGrid(HitObject hitObject, Vector2 centrePosition)
{
this.hitObject = hitObject;
this.CentrePosition = centrePosition;
@@ -114,14 +117,14 @@ namespace osu.Game.Screens.Edit.Compose.Components
///
/// Snaps a position to this grid.
///
- /// The original position in coordinate space local to this .
- /// The snapped position in coordinate space local to this .
+ /// The original position in coordinate space local to this .
+ /// The snapped position in coordinate space local to this .
public abstract Vector2 GetSnapPosition(Vector2 position);
///
/// Retrieves the time at a snapped position.
///
- /// The snapped position in coordinate space local to this .
+ /// The snapped position in coordinate space local to this .
/// The time at the snapped position.
public double GetSnapTime(Vector2 position) => startTime + (position - CentrePosition).Length / Velocity;
diff --git a/osu.Game/Screens/Edit/Compose/Components/MoveSelectionEvent.cs b/osu.Game/Screens/Edit/Compose/Components/MoveSelectionEvent.cs
index 13945381bb..fe0a47aec8 100644
--- a/osu.Game/Screens/Edit/Compose/Components/MoveSelectionEvent.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/MoveSelectionEvent.cs
@@ -40,7 +40,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
ScreenSpaceStartPosition = screenSpaceStartPosition;
ScreenSpacePosition = screenSpacePosition;
- InstantDelta = Blueprint.HitObject.Parent.ToLocalSpace(ScreenSpacePosition) - Blueprint.HitObject.Position;
+ InstantDelta = Blueprint.DrawableObject.Parent.ToLocalSpace(ScreenSpacePosition) - Blueprint.DrawableObject.Position;
}
}
}
diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs
index c9e862d99e..d7821eff07 100644
--- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs
@@ -26,10 +26,10 @@ namespace osu.Game.Screens.Edit.Compose.Components
{
public const float BORDER_RADIUS = 2;
- protected IEnumerable SelectedBlueprints => selectedBlueprints;
+ public IEnumerable SelectedBlueprints => selectedBlueprints;
private readonly List selectedBlueprints;
- protected IEnumerable SelectedHitObjects => selectedBlueprints.Select(b => b.HitObject.HitObject);
+ public IEnumerable SelectedHitObjects => selectedBlueprints.Select(b => b.DrawableObject.HitObject);
private Drawable outline;
@@ -81,7 +81,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
{
case Key.Delete:
foreach (var h in selectedBlueprints.ToList())
- placementHandler.Delete(h.HitObject.HitObject);
+ placementHandler.Delete(h.DrawableObject.HitObject);
return true;
}
diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs
index 7f08c2f8b9..35408e4003 100644
--- a/osu.Game/Screens/Edit/Editor.cs
+++ b/osu.Game/Screens/Edit/Editor.cs
@@ -173,6 +173,12 @@ namespace osu.Game.Screens.Edit
bottomBackground.Colour = colours.Gray2;
}
+ protected override void Update()
+ {
+ base.Update();
+ clock.ProcessFrame();
+ }
+
protected override bool OnKeyDown(KeyDownEvent e)
{
switch (e.Key)
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index ab7c40116b..64172a0954 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -26,7 +26,7 @@
-
+
diff --git a/osu.iOS.props b/osu.iOS.props
index 925a217a13..5d384888d2 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -118,8 +118,8 @@
-
-
+
+