1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-14 02:22:56 +08:00

Merge branch 'master' into mbd-beatmap-set-cover

This commit is contained in:
Bartłomiej Dach 2021-05-03 15:58:13 +02:00
commit f52375eed2
57 changed files with 1585 additions and 612 deletions

View File

@ -0,0 +1,80 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Allocation;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Tests.Visual;
using osu.Framework.Timing;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Configuration;
using osu.Framework.Bindables;
namespace osu.Game.Rulesets.Mania.Tests
{
[TestFixture]
public class TestSceneTimingBasedNoteColouring : OsuTestScene
{
[Resolved]
private RulesetConfigCache configCache { get; set; }
private readonly Bindable<bool> configTimingBasedNoteColouring = new Bindable<bool>();
protected override void LoadComplete()
{
const double beat_length = 500;
var ruleset = new ManiaRuleset();
var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 1 })
{
HitObjects =
{
new Note { StartTime = 0 },
new Note { StartTime = beat_length / 16 },
new Note { StartTime = beat_length / 12 },
new Note { StartTime = beat_length / 8 },
new Note { StartTime = beat_length / 6 },
new Note { StartTime = beat_length / 4 },
new Note { StartTime = beat_length / 3 },
new Note { StartTime = beat_length / 2 },
new Note { StartTime = beat_length }
},
ControlPointInfo = new ControlPointInfo(),
BeatmapInfo = { Ruleset = ruleset.RulesetInfo },
};
foreach (var note in beatmap.HitObjects)
{
note.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
}
beatmap.ControlPointInfo.Add(0, new TimingControlPoint
{
BeatLength = beat_length
});
Child = new Container
{
Clock = new FramedClock(new ManualClock()),
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Children = new[]
{
ruleset.CreateDrawableRulesetWith(beatmap)
}
};
var config = (ManiaRulesetConfigManager)configCache.GetConfigFor(Ruleset.Value.CreateInstance());
config.BindWith(ManiaRulesetSetting.TimingBasedNoteColouring, configTimingBasedNoteColouring);
AddStep("Enable", () => configTimingBasedNoteColouring.Value = true);
AddStep("Disable", () => configTimingBasedNoteColouring.Value = false);
}
}
}

View File

@ -22,6 +22,7 @@ namespace osu.Game.Rulesets.Mania.Configuration
SetDefault(ManiaRulesetSetting.ScrollTime, 1500.0, DrawableManiaRuleset.MIN_TIME_RANGE, DrawableManiaRuleset.MAX_TIME_RANGE, 5);
SetDefault(ManiaRulesetSetting.ScrollDirection, ManiaScrollingDirection.Down);
SetDefault(ManiaRulesetSetting.TimingBasedNoteColouring, false);
}
public override TrackedSettings CreateTrackedSettings() => new TrackedSettings
@ -34,6 +35,7 @@ namespace osu.Game.Rulesets.Mania.Configuration
public enum ManiaRulesetSetting
{
ScrollTime,
ScrollDirection
ScrollDirection,
TimingBasedNoteColouring
}
}

View File

@ -4,6 +4,7 @@
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Edit.Blueprints;
using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Screens.Edit.Compose.Components;
@ -30,6 +31,6 @@ namespace osu.Game.Rulesets.Mania.Edit
return base.CreateBlueprintFor(hitObject);
}
protected override SelectionHandler CreateSelectionHandler() => new ManiaSelectionHandler();
protected override SelectionHandler<HitObject> CreateSelectionHandler() => new ManiaSelectionHandler();
}
}

View File

@ -7,12 +7,13 @@ using osu.Framework.Allocation;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Edit.Blueprints;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Screens.Edit.Compose.Components;
namespace osu.Game.Rulesets.Mania.Edit
{
public class ManiaSelectionHandler : SelectionHandler
public class ManiaSelectionHandler : EditorSelectionHandler
{
[Resolved]
private IScrollingInfo scrollingInfo { get; set; }
@ -20,7 +21,7 @@ namespace osu.Game.Rulesets.Mania.Edit
[Resolved]
private HitObjectComposer composer { get; set; }
public override bool HandleMovement(MoveSelectionEvent moveEvent)
public override bool HandleMovement(MoveSelectionEvent<HitObject> moveEvent)
{
var maniaBlueprint = (ManiaSelectionBlueprint)moveEvent.Blueprint;
int lastColumn = maniaBlueprint.DrawableObject.HitObject.Column;
@ -30,11 +31,11 @@ namespace osu.Game.Rulesets.Mania.Edit
return true;
}
private void performColumnMovement(int lastColumn, MoveSelectionEvent moveEvent)
private void performColumnMovement(int lastColumn, MoveSelectionEvent<HitObject> moveEvent)
{
var maniaPlayfield = ((ManiaHitObjectComposer)composer).Playfield;
var currentColumn = maniaPlayfield.GetColumnByPosition(moveEvent.ScreenSpacePosition);
var currentColumn = maniaPlayfield.GetColumnByPosition(moveEvent.Blueprint.ScreenSpaceSelectionPoint + moveEvent.ScreenSpaceDelta);
if (currentColumn == null)
return;

View File

@ -37,6 +37,11 @@ namespace osu.Game.Rulesets.Mania
Current = config.GetBindable<double>(ManiaRulesetSetting.ScrollTime),
KeyboardStep = 5
},
new SettingsCheckbox
{
LabelText = "Timing-based note colouring",
Current = config.GetBindable<bool>(ManiaRulesetSetting.TimingBasedNoteColouring),
}
};
}

View File

@ -2,13 +2,19 @@
// See the LICENCE file in the repository root for full licence text.
using System.Diagnostics;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Input.Bindings;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Rulesets.Mania.Configuration;
using osu.Game.Rulesets.Mania.Skinning.Default;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Screens.Edit;
using osu.Game.Skinning;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Mania.Objects.Drawables
{
@ -17,6 +23,14 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
/// </summary>
public class DrawableNote : DrawableManiaHitObject<Note>, IKeyBindingHandler<ManiaAction>
{
[Resolved]
private OsuColour colours { get; set; }
[Resolved(canBeNull: true)]
private IBeatmap beatmap { get; set; }
private readonly Bindable<bool> configTimingBasedNoteColouring = new Bindable<bool>();
protected virtual ManiaSkinComponents Component => ManiaSkinComponents.Note;
private readonly Drawable headPiece;
@ -34,6 +48,18 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
});
}
[BackgroundDependencyLoader(true)]
private void load(ManiaRulesetConfigManager rulesetConfig)
{
rulesetConfig?.BindWith(ManiaRulesetSetting.TimingBasedNoteColouring, configTimingBasedNoteColouring);
}
protected override void LoadComplete()
{
HitObject.StartTimeBindable.BindValueChanged(_ => updateSnapColour());
configTimingBasedNoteColouring.BindValueChanged(_ => updateSnapColour(), true);
}
protected override void OnDirectionChanged(ValueChangedEvent<ScrollingDirection> e)
{
base.OnDirectionChanged(e);
@ -73,5 +99,14 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
public virtual void OnReleased(ManiaAction action)
{
}
private void updateSnapColour()
{
if (beatmap == null) return;
int snapDivisor = beatmap.ControlPointInfo.GetClosestBeatDivisor(HitObject.StartTime);
Colour = configTimingBasedNoteColouring.Value ? BindableBeatDivisor.GetColourFor(snapDivisor, colours) : Color4.White;
}
}
}

View File

@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables;
using osu.Game.Rulesets.Mania.Objects.Types;
using osu.Game.Rulesets.Mania.Scoring;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;

View File

@ -10,7 +10,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints
public abstract class OsuSelectionBlueprint<T> : OverlaySelectionBlueprint
where T : OsuHitObject
{
protected new T HitObject => (T)DrawableObject.HitObject;
protected T HitObject => (T)DrawableObject.HitObject;
protected override bool AlwaysShowWhenSelected => true;

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Edit;
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;
@ -18,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Edit
{
}
protected override SelectionHandler CreateSelectionHandler() => new OsuSelectionHandler();
protected override SelectionHandler<HitObject> CreateSelectionHandler() => new OsuSelectionHandler();
public override OverlaySelectionBlueprint CreateBlueprintFor(DrawableHitObject hitObject)
{

View File

@ -147,7 +147,7 @@ namespace osu.Game.Rulesets.Osu.Edit
if (b.IsSelected)
continue;
var hitObject = (OsuHitObject)b.HitObject;
var hitObject = (OsuHitObject)b.Item;
Vector2? snap = checkSnap(hitObject.Position);
if (snap == null && hitObject.Position != hitObject.EndPosition)

View File

@ -7,6 +7,7 @@ using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Utils;
using osu.Game.Extensions;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
@ -15,7 +16,7 @@ using osuTK;
namespace osu.Game.Rulesets.Osu.Edit
{
public class OsuSelectionHandler : SelectionHandler
public class OsuSelectionHandler : EditorSelectionHandler
{
protected override void OnSelectionChanged()
{
@ -36,13 +37,13 @@ namespace osu.Game.Rulesets.Osu.Edit
referencePathTypes = null;
}
public override bool HandleMovement(MoveSelectionEvent moveEvent)
public override bool HandleMovement(MoveSelectionEvent<HitObject> moveEvent)
{
var hitObjects = selectedMovableObjects;
// this will potentially move the selection out of bounds...
foreach (var h in hitObjects)
h.Position += moveEvent.InstantDelta;
h.Position += this.ScreenSpaceDeltaToParentSpace(moveEvent.ScreenSpaceDelta);
// but this will be corrected.
moveSelectionInBounds();
@ -374,8 +375,7 @@ namespace osu.Game.Rulesets.Osu.Edit
/// <summary>
/// All osu! hitobjects which can be moved/rotated/scaled.
/// </summary>
private OsuHitObject[] selectedMovableObjects => EditorBeatmap.SelectedHitObjects
.OfType<OsuHitObject>()
private OsuHitObject[] selectedMovableObjects => SelectedItems.OfType<OsuHitObject>()
.Where(h => !(h is Spinner))
.ToArray();

View File

@ -2,53 +2,15 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Game.Configuration;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.UI;
using osuTK;
namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModBarrelRoll : Mod, IUpdatableByPlayfield, IApplicableToDrawableRuleset<OsuHitObject>, IApplicableToDrawableHitObjects
public class OsuModBarrelRoll : ModBarrelRoll<OsuHitObject>, IApplicableToDrawableHitObjects
{
private float currentRotation;
[SettingSource("Roll speed", "Rotations per minute")]
public BindableNumber<double> SpinSpeed { get; } = new BindableDouble(0.5)
{
MinValue = 0.02,
MaxValue = 12,
Precision = 0.01,
};
[SettingSource("Direction", "The direction of rotation")]
public Bindable<RotationDirection> Direction { get; } = new Bindable<RotationDirection>(RotationDirection.Clockwise);
public override string Name => "Barrel Roll";
public override string Acronym => "BR";
public override string Description => "The whole playfield is on a wheel!";
public override double ScoreMultiplier => 1;
public override string SettingDescription => $"{SpinSpeed.Value} rpm {Direction.Value.GetDescription().ToLowerInvariant()}";
public void Update(Playfield playfield)
{
playfield.Rotation = currentRotation = (Direction.Value == RotationDirection.Counterclockwise ? -1 : 1) * 360 * (float)(playfield.Time.Current / 60000 * SpinSpeed.Value);
}
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
{
// scale the playfield to allow all hitobjects to stay within the visible region.
drawableRuleset.Playfield.Scale = new Vector2(OsuPlayfield.BASE_SIZE.Y / OsuPlayfield.BASE_SIZE.X);
}
public void ApplyToDrawableHitObjects(IEnumerable<DrawableHitObject> drawables)
{
foreach (var d in drawables)
@ -58,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Mods
switch (d)
{
case DrawableHitCircle circle:
circle.CirclePiece.Rotation = -currentRotation;
circle.CirclePiece.Rotation = -CurrentRotation;
break;
}
};

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Taiko.Edit.Blueprints;
using osu.Game.Screens.Edit.Compose.Components;
@ -15,7 +16,7 @@ namespace osu.Game.Rulesets.Taiko.Edit
{
}
protected override SelectionHandler CreateSelectionHandler() => new TaikoSelectionHandler();
protected override SelectionHandler<HitObject> CreateSelectionHandler() => new TaikoSelectionHandler();
public override OverlaySelectionBlueprint CreateBlueprintFor(DrawableHitObject hitObject) =>
new TaikoSelectionBlueprint(hitObject);

View File

@ -8,12 +8,13 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Screens.Edit.Compose.Components;
namespace osu.Game.Rulesets.Taiko.Edit
{
public class TaikoSelectionHandler : SelectionHandler
public class TaikoSelectionHandler : EditorSelectionHandler
{
private readonly Bindable<TernaryState> selectionRimState = new Bindable<TernaryState>();
private readonly Bindable<TernaryState> selectionStrongState = new Bindable<TernaryState>();
@ -72,16 +73,19 @@ namespace osu.Game.Rulesets.Taiko.Edit
});
}
protected override IEnumerable<MenuItem> GetContextMenuItemsForSelection(IEnumerable<SelectionBlueprint> selection)
protected override IEnumerable<MenuItem> GetContextMenuItemsForSelection(IEnumerable<SelectionBlueprint<HitObject>> selection)
{
if (selection.All(s => s.HitObject is Hit))
if (selection.All(s => s.Item is Hit))
yield return new TernaryStateMenuItem("Rim") { State = { BindTarget = selectionRimState } };
if (selection.All(s => s.HitObject is TaikoHitObject))
if (selection.All(s => s.Item is TaikoHitObject))
yield return new TernaryStateMenuItem("Strong") { State = { BindTarget = selectionStrongState } };
foreach (var item in base.GetContextMenuItemsForSelection(selection))
yield return item;
}
public override bool HandleMovement(MoveSelectionEvent moveEvent) => true;
public override bool HandleMovement(MoveSelectionEvent<HitObject> moveEvent) => true;
protected override void UpdateTernaryStates()
{

View File

@ -0,0 +1,194 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using Moq;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit.Checks;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
namespace osu.Game.Tests.Editing.Checks
{
[TestFixture]
public class CheckConcurrentObjectsTest
{
private CheckConcurrentObjects check;
[SetUp]
public void Setup()
{
check = new CheckConcurrentObjects();
}
[Test]
public void TestCirclesSeparate()
{
assertOk(new List<HitObject>
{
new HitCircle { StartTime = 100 },
new HitCircle { StartTime = 150 }
});
}
[Test]
public void TestCirclesConcurrent()
{
assertConcurrentSame(new List<HitObject>
{
new HitCircle { StartTime = 100 },
new HitCircle { StartTime = 100 }
});
}
[Test]
public void TestCirclesAlmostConcurrent()
{
assertConcurrentSame(new List<HitObject>
{
new HitCircle { StartTime = 100 },
new HitCircle { StartTime = 101 }
});
}
[Test]
public void TestSlidersSeparate()
{
assertOk(new List<HitObject>
{
getSliderMock(startTime: 100, endTime: 400.75d).Object,
getSliderMock(startTime: 500, endTime: 900.75d).Object
});
}
[Test]
public void TestSlidersConcurrent()
{
assertConcurrentSame(new List<HitObject>
{
getSliderMock(startTime: 100, endTime: 400.75d).Object,
getSliderMock(startTime: 300, endTime: 700.75d).Object
});
}
[Test]
public void TestSlidersAlmostConcurrent()
{
assertConcurrentSame(new List<HitObject>
{
getSliderMock(startTime: 100, endTime: 400.75d).Object,
getSliderMock(startTime: 402, endTime: 902.75d).Object
});
}
[Test]
public void TestSliderAndCircleConcurrent()
{
assertConcurrentDifferent(new List<HitObject>
{
getSliderMock(startTime: 100, endTime: 400.75d).Object,
new HitCircle { StartTime = 300 }
});
}
[Test]
public void TestManyObjectsConcurrent()
{
var hitobjects = new List<HitObject>
{
getSliderMock(startTime: 100, endTime: 400.75d).Object,
getSliderMock(startTime: 200, endTime: 500.75d).Object,
new HitCircle { StartTime = 300 }
};
var issues = check.Run(getPlayableBeatmap(hitobjects), null).ToList();
Assert.That(issues, Has.Count.EqualTo(3));
Assert.That(issues.Where(issue => issue.Template is CheckConcurrentObjects.IssueTemplateConcurrentDifferent).ToList(), Has.Count.EqualTo(2));
Assert.That(issues.Any(issue => issue.Template is CheckConcurrentObjects.IssueTemplateConcurrentSame));
}
[Test]
public void TestHoldNotesSeparateOnSameColumn()
{
assertOk(new List<HitObject>
{
getHoldNoteMock(startTime: 100, endTime: 400.75d, column: 1).Object,
getHoldNoteMock(startTime: 500, endTime: 900.75d, column: 1).Object
});
}
[Test]
public void TestHoldNotesConcurrentOnDifferentColumns()
{
assertOk(new List<HitObject>
{
getHoldNoteMock(startTime: 100, endTime: 400.75d, column: 1).Object,
getHoldNoteMock(startTime: 300, endTime: 700.75d, column: 2).Object
});
}
[Test]
public void TestHoldNotesConcurrentOnSameColumn()
{
assertConcurrentSame(new List<HitObject>
{
getHoldNoteMock(startTime: 100, endTime: 400.75d, column: 1).Object,
getHoldNoteMock(startTime: 300, endTime: 700.75d, column: 1).Object
});
}
private Mock<Slider> getSliderMock(double startTime, double endTime, int repeats = 0)
{
var mock = new Mock<Slider>();
mock.SetupGet(s => s.StartTime).Returns(startTime);
mock.As<IHasRepeats>().Setup(r => r.RepeatCount).Returns(repeats);
mock.As<IHasDuration>().Setup(d => d.EndTime).Returns(endTime);
return mock;
}
private Mock<HoldNote> getHoldNoteMock(double startTime, double endTime, int column)
{
var mock = new Mock<HoldNote>();
mock.SetupGet(s => s.StartTime).Returns(startTime);
mock.As<IHasDuration>().Setup(d => d.EndTime).Returns(endTime);
mock.As<IHasColumn>().Setup(c => c.Column).Returns(column);
return mock;
}
private void assertOk(List<HitObject> hitobjects)
{
Assert.That(check.Run(getPlayableBeatmap(hitobjects), null), Is.Empty);
}
private void assertConcurrentSame(List<HitObject> hitobjects, int count = 1)
{
var issues = check.Run(getPlayableBeatmap(hitobjects), null).ToList();
Assert.That(issues, Has.Count.EqualTo(count));
Assert.That(issues.All(issue => issue.Template is CheckConcurrentObjects.IssueTemplateConcurrentSame));
}
private void assertConcurrentDifferent(List<HitObject> hitobjects, int count = 1)
{
var issues = check.Run(getPlayableBeatmap(hitobjects), null).ToList();
Assert.That(issues, Has.Count.EqualTo(count));
Assert.That(issues.All(issue => issue.Template is CheckConcurrentObjects.IssueTemplateConcurrentDifferent));
}
private IBeatmap getPlayableBeatmap(List<HitObject> hitobjects)
{
return new Beatmap<HitObject>
{
HitObjects = hitobjects
};
}
}
}

View File

@ -0,0 +1,155 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using Moq;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Edit.Checks;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
namespace osu.Game.Tests.Editing.Checks
{
[TestFixture]
public class CheckUnsnappedObjectsTest
{
private CheckUnsnappedObjects check;
private ControlPointInfo cpi;
[SetUp]
public void Setup()
{
check = new CheckUnsnappedObjects();
cpi = new ControlPointInfo();
cpi.Add(100, new TimingControlPoint { BeatLength = 100 });
}
[Test]
public void TestCircleSnapped()
{
assertOk(new List<HitObject>
{
new HitCircle { StartTime = 100 }
});
}
[Test]
public void TestCircleUnsnapped1Ms()
{
assert1Ms(new List<HitObject>
{
new HitCircle { StartTime = 101 }
});
assert1Ms(new List<HitObject>
{
new HitCircle { StartTime = 99 }
});
}
[Test]
public void TestCircleUnsnapped2Ms()
{
assert2Ms(new List<HitObject>
{
new HitCircle { StartTime = 102 }
});
assert2Ms(new List<HitObject>
{
new HitCircle { StartTime = 98 }
});
}
[Test]
public void TestSliderSnapped()
{
// Slider ends are naturally < 1 ms unsnapped because of how SV works.
assertOk(new List<HitObject>
{
getSliderMock(startTime: 100, endTime: 400.75d).Object
});
}
[Test]
public void TestSliderUnsnapped1Ms()
{
assert1Ms(new List<HitObject>
{
getSliderMock(startTime: 101, endTime: 401.75d).Object
}, count: 2);
// End is only off by 0.25 ms, hence count 1.
assert1Ms(new List<HitObject>
{
getSliderMock(startTime: 99, endTime: 399.75d).Object
}, count: 1);
}
[Test]
public void TestSliderUnsnapped2Ms()
{
assert2Ms(new List<HitObject>
{
getSliderMock(startTime: 102, endTime: 402.75d).Object
}, count: 2);
// Start and end are 2 ms and 1.25 ms off respectively, hence two different issues in one object.
var hitobjects = new List<HitObject>
{
getSliderMock(startTime: 98, endTime: 398.75d).Object
};
var issues = check.Run(getPlayableBeatmap(hitobjects), null).ToList();
Assert.That(issues, Has.Count.EqualTo(2));
Assert.That(issues.Any(issue => issue.Template is CheckUnsnappedObjects.IssueTemplateSmallUnsnap));
Assert.That(issues.Any(issue => issue.Template is CheckUnsnappedObjects.IssueTemplateLargeUnsnap));
}
private Mock<Slider> getSliderMock(double startTime, double endTime, int repeats = 0)
{
var mockSlider = new Mock<Slider>();
mockSlider.SetupGet(s => s.StartTime).Returns(startTime);
mockSlider.As<IHasRepeats>().Setup(r => r.RepeatCount).Returns(repeats);
mockSlider.As<IHasDuration>().Setup(d => d.EndTime).Returns(endTime);
return mockSlider;
}
private void assertOk(List<HitObject> hitobjects)
{
Assert.That(check.Run(getPlayableBeatmap(hitobjects), null), Is.Empty);
}
private void assert1Ms(List<HitObject> hitobjects, int count = 1)
{
var issues = check.Run(getPlayableBeatmap(hitobjects), null).ToList();
Assert.That(issues, Has.Count.EqualTo(count));
Assert.That(issues.All(issue => issue.Template is CheckUnsnappedObjects.IssueTemplateSmallUnsnap));
}
private void assert2Ms(List<HitObject> hitobjects, int count = 1)
{
var issues = check.Run(getPlayableBeatmap(hitobjects), null).ToList();
Assert.That(issues, Has.Count.EqualTo(count));
Assert.That(issues.All(issue => issue.Template is CheckUnsnappedObjects.IssueTemplateLargeUnsnap));
}
private IBeatmap getPlayableBeatmap(List<HitObject> hitobjects)
{
return new Beatmap<HitObject>
{
ControlPointInfo = cpi,
HitObjects = hitobjects
};
}
}
}

View File

@ -0,0 +1,91 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Objects;
namespace osu.Game.Tests.NonVisual
{
public class ClosestBeatDivisorTest
{
[Test]
public void TestExactDivisors()
{
var cpi = new ControlPointInfo();
cpi.Add(-1000, new TimingControlPoint { BeatLength = 1000 });
double[] divisors = { 3, 1, 16, 12, 8, 6, 4, 3, 2, 1 };
assertClosestDivisors(divisors, divisors, cpi);
}
[Test]
public void TestExactDivisorWithTempoChanges()
{
int offset = 0;
int[] beatLengths = { 1000, 200, 100, 50 };
var cpi = new ControlPointInfo();
foreach (int beatLength in beatLengths)
{
cpi.Add(offset, new TimingControlPoint { BeatLength = beatLength });
offset += beatLength * 2;
}
double[] divisors = { 3, 1, 16, 12, 8, 6, 4, 3 };
assertClosestDivisors(divisors, divisors, cpi);
}
[Test]
public void TestExactDivisorsHighBPMStream()
{
var cpi = new ControlPointInfo();
cpi.Add(-50, new TimingControlPoint { BeatLength = 50 }); // 1200 BPM 1/4 (limit testing)
// A 1/4 stream should land on 1/1, 1/2 and 1/4 divisors.
double[] divisors = { 4, 4, 4, 4, 4, 4, 4, 4 };
double[] closestDivisors = { 4, 2, 4, 1, 4, 2, 4, 1 };
assertClosestDivisors(divisors, closestDivisors, cpi, step: 1 / 4d);
}
[Test]
public void TestApproximateDivisors()
{
var cpi = new ControlPointInfo();
cpi.Add(-1000, new TimingControlPoint { BeatLength = 1000 });
double[] divisors = { 3.03d, 0.97d, 14, 13, 7.94d, 6.08d, 3.93d, 2.96d, 2.02d, 64 };
double[] closestDivisors = { 3, 1, 16, 12, 8, 6, 4, 3, 2, 1 };
assertClosestDivisors(divisors, closestDivisors, cpi);
}
private void assertClosestDivisors(IReadOnlyList<double> divisors, IReadOnlyList<double> closestDivisors, ControlPointInfo cpi, double step = 1)
{
List<HitObject> hitobjects = new List<HitObject>();
double offset = cpi.TimingPoints[0].Time;
for (int i = 0; i < divisors.Count; ++i)
{
double beatLength = cpi.TimingPointAt(offset).BeatLength;
hitobjects.Add(new HitObject { StartTime = offset + beatLength / divisors[i] });
offset += beatLength * step;
}
var beatmap = new Beatmap
{
HitObjects = hitobjects,
ControlPointInfo = cpi
};
for (int i = 0; i < divisors.Count; ++i)
Assert.AreEqual(closestDivisors[i], beatmap.ControlPointInfo.GetClosestBeatDivisor(beatmap.HitObjects[i].StartTime), $"at index {i}");
}
}
}

View File

@ -23,8 +23,8 @@ namespace osu.Game.Tests.Visual.Editing
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false);
private BlueprintContainer blueprintContainer
=> Editor.ChildrenOfType<BlueprintContainer>().First();
private EditorBlueprintContainer blueprintContainer
=> Editor.ChildrenOfType<EditorBlueprintContainer>().First();
[Test]
public void TestSelectedObjectHasPriorityWhenOverlapping()

View File

@ -132,8 +132,8 @@ namespace osu.Game.Tests.Visual.Editing
{
AddStep("deselect", () => EditorBeatmap.SelectedHitObjects.Clear());
AddUntilStep("timeline selection box is not visible", () => Editor.ChildrenOfType<Timeline>().First().ChildrenOfType<SelectionHandler>().First().Alpha == 0);
AddUntilStep("composer selection box is not visible", () => Editor.ChildrenOfType<HitObjectComposer>().First().ChildrenOfType<SelectionHandler>().First().Alpha == 0);
AddUntilStep("timeline selection box is not visible", () => Editor.ChildrenOfType<Timeline>().First().ChildrenOfType<EditorSelectionHandler>().First().Alpha == 0);
AddUntilStep("composer selection box is not visible", () => Editor.ChildrenOfType<HitObjectComposer>().First().ChildrenOfType<EditorSelectionHandler>().First().Alpha == 0);
}
AddStep("paste hitobject", () => Editor.Paste());
@ -142,8 +142,8 @@ namespace osu.Game.Tests.Visual.Editing
AddAssert("new object selected", () => EditorBeatmap.SelectedHitObjects.Single().StartTime == 2000);
AddUntilStep("timeline selection box is visible", () => Editor.ChildrenOfType<Timeline>().First().ChildrenOfType<SelectionHandler>().First().Alpha > 0);
AddUntilStep("composer selection box is visible", () => Editor.ChildrenOfType<HitObjectComposer>().First().ChildrenOfType<SelectionHandler>().First().Alpha > 0);
AddUntilStep("timeline selection box is visible", () => Editor.ChildrenOfType<Timeline>().First().ChildrenOfType<EditorSelectionHandler>().First().Alpha > 0);
AddUntilStep("composer selection box is visible", () => Editor.ChildrenOfType<HitObjectComposer>().First().ChildrenOfType<EditorSelectionHandler>().First().Alpha > 0);
}
[Test]

View File

@ -26,15 +26,15 @@ namespace osu.Game.Tests.Visual.Editing
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false);
private BlueprintContainer blueprintContainer
=> Editor.ChildrenOfType<BlueprintContainer>().First();
private EditorBlueprintContainer blueprintContainer
=> Editor.ChildrenOfType<EditorBlueprintContainer>().First();
private void moveMouseToObject(Func<HitObject> targetFunc)
{
AddStep("move mouse to object", () =>
{
var pos = blueprintContainer.SelectionBlueprints
.First(s => s.HitObject == targetFunc())
.First(s => s.Item == targetFunc())
.ChildrenOfType<HitCirclePiece>()
.First().ScreenSpaceDrawQuad.Centre;

View File

@ -14,4 +14,10 @@ namespace osu.Game.Beatmaps
Qualified = 3,
Loved = 4,
}
public static class BeatmapSetOnlineStatusExtensions
{
public static bool GrantsPerformancePoints(this BeatmapSetOnlineStatus status)
=> status == BeatmapSetOnlineStatus.Ranked || status == BeatmapSetOnlineStatus.Approved;
}
}

View File

@ -7,6 +7,8 @@ using System.Linq;
using Newtonsoft.Json;
using osu.Framework.Bindables;
using osu.Framework.Lists;
using osu.Framework.Utils;
using osu.Game.Screens.Edit;
namespace osu.Game.Beatmaps.ControlPoints
{
@ -160,6 +162,58 @@ namespace osu.Game.Beatmaps.ControlPoints
groups.Remove(group);
}
/// <summary>
/// Returns the time on the given beat divisor closest to the given time.
/// </summary>
/// <param name="time">The time to find the closest snapped time to.</param>
/// <param name="beatDivisor">The beat divisor to snap to.</param>
/// <param name="referenceTime">An optional reference point to use for timing point lookup.</param>
public double GetClosestSnappedTime(double time, int beatDivisor, double? referenceTime = null)
{
var timingPoint = TimingPointAt(referenceTime ?? time);
return getClosestSnappedTime(timingPoint, time, beatDivisor);
}
/// <summary>
/// Returns the time on *ANY* valid beat divisor, favouring the divisor closest to the given time.
/// </summary>
/// <param name="time">The time to find the closest snapped time to.</param>
public double GetClosestSnappedTime(double time) => GetClosestSnappedTime(time, GetClosestBeatDivisor(time));
/// <summary>
/// Returns the beat snap divisor closest to the given time. If two are equally close, the smallest divisor is returned.
/// </summary>
/// <param name="time">The time to find the closest beat snap divisor to.</param>
/// <param name="referenceTime">An optional reference point to use for timing point lookup.</param>
public int GetClosestBeatDivisor(double time, double? referenceTime = null)
{
TimingControlPoint timingPoint = TimingPointAt(referenceTime ?? time);
int closestDivisor = 0;
double closestTime = double.MaxValue;
foreach (int divisor in BindableBeatDivisor.VALID_DIVISORS)
{
double distanceFromSnap = Math.Abs(time - getClosestSnappedTime(timingPoint, time, divisor));
if (Precision.DefinitelyBigger(closestTime, distanceFromSnap))
{
closestDivisor = divisor;
closestTime = distanceFromSnap;
}
}
return closestDivisor;
}
private static double getClosestSnappedTime(TimingControlPoint timingPoint, double time, int beatDivisor)
{
var beatLength = timingPoint.BeatLength / beatDivisor;
var beatLengths = (int)Math.Round((time - timingPoint.Time) / beatLength, MidpointRounding.AwayFromZero);
return timingPoint.Time + beatLengths * beatLength;
}
/// <summary>
/// Binary searches one of the control point lists to find the active control point at <paramref name="time"/>.
/// Includes logic for returning a specific point when no matching point is found.

View File

@ -2,8 +2,10 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Graphics;
using osu.Framework.Input.Bindings;
using osu.Framework.Threading;
using osuTK;
namespace osu.Game.Extensions
{
@ -32,5 +34,14 @@ namespace osu.Game.Extensions
scheduler.Add(repeatDelegate);
return repeatDelegate;
}
/// <summary>
/// Accepts a delta vector in screen-space coordinates and converts it to one which can be applied to this drawable's position.
/// </summary>
/// <param name="drawable">The drawable.</param>
/// <param name="delta">A delta in screen-space coordinates.</param>
/// <returns>The delta vector in Parent's coordinates.</returns>
public static Vector2 ScreenSpaceDeltaToParentSpace(this Drawable drawable, Vector2 delta) =>
drawable.Parent.ToLocalSpace(drawable.Parent.ToScreenSpace(Vector2.Zero) + delta);
}
}

View File

@ -47,52 +47,44 @@ namespace osu.Game.Overlays.BeatmapSet.Buttons
{
FillFlowContainer textSprites;
AddRangeInternal(new Drawable[]
AddInternal(shakeContainer = new ShakeContainer
{
shakeContainer = new ShakeContainer
RelativeSizeAxes = Axes.Both,
Masking = true,
CornerRadius = 5,
Child = button = new HeaderButton { RelativeSizeAxes = Axes.Both },
});
button.AddRange(new Drawable[]
{
new Container
{
Depth = -1,
Padding = new MarginPadding { Horizontal = 10 },
RelativeSizeAxes = Axes.Both,
Masking = true,
CornerRadius = 5,
Children = new Drawable[]
{
button = new HeaderButton { RelativeSizeAxes = Axes.Both },
new Container
textSprites = new FillFlowContainer
{
// cannot nest inside here due to the structure of button (putting things in its own content).
// requires framework fix.
Padding = new MarginPadding { Horizontal = 10 },
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
textSprites = new FillFlowContainer
{
Depth = -1,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
AutoSizeAxes = Axes.Both,
AutoSizeDuration = 500,
AutoSizeEasing = Easing.OutQuint,
Direction = FillDirection.Vertical,
},
new SpriteIcon
{
Depth = -1,
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Icon = FontAwesome.Solid.Download,
Size = new Vector2(18),
},
}
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
AutoSizeAxes = Axes.Both,
AutoSizeDuration = 500,
AutoSizeEasing = Easing.OutQuint,
Direction = FillDirection.Vertical,
},
new DownloadProgressBar(BeatmapSet.Value)
new SpriteIcon
{
Depth = -2,
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Icon = FontAwesome.Solid.Download,
Size = new Vector2(18),
},
},
}
},
new DownloadProgressBar(BeatmapSet.Value)
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
},
});

View File

@ -60,7 +60,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
var scoreInfos = value.Scores.Select(s => s.CreateScoreInfo(rulesets)).ToList();
var topScore = scoreInfos.First();
scoreTable.DisplayScores(scoreInfos, topScore.Beatmap?.Status == BeatmapSetOnlineStatus.Ranked);
scoreTable.DisplayScores(scoreInfos, topScore.Beatmap?.Status.GrantsPerformancePoints() == true);
scoreTable.Show();
var userScore = value.UserScore;

View File

@ -111,7 +111,8 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
accuracyColumn.Text = value.DisplayAccuracy;
maxComboColumn.Text = $@"{value.MaxCombo:N0}x";
ppColumn.Alpha = value.Beatmap?.Status == BeatmapSetOnlineStatus.Ranked ? 1 : 0;
ppColumn.Alpha = value.Beatmap?.Status.GrantsPerformancePoints() == true ? 1 : 0;
ppColumn.Text = $@"{value.PP:N0}";
statisticsColumns.ChildrenEnumerable = value.GetStatisticsForDisplay().Select(createStatisticsColumn);

View File

@ -20,7 +20,7 @@ namespace osu.Game.Overlays.Profile.Sections.Beatmaps
private readonly BeatmapSetType type;
public PaginatedBeatmapContainer(BeatmapSetType type, Bindable<User> user, string headerText)
: base(user, headerText, "", CounterVisibilityState.AlwaysVisible)
: base(user, headerText)
{
this.type = type;
ItemsPerPage = 6;

View File

@ -16,7 +16,7 @@ namespace osu.Game.Overlays.Profile.Sections.Historical
public class PaginatedMostPlayedBeatmapContainer : PaginatedProfileSubsection<APIUserMostPlayedBeatmap>
{
public PaginatedMostPlayedBeatmapContainer(Bindable<User> user)
: base(user, "Most Played Beatmaps", "No records. :(", CounterVisibilityState.AlwaysVisible)
: base(user, "Most Played Beatmaps")
{
ItemsPerPage = 5;
}

View File

@ -20,7 +20,7 @@ namespace osu.Game.Overlays.Profile.Sections
{
new PlayHistorySubsection(User),
new PaginatedMostPlayedBeatmapContainer(User),
new PaginatedScoreContainer(ScoreType.Recent, User, "Recent Plays (24h)", CounterVisibilityState.VisibleWhenZero),
new PaginatedScoreContainer(ScoreType.Recent, User, "Recent Plays (24h)"),
new ReplaysSubsection(User)
};
}

View File

@ -38,8 +38,8 @@ namespace osu.Game.Overlays.Profile.Sections
private OsuSpriteText missing;
private readonly string missingText;
protected PaginatedProfileSubsection(Bindable<User> user, string headerText = "", string missingText = "", CounterVisibilityState counterVisibilityState = CounterVisibilityState.AlwaysHidden)
: base(user, headerText, counterVisibilityState)
protected PaginatedProfileSubsection(Bindable<User> user, string headerText = "", string missingText = "")
: base(user, headerText, CounterVisibilityState.AlwaysVisible)
{
this.missingText = missingText;
}

View File

@ -18,8 +18,8 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks
{
private readonly ScoreType type;
public PaginatedScoreContainer(ScoreType type, Bindable<User> user, string headerText, CounterVisibilityState counterVisibilityState, string missingText = "")
: base(user, headerText, missingText, counterVisibilityState)
public PaginatedScoreContainer(ScoreType type, Bindable<User> user, string headerText)
: base(user, headerText)
{
this.type = type;
@ -36,9 +36,15 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks
{
switch (type)
{
case ScoreType.Best:
return user.ScoresBestCount;
case ScoreType.Firsts:
return user.ScoresFirstCount;
case ScoreType.Recent:
return user.ScoresRecentCount;
default:
return 0;
}
@ -50,9 +56,6 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks
drawableItemIndex = 0;
base.OnItemsReceived(items);
if (type == ScoreType.Recent)
SetCount(items.Count);
}
protected override APIRequest<List<APILegacyScoreInfo>> CreateRequest() =>

View File

@ -16,8 +16,8 @@ namespace osu.Game.Overlays.Profile.Sections
{
Children = new[]
{
new PaginatedScoreContainer(ScoreType.Best, User, "Best Performance", CounterVisibilityState.AlwaysHidden, "No performance records. :("),
new PaginatedScoreContainer(ScoreType.Firsts, User, "First Place Ranks", CounterVisibilityState.AlwaysVisible)
new PaginatedScoreContainer(ScoreType.Best, User, "Best Performance"),
new PaginatedScoreContainer(ScoreType.Firsts, User, "First Place Ranks")
};
}
}

View File

@ -22,7 +22,11 @@ namespace osu.Game.Rulesets.Edit
// Audio
new CheckAudioPresence(),
new CheckAudioQuality()
new CheckAudioQuality(),
// Compose
new CheckUnsnappedObjects(),
new CheckConcurrentObjects()
};
public IEnumerable<Issue> Run(IBeatmap playableBeatmap, WorkingBeatmap workingBeatmap)

View File

@ -0,0 +1,88 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit.Checks.Components;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
namespace osu.Game.Rulesets.Edit.Checks
{
public class CheckConcurrentObjects : ICheck
{
// We guarantee that the objects are either treated as concurrent or unsnapped when near the same beat divisor.
private const double ms_leniency = CheckUnsnappedObjects.UNSNAP_MS_THRESHOLD;
public CheckMetadata Metadata { get; } = new CheckMetadata(CheckCategory.Compose, "Concurrent hitobjects");
public IEnumerable<IssueTemplate> PossibleTemplates => new IssueTemplate[]
{
new IssueTemplateConcurrentSame(this),
new IssueTemplateConcurrentDifferent(this)
};
public IEnumerable<Issue> Run(IBeatmap playableBeatmap, IWorkingBeatmap workingBeatmap)
{
for (int i = 0; i < playableBeatmap.HitObjects.Count - 1; ++i)
{
var hitobject = playableBeatmap.HitObjects[i];
for (int j = i + 1; j < playableBeatmap.HitObjects.Count; ++j)
{
var nextHitobject = playableBeatmap.HitObjects[j];
// Accounts for rulesets with hitobjects separated by columns, such as Mania.
// In these cases we only care about concurrent objects within the same column.
if ((hitobject as IHasColumn)?.Column != (nextHitobject as IHasColumn)?.Column)
continue;
// Two hitobjects cannot be concurrent without also being concurrent with all objects in between.
// So if the next object is not concurrent, then we know no future objects will be either.
if (!areConcurrent(hitobject, nextHitobject))
break;
if (hitobject.GetType() == nextHitobject.GetType())
yield return new IssueTemplateConcurrentSame(this).Create(hitobject, nextHitobject);
else
yield return new IssueTemplateConcurrentDifferent(this).Create(hitobject, nextHitobject);
}
}
}
private bool areConcurrent(HitObject hitobject, HitObject nextHitobject) => nextHitobject.StartTime <= hitobject.GetEndTime() + ms_leniency;
public abstract class IssueTemplateConcurrent : IssueTemplate
{
protected IssueTemplateConcurrent(ICheck check, string unformattedMessage)
: base(check, IssueType.Problem, unformattedMessage)
{
}
public Issue Create(HitObject hitobject, HitObject nextHitobject)
{
var hitobjects = new List<HitObject> { hitobject, nextHitobject };
return new Issue(hitobjects, this, hitobject.GetType().Name, nextHitobject.GetType().Name)
{
Time = nextHitobject.StartTime
};
}
}
public class IssueTemplateConcurrentSame : IssueTemplateConcurrent
{
public IssueTemplateConcurrentSame(ICheck check)
: base(check, "{0}s are concurrent here.")
{
}
}
public class IssueTemplateConcurrentDifferent : IssueTemplateConcurrent
{
public IssueTemplateConcurrentDifferent(ICheck check)
: base(check, "{0} and {1} are concurrent here.")
{
}
}
}
}

View File

@ -0,0 +1,100 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit.Checks.Components;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
namespace osu.Game.Rulesets.Edit.Checks
{
public class CheckUnsnappedObjects : ICheck
{
public const double UNSNAP_MS_THRESHOLD = 2;
public CheckMetadata Metadata { get; } = new CheckMetadata(CheckCategory.Timing, "Unsnapped hitobjects");
public IEnumerable<IssueTemplate> PossibleTemplates => new IssueTemplate[]
{
new IssueTemplateLargeUnsnap(this),
new IssueTemplateSmallUnsnap(this)
};
public IEnumerable<Issue> Run(IBeatmap playableBeatmap, IWorkingBeatmap workingBeatmap)
{
var controlPointInfo = playableBeatmap.ControlPointInfo;
foreach (var hitobject in playableBeatmap.HitObjects)
{
double startUnsnap = hitobject.StartTime - controlPointInfo.GetClosestSnappedTime(hitobject.StartTime);
string startPostfix = hitobject is IHasDuration ? "start" : "";
foreach (var issue in getUnsnapIssues(hitobject, startUnsnap, hitobject.StartTime, startPostfix))
yield return issue;
if (hitobject is IHasRepeats hasRepeats)
{
for (int repeatIndex = 0; repeatIndex < hasRepeats.RepeatCount; ++repeatIndex)
{
double spanDuration = hasRepeats.Duration / (hasRepeats.RepeatCount + 1);
double repeatTime = hitobject.StartTime + spanDuration * (repeatIndex + 1);
double repeatUnsnap = repeatTime - controlPointInfo.GetClosestSnappedTime(repeatTime);
foreach (var issue in getUnsnapIssues(hitobject, repeatUnsnap, repeatTime, "repeat"))
yield return issue;
}
}
if (hitobject is IHasDuration hasDuration)
{
double endUnsnap = hasDuration.EndTime - controlPointInfo.GetClosestSnappedTime(hasDuration.EndTime);
foreach (var issue in getUnsnapIssues(hitobject, endUnsnap, hasDuration.EndTime, "end"))
yield return issue;
}
}
}
private IEnumerable<Issue> getUnsnapIssues(HitObject hitobject, double unsnap, double time, string postfix = "")
{
if (Math.Abs(unsnap) >= UNSNAP_MS_THRESHOLD)
yield return new IssueTemplateLargeUnsnap(this).Create(hitobject, unsnap, time, postfix);
else if (Math.Abs(unsnap) >= 1)
yield return new IssueTemplateSmallUnsnap(this).Create(hitobject, unsnap, time, postfix);
// We don't care about unsnaps < 1 ms, as all object ends have these due to the way SV works.
}
public abstract class IssueTemplateUnsnap : IssueTemplate
{
protected IssueTemplateUnsnap(ICheck check, IssueType type)
: base(check, type, "{0} is unsnapped by {1:0.##} ms.")
{
}
public Issue Create(HitObject hitobject, double unsnap, double time, string postfix = "")
{
string objectName = hitobject.GetType().Name;
if (!string.IsNullOrEmpty(postfix))
objectName += " " + postfix;
return new Issue(hitobject, this, objectName, unsnap) { Time = time };
}
}
public class IssueTemplateLargeUnsnap : IssueTemplateUnsnap
{
public IssueTemplateLargeUnsnap(ICheck check)
: base(check, IssueType.Problem)
{
}
}
public class IssueTemplateSmallUnsnap : IssueTemplateUnsnap
{
public IssueTemplateSmallUnsnap(ICheck check)
: base(check, IssueType.Negligible)
{
}
}
}
}

View File

@ -182,8 +182,7 @@ namespace osu.Game.Rulesets.Edit
/// <summary>
/// Construct a relevant blueprint container. This will manage hitobject selection/placement input handling and display logic.
/// </summary>
protected virtual ComposeBlueprintContainer CreateBlueprintContainer()
=> new ComposeBlueprintContainer(this);
protected virtual ComposeBlueprintContainer CreateBlueprintContainer() => new ComposeBlueprintContainer(this);
/// <summary>
/// Construct a drawable ruleset for the provided ruleset.

View File

@ -3,12 +3,13 @@
using osu.Framework.Graphics.Primitives;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osuTK;
namespace osu.Game.Rulesets.Edit
{
public abstract class OverlaySelectionBlueprint : SelectionBlueprint
public abstract class OverlaySelectionBlueprint : SelectionBlueprint<HitObject>
{
/// <summary>
/// The <see cref="DrawableHitObject"/> which this <see cref="OverlaySelectionBlueprint"/> applies to.
@ -33,7 +34,5 @@ namespace osu.Game.Rulesets.Edit
public override Vector2 ScreenSpaceSelectionPoint => DrawableObject.ScreenSpaceDrawQuad.Centre;
public override Quad SelectionQuad => DrawableObject.ScreenSpaceDrawQuad;
public override Vector2 GetInstantDelta(Vector2 screenSpacePosition) => DrawableObject.Parent.ToLocalSpace(screenSpacePosition) - DrawableObject.Position;
}
}

View File

@ -8,35 +8,33 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osuTK;
namespace osu.Game.Rulesets.Edit
{
/// <summary>
/// A blueprint placed above a <see cref="DrawableHitObject"/> adding editing functionality.
/// A blueprint placed above a displaying item adding editing functionality.
/// </summary>
public abstract class SelectionBlueprint : CompositeDrawable, IStateful<SelectionState>
public abstract class SelectionBlueprint<T> : CompositeDrawable, IStateful<SelectionState>
{
public readonly HitObject HitObject;
public readonly T Item;
/// <summary>
/// Invoked when this <see cref="SelectionBlueprint"/> has been selected.
/// Invoked when this <see cref="SelectionBlueprint{T}"/> has been selected.
/// </summary>
public event Action<SelectionBlueprint> Selected;
public event Action<SelectionBlueprint<T>> Selected;
/// <summary>
/// Invoked when this <see cref="SelectionBlueprint"/> has been deselected.
/// Invoked when this <see cref="SelectionBlueprint{T}"/> has been deselected.
/// </summary>
public event Action<SelectionBlueprint> Deselected;
public event Action<SelectionBlueprint<T>> Deselected;
public override bool HandlePositionalInput => ShouldBeAlive;
public override bool RemoveWhenNotAlive => false;
protected SelectionBlueprint(HitObject hitObject)
protected SelectionBlueprint(T item)
{
HitObject = hitObject;
Item = item;
RelativeSizeAxes = Axes.Both;
AlwaysPresent = true;
@ -87,7 +85,7 @@ namespace osu.Game.Rulesets.Edit
protected virtual void OnDeselected()
{
// selection blueprints are AlwaysPresent while the related DrawableHitObject is visible
// selection blueprints are AlwaysPresent while the related item is visible
// set the body piece's alpha directly to avoid arbitrarily rendering frame buffers etc. of children.
foreach (var d in InternalChildren)
d.Hide();
@ -129,7 +127,7 @@ namespace osu.Game.Rulesets.Edit
public virtual MenuItem[] ContextMenuItems => Array.Empty<MenuItem>();
/// <summary>
/// The screen-space point that causes this <see cref="OverlaySelectionBlueprint"/> to be selected.
/// The screen-space point that causes this <see cref="OverlaySelectionBlueprint"/> to be selected via a drag.
/// </summary>
public virtual Vector2 ScreenSpaceSelectionPoint => ScreenSpaceDrawQuad.Centre;
@ -138,8 +136,6 @@ namespace osu.Game.Rulesets.Edit
/// </summary>
public virtual Quad SelectionQuad => ScreenSpaceDrawQuad;
public virtual Vector2 GetInstantDelta(Vector2 screenSpacePosition) => Parent.ToLocalSpace(screenSpacePosition) - Position;
/// <summary>
/// Handle to perform a partial deletion when the user requests a quick delete (Shift+Right Click).
/// </summary>

View File

@ -0,0 +1,57 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Game.Configuration;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.UI;
using osuTK;
namespace osu.Game.Rulesets.Mods
{
public abstract class ModBarrelRoll<TObject> : Mod, IUpdatableByPlayfield, IApplicableToDrawableRuleset<TObject>
where TObject : HitObject
{
/// <summary>
/// The current angle of rotation being applied by this mod.
/// Generally should be used to apply inverse rotation to elements which should not be rotated.
/// </summary>
protected float CurrentRotation { get; private set; }
[SettingSource("Roll speed", "Rotations per minute")]
public BindableNumber<double> SpinSpeed { get; } = new BindableDouble(0.5)
{
MinValue = 0.02,
MaxValue = 12,
Precision = 0.01,
};
[SettingSource("Direction", "The direction of rotation")]
public Bindable<RotationDirection> Direction { get; } = new Bindable<RotationDirection>(RotationDirection.Clockwise);
public override string Name => "Barrel Roll";
public override string Acronym => "BR";
public override string Description => "The whole playfield is on a wheel!";
public override double ScoreMultiplier => 1;
public override string SettingDescription => $"{SpinSpeed.Value} rpm {Direction.Value.GetDescription().ToLowerInvariant()}";
public void Update(Playfield playfield)
{
playfield.Rotation = CurrentRotation = (Direction.Value == RotationDirection.Counterclockwise ? -1 : 1) * 360 * (float)(playfield.Time.Current / 60000 * SpinSpeed.Value);
}
public void ApplyToDrawableRuleset(DrawableRuleset<TObject> drawableRuleset)
{
// scale the playfield to allow all hitobjects to stay within the visible region.
var playfieldSize = drawableRuleset.Playfield.DrawSize;
var minSide = MathF.Min(playfieldSize.X, playfieldSize.Y);
var maxSide = MathF.Max(playfieldSize.X, playfieldSize.Y);
drawableRuleset.Playfield.Scale = new Vector2(minSide / maxSide);
}
}
}

View File

@ -1,7 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
namespace osu.Game.Rulesets.Mania.Objects.Types
namespace osu.Game.Rulesets.Objects.Types
{
/// <summary>
/// A type of hit object which lies in one of a number of predetermined columns.

View File

@ -85,6 +85,7 @@ namespace osu.Game.Rulesets.UI
/// <summary>
/// The beatmap.
/// </summary>
[Cached(typeof(IBeatmap))]
public readonly Beatmap<TObject> Beatmap;
public override IEnumerable<HitObject> Objects => Beatmap.HitObjects;

View File

@ -8,7 +8,6 @@ using System.Text;
using osu.Framework.Extensions;
using osu.Game.Beatmaps;
using osu.Game.IO.Legacy;
using osu.Game.Replays.Legacy;
using osu.Game.Rulesets.Replays.Types;
using SharpCompress.Compressors.LZMA;
@ -91,12 +90,14 @@ namespace osu.Game.Scoring.Legacy
if (score.Replay != null)
{
LegacyReplayFrame lastF = new LegacyReplayFrame(0, 0, 0, ReplayButtonState.None);
int lastTime = 0;
foreach (var f in score.Replay.Frames.OfType<IConvertibleReplayFrame>().Select(f => f.ToLegacy(beatmap)))
{
replayData.Append(FormattableString.Invariant($"{(int)Math.Round(f.Time - lastF.Time)}|{f.MouseX ?? 0}|{f.MouseY ?? 0}|{(int)f.ButtonState},"));
lastF = f;
// Rounding because stable could only parse integral values
int time = (int)Math.Round(f.Time);
replayData.Append(FormattableString.Invariant($"{time - lastTime}|{f.MouseX ?? 0}|{f.MouseY ?? 0}|{(int)f.ButtonState},"));
lastTime = time;
}
}

View File

@ -16,7 +16,7 @@ using osuTK.Graphics;
namespace osu.Game.Screens.Edit.Components.RadioButtons
{
public class DrawableRadioButton : TriangleButton
public class DrawableRadioButton : OsuButton
{
/// <summary>
/// Invoked when this <see cref="DrawableRadioButton"/> has been selected.
@ -49,8 +49,6 @@ namespace osu.Game.Screens.Edit.Components.RadioButtons
selectedBackgroundColour = colours.BlueDark;
selectedBubbleColour = selectedBackgroundColour.Lighten(0.5f);
Triangles.Alpha = 0;
Content.EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Shadow,

View File

@ -15,7 +15,7 @@ using osuTK.Graphics;
namespace osu.Game.Screens.Edit.Components.TernaryButtons
{
internal class DrawableTernaryButton : TriangleButton
internal class DrawableTernaryButton : OsuButton
{
private Color4 defaultBackgroundColour;
private Color4 defaultBubbleColour;
@ -43,8 +43,6 @@ namespace osu.Game.Screens.Edit.Components.TernaryButtons
selectedBackgroundColour = colours.BlueDark;
selectedBubbleColour = selectedBackgroundColour.Lighten(0.5f);
Triangles.Alpha = 0;
Content.EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Shadow,

View File

@ -3,11 +3,9 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
@ -16,46 +14,33 @@ using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osuTK;
using osuTK.Input;
namespace osu.Game.Screens.Edit.Compose.Components
{
/// <summary>
/// A container which provides a "blueprint" display of hitobjects.
/// Includes selection and manipulation support via a <see cref="Components.SelectionHandler"/>.
/// A container which provides a "blueprint" display of items.
/// Includes selection and manipulation support via a <see cref="Components.SelectionHandler{T}"/>.
/// </summary>
public abstract class BlueprintContainer : CompositeDrawable, IKeyBindingHandler<PlatformAction>
public abstract class BlueprintContainer<T> : CompositeDrawable, IKeyBindingHandler<PlatformAction>
{
protected DragBox DragBox { get; private set; }
public Container<SelectionBlueprint> SelectionBlueprints { get; private set; }
public Container<SelectionBlueprint<T>> SelectionBlueprints { get; private set; }
protected SelectionHandler SelectionHandler { get; private set; }
protected SelectionHandler<T> SelectionHandler { get; private set; }
protected readonly HitObjectComposer Composer;
[Resolved(CanBeNull = true)]
private IEditorChangeHandler changeHandler { get; set; }
[Resolved]
protected EditorClock EditorClock { get; private set; }
[Resolved]
protected EditorBeatmap Beatmap { get; private set; }
private readonly BindableList<HitObject> selectedHitObjects = new BindableList<HitObject>();
private readonly Dictionary<HitObject, SelectionBlueprint> blueprintMap = new Dictionary<HitObject, SelectionBlueprint>();
private readonly Dictionary<T, SelectionBlueprint<T>> blueprintMap = new Dictionary<T, SelectionBlueprint<T>>();
[Resolved(canBeNull: true)]
private IPositionSnapProvider snapProvider { get; set; }
protected BlueprintContainer(HitObjectComposer composer)
{
Composer = composer;
[Resolved(CanBeNull = true)]
private IEditorChangeHandler changeHandler { get; set; }
protected BlueprintContainer()
{
RelativeSizeAxes = Axes.Both;
}
@ -73,66 +58,28 @@ namespace osu.Game.Screens.Edit.Compose.Components
SelectionHandler.CreateProxy(),
DragBox.CreateProxy().With(p => p.Depth = float.MinValue)
});
// For non-pooled rulesets, hitobjects are already present in the playfield which allows the blueprints to be loaded in the async context.
if (Composer != null)
{
foreach (var obj in Composer.HitObjects)
addBlueprintFor(obj.HitObject);
}
selectedHitObjects.BindTo(Beatmap.SelectedHitObjects);
selectedHitObjects.CollectionChanged += (selectedObjects, args) =>
{
switch (args.Action)
{
case NotifyCollectionChangedAction.Add:
foreach (var o in args.NewItems)
SelectionBlueprints.FirstOrDefault(b => b.HitObject == o)?.Select();
break;
case NotifyCollectionChangedAction.Remove:
foreach (var o in args.OldItems)
SelectionBlueprints.FirstOrDefault(b => b.HitObject == o)?.Deselect();
break;
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
Beatmap.HitObjectAdded += addBlueprintFor;
Beatmap.HitObjectRemoved += removeBlueprintFor;
if (Composer != null)
{
// For pooled rulesets, blueprints must be added for hitobjects already "current" as they would've not been "current" during the async load addition process above.
foreach (var obj in Composer.HitObjects)
addBlueprintFor(obj.HitObject);
Composer.Playfield.HitObjectUsageBegan += addBlueprintFor;
Composer.Playfield.HitObjectUsageFinished += removeBlueprintFor;
}
}
protected virtual Container<SelectionBlueprint> CreateSelectionBlueprintContainer() => new HitObjectOrderedSelectionContainer { RelativeSizeAxes = Axes.Both };
protected virtual Container<SelectionBlueprint<T>> CreateSelectionBlueprintContainer() => new Container<SelectionBlueprint<T>> { RelativeSizeAxes = Axes.Both };
/// <summary>
/// Creates a <see cref="Components.SelectionHandler"/> which outlines <see cref="DrawableHitObject"/>s and handles movement of selections.
/// Creates a <see cref="Components.SelectionHandler{T}"/> which outlines items and handles movement of selections.
/// </summary>
protected virtual SelectionHandler CreateSelectionHandler() => new SelectionHandler();
protected abstract SelectionHandler<T> CreateSelectionHandler();
/// <summary>
/// Creates a <see cref="SelectionBlueprint"/> for a specific <see cref="DrawableHitObject"/>.
/// Creates a <see cref="SelectionBlueprint{T}"/> for a specific item.
/// </summary>
/// <param name="hitObject">The <see cref="DrawableHitObject"/> to create the overlay for.</param>
protected virtual SelectionBlueprint CreateBlueprintFor(HitObject hitObject) => null;
/// <param name="item">The item to create the overlay for.</param>
protected virtual SelectionBlueprint<T> CreateBlueprintFor(T item) => null;
protected virtual DragBox CreateDragBox(Action<RectangleF> performSelect) => new DragBox(performSelect);
/// <summary>
/// Whether this component is in a state where items outside a drag selection should be deselected. If false, selection will only be added to.
/// </summary>
protected virtual bool AllowDeselectionDuringDrag => true;
protected override bool OnMouseDown(MouseDownEvent e)
{
bool selectionPerformed = performMouseDownActions(e);
@ -143,7 +90,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
return selectionPerformed || e.Button == MouseButton.Left;
}
private SelectionBlueprint clickedBlueprint;
protected SelectionBlueprint<T> ClickedBlueprint { get; private set; }
protected override bool OnClick(ClickEvent e)
{
@ -151,11 +98,11 @@ namespace osu.Game.Screens.Edit.Compose.Components
return false;
// store for double-click handling
clickedBlueprint = SelectionHandler.SelectedBlueprints.FirstOrDefault(b => b.IsHovered);
ClickedBlueprint = SelectionHandler.SelectedBlueprints.FirstOrDefault(b => b.IsHovered);
// Deselection should only occur if no selected blueprints are hovered
// A special case for when a blueprint was selected via this click is added since OnClick() may occur outside the hitobject and should not trigger deselection
if (endClickSelection(e) || clickedBlueprint != null)
// A special case for when a blueprint was selected via this click is added since OnClick() may occur outside the item and should not trigger deselection
if (endClickSelection(e) || ClickedBlueprint != null)
return true;
deselectAll();
@ -168,10 +115,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
return false;
// ensure the blueprint which was hovered for the first click is still the hovered blueprint.
if (clickedBlueprint == null || SelectionHandler.SelectedBlueprints.FirstOrDefault(b => b.IsHovered) != clickedBlueprint)
if (ClickedBlueprint == null || SelectionHandler.SelectedBlueprints.FirstOrDefault(b => b.IsHovered) != ClickedBlueprint)
return false;
EditorClock?.SeekSmoothlyTo(clickedBlueprint.HitObject.StartTime);
return true;
}
@ -227,10 +173,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
if (isDraggingBlueprint)
{
// handle positional change etc.
foreach (var obj in selectedHitObjects)
Beatmap.Update(obj);
DragOperationCompleted();
changeHandler?.EndChange();
}
@ -238,6 +181,13 @@ namespace osu.Game.Screens.Edit.Compose.Components
DragBox.Hide();
}
/// <summary>
/// Called whenever a drag operation completes, before any change transaction is committed.
/// </summary>
protected virtual void DragOperationCompleted()
{
}
protected override bool OnKeyDown(KeyDownEvent e)
{
switch (e.Key)
@ -258,7 +208,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
switch (action.ActionType)
{
case PlatformActionType.SelectAll:
selectAll();
SelectAll();
return true;
}
@ -271,61 +221,58 @@ namespace osu.Game.Screens.Edit.Compose.Components
#region Blueprint Addition/Removal
private void addBlueprintFor(HitObject hitObject)
protected virtual void AddBlueprintFor(T item)
{
if (hitObject is IBarLine)
if (blueprintMap.ContainsKey(item))
return;
if (blueprintMap.ContainsKey(hitObject))
return;
var blueprint = CreateBlueprintFor(hitObject);
var blueprint = CreateBlueprintFor(item);
if (blueprint == null)
return;
blueprintMap[hitObject] = blueprint;
blueprintMap[item] = blueprint;
blueprint.Selected += onBlueprintSelected;
blueprint.Deselected += onBlueprintDeselected;
if (Beatmap.SelectedHitObjects.Contains(hitObject))
blueprint.Select();
blueprint.Selected += OnBlueprintSelected;
blueprint.Deselected += OnBlueprintDeselected;
SelectionBlueprints.Add(blueprint);
OnBlueprintAdded(hitObject);
if (SelectionHandler.SelectedItems.Contains(item))
blueprint.Select();
OnBlueprintAdded(blueprint.Item);
}
private void removeBlueprintFor(HitObject hitObject)
protected void RemoveBlueprintFor(T item)
{
if (!blueprintMap.Remove(hitObject, out var blueprint))
if (!blueprintMap.Remove(item, out var blueprint))
return;
blueprint.Deselect();
blueprint.Selected -= onBlueprintSelected;
blueprint.Deselected -= onBlueprintDeselected;
blueprint.Selected -= OnBlueprintSelected;
blueprint.Deselected -= OnBlueprintDeselected;
SelectionBlueprints.Remove(blueprint);
if (movementBlueprints?.Contains(blueprint) == true)
finishSelectionMovement();
OnBlueprintRemoved(hitObject);
OnBlueprintRemoved(blueprint.Item);
}
/// <summary>
/// Called after a <see cref="HitObject"/> blueprint has been added.
/// Called after an item's blueprint has been added.
/// </summary>
/// <param name="hitObject">The <see cref="HitObject"/> for which the blueprint has been added.</param>
protected virtual void OnBlueprintAdded(HitObject hitObject)
/// <param name="item">The item for which the blueprint has been added.</param>
protected virtual void OnBlueprintAdded(T item)
{
}
/// <summary>
/// Called after a <see cref="HitObject"/> blueprint has been removed.
/// Called after an item's blueprint has been removed.
/// </summary>
/// <param name="hitObject">The <see cref="HitObject"/> for which the blueprint has been removed.</param>
protected virtual void OnBlueprintRemoved(HitObject hitObject)
/// <param name="item">The item for which the blueprint has been removed.</param>
protected virtual void OnBlueprintRemoved(T item)
{
}
@ -347,7 +294,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
{
// Iterate from the top of the input stack (blueprints closest to the front of the screen first).
// Priority is given to already-selected blueprints.
foreach (SelectionBlueprint blueprint in SelectionBlueprints.AliveChildren.Reverse().OrderByDescending(b => b.IsSelected))
foreach (SelectionBlueprint<T> blueprint in SelectionBlueprints.AliveChildren.Reverse().OrderByDescending(b => b.IsSelected))
{
if (!blueprint.IsHovered) continue;
@ -371,7 +318,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
{
// Iterate from the top of the input stack (blueprints closest to the front of the screen first).
// Priority is given to already-selected blueprints.
foreach (SelectionBlueprint blueprint in SelectionBlueprints.AliveChildren.Reverse().OrderByDescending(b => b.IsSelected))
foreach (SelectionBlueprint<T> blueprint in SelectionBlueprints.AliveChildren.Reverse().OrderByDescending(b => b.IsSelected))
{
if (!blueprint.IsHovered) continue;
@ -404,8 +351,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
break;
case SelectionState.Selected:
// if the editor is playing, we generally don't want to deselect objects even if outside the selection area.
if (!EditorClock.IsRunning && !isValidForSelection())
if (AllowDeselectionDuringDrag && !isValidForSelection())
blueprint.Deselect();
break;
}
@ -413,35 +359,29 @@ namespace osu.Game.Screens.Edit.Compose.Components
}
/// <summary>
/// Selects all <see cref="SelectionBlueprint"/>s.
/// Selects all <see cref="SelectionBlueprint{T}"/>s.
/// </summary>
private void selectAll()
protected virtual void SelectAll()
{
Composer.Playfield.KeepAllAlive();
// Scheduled to allow the change in lifetime to take place.
Schedule(() => SelectionBlueprints.ToList().ForEach(m => m.Select()));
}
/// <summary>
/// Deselects all selected <see cref="SelectionBlueprint"/>s.
/// Deselects all selected <see cref="SelectionBlueprint{T}"/>s.
/// </summary>
private void deselectAll() => SelectionHandler.SelectedBlueprints.ToList().ForEach(m => m.Deselect());
private void onBlueprintSelected(SelectionBlueprint blueprint)
protected virtual void OnBlueprintSelected(SelectionBlueprint<T> blueprint)
{
SelectionHandler.HandleSelected(blueprint);
SelectionBlueprints.ChangeChildDepth(blueprint, 1);
Composer.Playfield.SetKeepAlive(blueprint.HitObject, true);
}
private void onBlueprintDeselected(SelectionBlueprint blueprint)
protected virtual void OnBlueprintDeselected(SelectionBlueprint<T> blueprint)
{
SelectionBlueprints.ChangeChildDepth(blueprint, 0);
SelectionHandler.HandleDeselected(blueprint);
Composer.Playfield.SetKeepAlive(blueprint.HitObject, false);
}
#endregion
@ -449,7 +389,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
#region Selection Movement
private Vector2[] movementBlueprintOriginalPositions;
private SelectionBlueprint[] movementBlueprints;
private SelectionBlueprint<T>[] movementBlueprints;
private bool isDraggingBlueprint;
/// <summary>
@ -460,16 +400,23 @@ namespace osu.Game.Screens.Edit.Compose.Components
if (!SelectionHandler.SelectedBlueprints.Any())
return;
// Any selected blueprint that is hovered can begin the movement of the group, however only the earliest hitobject is used for movement
// Any selected blueprint that is hovered can begin the movement of the group, however only the first item (according to SortForMovement) is used for movement.
// A special case is added for when a click selection occurred before the drag
if (!clickSelectionBegan && !SelectionHandler.SelectedBlueprints.Any(b => b.IsHovered))
return;
// Movement is tracked from the blueprint of the earliest hitobject, since it only makes sense to distance snap from that hitobject
movementBlueprints = SelectionHandler.SelectedBlueprints.OrderBy(b => b.HitObject.StartTime).ToArray();
// Movement is tracked from the blueprint of the earliest item, since it only makes sense to distance snap from that item
movementBlueprints = SortForMovement(SelectionHandler.SelectedBlueprints).ToArray();
movementBlueprintOriginalPositions = movementBlueprints.Select(m => m.ScreenSpaceSelectionPoint).ToArray();
}
/// <summary>
/// Apply sorting of selected blueprints before performing movement. Generally used to surface the "main" item to the beginning of the collection.
/// </summary>
/// <param name="blueprints">The blueprints to be moved.</param>
/// <returns>Sorted blueprints.</returns>
protected virtual IEnumerable<SelectionBlueprint<T>> SortForMovement(IReadOnlyList<SelectionBlueprint<T>> blueprints) => blueprints;
/// <summary>
/// Moves the current selected blueprints.
/// </summary>
@ -480,52 +427,50 @@ namespace osu.Game.Screens.Edit.Compose.Components
if (movementBlueprints == null)
return false;
if (snapProvider == null)
return true;
Debug.Assert(movementBlueprintOriginalPositions != null);
Vector2 distanceTravelled = e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition;
// check for positional snap for every object in selection (for things like object-object snapping)
for (var i = 0; i < movementBlueprintOriginalPositions.Length; i++)
if (snapProvider != null)
{
var testPosition = movementBlueprintOriginalPositions[i] + distanceTravelled;
// check for positional snap for every object in selection (for things like object-object snapping)
for (var i = 0; i < movementBlueprintOriginalPositions.Length; i++)
{
Vector2 originalPosition = movementBlueprintOriginalPositions[i];
var testPosition = originalPosition + distanceTravelled;
var positionalResult = snapProvider.SnapScreenSpacePositionToValidPosition(testPosition);
var positionalResult = snapProvider.SnapScreenSpacePositionToValidPosition(testPosition);
if (positionalResult.ScreenSpacePosition == testPosition) continue;
if (positionalResult.ScreenSpacePosition == testPosition) continue;
// attempt to move the objects, and abort any time based snapping if we can.
if (SelectionHandler.HandleMovement(new MoveSelectionEvent(movementBlueprints[i], positionalResult.ScreenSpacePosition)))
return true;
var delta = positionalResult.ScreenSpacePosition - movementBlueprints[i].ScreenSpaceSelectionPoint;
// attempt to move the objects, and abort any time based snapping if we can.
if (SelectionHandler.HandleMovement(new MoveSelectionEvent<T>(movementBlueprints[i], delta)))
return true;
}
}
// if no positional snapping could be performed, try unrestricted snapping from the earliest
// hitobject in the selection.
// item in the selection.
// The final movement position, relative to movementBlueprintOriginalPosition.
Vector2 movePosition = movementBlueprintOriginalPositions.First() + distanceTravelled;
// Retrieve a snapped position.
var result = snapProvider.SnapScreenSpacePositionToValidTime(movePosition);
var result = snapProvider?.SnapScreenSpacePositionToValidTime(movePosition);
// Move the hitobjects.
if (!SelectionHandler.HandleMovement(new MoveSelectionEvent(movementBlueprints.First(), result.ScreenSpacePosition)))
return true;
if (result.Time.HasValue)
if (result == null)
{
// Apply the start time at the newly snapped-to position
double offset = result.Time.Value - movementBlueprints.First().HitObject.StartTime;
if (offset != 0)
Beatmap.PerformOnSelection(obj => obj.StartTime += offset);
return SelectionHandler.HandleMovement(new MoveSelectionEvent<T>(movementBlueprints.First(), movePosition - movementBlueprints.First().ScreenSpaceSelectionPoint));
}
return true;
return ApplySnapResult(movementBlueprints, result);
}
protected virtual bool ApplySnapResult(SelectionBlueprint<T>[] blueprints, SnapResult result) =>
SelectionHandler.HandleMovement(new MoveSelectionEvent<T>(blueprints.First(), result.ScreenSpacePosition - blueprints.First().ScreenSpaceSelectionPoint));
/// <summary>
/// Finishes the current movement of selected blueprints.
/// </summary>
@ -542,22 +487,5 @@ namespace osu.Game.Screens.Edit.Compose.Components
}
#endregion
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (Beatmap != null)
{
Beatmap.HitObjectAdded -= addBlueprintFor;
Beatmap.HitObjectRemoved -= removeBlueprintFor;
}
if (Composer != null)
{
Composer.Playfield.HitObjectUsageBegan -= addBlueprintFor;
Composer.Playfield.HitObjectUsageFinished -= removeBlueprintFor;
}
}
}
}

View File

@ -27,12 +27,14 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// <summary>
/// A blueprint container generally displayed as an overlay to a ruleset's playfield.
/// </summary>
public class ComposeBlueprintContainer : BlueprintContainer
public class ComposeBlueprintContainer : EditorBlueprintContainer
{
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
private readonly Container<PlacementBlueprint> placementBlueprintContainer;
protected new EditorSelectionHandler SelectionHandler => (EditorSelectionHandler)base.SelectionHandler;
private PlacementBlueprint currentPlacement;
private InputManager inputManager;
@ -113,7 +115,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
// convert to game space coordinates
delta = firstBlueprint.ToScreenSpace(delta) - firstBlueprint.ToScreenSpace(Vector2.Zero);
SelectionHandler.HandleMovement(new MoveSelectionEvent(firstBlueprint, firstBlueprint.ScreenSpaceSelectionPoint + delta));
SelectionHandler.HandleMovement(new MoveSelectionEvent<HitObject>(firstBlueprint, delta));
}
private void updatePlacementNewCombo()
@ -237,9 +239,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
updatePlacementPosition();
}
protected sealed override SelectionBlueprint CreateBlueprintFor(HitObject hitObject)
protected sealed override SelectionBlueprint<HitObject> CreateBlueprintFor(HitObject item)
{
var drawable = Composer.HitObjects.FirstOrDefault(d => d.HitObject == hitObject);
var drawable = Composer.HitObjects.FirstOrDefault(d => d.HitObject == item);
if (drawable == null)
return null;
@ -249,9 +251,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
public virtual OverlaySelectionBlueprint CreateBlueprintFor(DrawableHitObject hitObject) => null;
protected override void OnBlueprintAdded(HitObject hitObject)
protected override void OnBlueprintAdded(HitObject item)
{
base.OnBlueprintAdded(hitObject);
base.OnBlueprintAdded(item);
refreshTool();

View File

@ -0,0 +1,171 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
namespace osu.Game.Screens.Edit.Compose.Components
{
public class EditorBlueprintContainer : BlueprintContainer<HitObject>
{
[Resolved]
protected EditorClock EditorClock { get; private set; }
[Resolved]
protected EditorBeatmap Beatmap { get; private set; }
protected readonly HitObjectComposer Composer;
private readonly BindableList<HitObject> selectedHitObjects = new BindableList<HitObject>();
protected EditorBlueprintContainer(HitObjectComposer composer)
{
Composer = composer;
}
[BackgroundDependencyLoader]
private void load()
{
// For non-pooled rulesets, hitobjects are already present in the playfield which allows the blueprints to be loaded in the async context.
if (Composer != null)
{
foreach (var obj in Composer.HitObjects)
AddBlueprintFor(obj.HitObject);
}
selectedHitObjects.BindTo(Beatmap.SelectedHitObjects);
selectedHitObjects.CollectionChanged += (selectedObjects, args) =>
{
switch (args.Action)
{
case NotifyCollectionChangedAction.Add:
foreach (var o in args.NewItems)
SelectionBlueprints.FirstOrDefault(b => b.Item == o)?.Select();
break;
case NotifyCollectionChangedAction.Remove:
foreach (var o in args.OldItems)
SelectionBlueprints.FirstOrDefault(b => b.Item == o)?.Deselect();
break;
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
Beatmap.HitObjectAdded += AddBlueprintFor;
Beatmap.HitObjectRemoved += RemoveBlueprintFor;
if (Composer != null)
{
// For pooled rulesets, blueprints must be added for hitobjects already "current" as they would've not been "current" during the async load addition process above.
foreach (var obj in Composer.HitObjects)
AddBlueprintFor(obj.HitObject);
Composer.Playfield.HitObjectUsageBegan += AddBlueprintFor;
Composer.Playfield.HitObjectUsageFinished += RemoveBlueprintFor;
}
}
protected override IEnumerable<SelectionBlueprint<HitObject>> SortForMovement(IReadOnlyList<SelectionBlueprint<HitObject>> blueprints)
=> blueprints.OrderBy(b => b.Item.StartTime);
protected override bool AllowDeselectionDuringDrag => !EditorClock.IsRunning;
protected override bool ApplySnapResult(SelectionBlueprint<HitObject>[] blueprints, SnapResult result)
{
if (!base.ApplySnapResult(blueprints, result))
return false;
if (result.Time.HasValue)
{
// Apply the start time at the newly snapped-to position
double offset = result.Time.Value - blueprints.First().Item.StartTime;
if (offset != 0)
Beatmap.PerformOnSelection(obj => obj.StartTime += offset);
}
return true;
}
protected override void AddBlueprintFor(HitObject item)
{
if (item is IBarLine)
return;
base.AddBlueprintFor(item);
}
protected override void DragOperationCompleted()
{
base.DragOperationCompleted();
// handle positional change etc.
foreach (var blueprint in SelectionBlueprints)
Beatmap.Update(blueprint.Item);
}
protected override bool OnDoubleClick(DoubleClickEvent e)
{
if (!base.OnDoubleClick(e))
return false;
EditorClock?.SeekSmoothlyTo(ClickedBlueprint.Item.StartTime);
return true;
}
protected override Container<SelectionBlueprint<HitObject>> CreateSelectionBlueprintContainer() => new HitObjectOrderedSelectionContainer { RelativeSizeAxes = Axes.Both };
protected override SelectionHandler<HitObject> CreateSelectionHandler() => new EditorSelectionHandler();
protected override void SelectAll()
{
Composer.Playfield.KeepAllAlive();
base.SelectAll();
}
protected override void OnBlueprintSelected(SelectionBlueprint<HitObject> blueprint)
{
base.OnBlueprintSelected(blueprint);
Composer.Playfield.SetKeepAlive(blueprint.Item, true);
}
protected override void OnBlueprintDeselected(SelectionBlueprint<HitObject> blueprint)
{
base.OnBlueprintDeselected(blueprint);
Composer.Playfield.SetKeepAlive(blueprint.Item, false);
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (Beatmap != null)
{
Beatmap.HitObjectAdded -= AddBlueprintFor;
Beatmap.HitObjectRemoved -= RemoveBlueprintFor;
}
if (Composer != null)
{
Composer.Playfield.HitObjectUsageBegan -= AddBlueprintFor;
Composer.Playfield.HitObjectUsageFinished -= RemoveBlueprintFor;
}
}
}
}

View File

@ -0,0 +1,194 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using Humanizer;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Audio;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
namespace osu.Game.Screens.Edit.Compose.Components
{
public class EditorSelectionHandler : SelectionHandler<HitObject>
{
[Resolved]
protected EditorBeatmap EditorBeatmap { get; private set; }
[BackgroundDependencyLoader]
private void load()
{
createStateBindables();
// bring in updates from selection changes
EditorBeatmap.HitObjectUpdated += _ => Scheduler.AddOnce(UpdateTernaryStates);
SelectedItems.BindTo(EditorBeatmap.SelectedHitObjects);
SelectedItems.CollectionChanged += (sender, args) =>
{
Scheduler.AddOnce(UpdateTernaryStates);
};
}
protected override void DeleteItems(IEnumerable<HitObject> items) => EditorBeatmap.RemoveRange(items);
#region Selection State
/// <summary>
/// The state of "new combo" for all selected hitobjects.
/// </summary>
public readonly Bindable<TernaryState> SelectionNewComboState = new Bindable<TernaryState>();
/// <summary>
/// The state of each sample type for all selected hitobjects. Keys match with <see cref="HitSampleInfo"/> constant specifications.
/// </summary>
public readonly Dictionary<string, Bindable<TernaryState>> SelectionSampleStates = new Dictionary<string, Bindable<TernaryState>>();
/// <summary>
/// Set up ternary state bindables and bind them to selection/hitobject changes (in both directions)
/// </summary>
private void createStateBindables()
{
foreach (var sampleName in HitSampleInfo.AllAdditions)
{
var bindable = new Bindable<TernaryState>
{
Description = sampleName.Replace("hit", string.Empty).Titleize()
};
bindable.ValueChanged += state =>
{
switch (state.NewValue)
{
case TernaryState.False:
RemoveHitSample(sampleName);
break;
case TernaryState.True:
AddHitSample(sampleName);
break;
}
};
SelectionSampleStates[sampleName] = bindable;
}
// new combo
SelectionNewComboState.ValueChanged += state =>
{
switch (state.NewValue)
{
case TernaryState.False:
SetNewCombo(false);
break;
case TernaryState.True:
SetNewCombo(true);
break;
}
};
}
/// <summary>
/// Called when context menu ternary states may need to be recalculated (selection changed or hitobject updated).
/// </summary>
protected virtual void UpdateTernaryStates()
{
SelectionNewComboState.Value = GetStateFromSelection(SelectedItems.OfType<IHasComboInformation>(), h => h.NewCombo);
foreach (var (sampleName, bindable) in SelectionSampleStates)
{
bindable.Value = GetStateFromSelection(SelectedItems, h => h.Samples.Any(s => s.Name == sampleName));
}
}
/// <summary>
/// Given a selection target and a function of truth, retrieve the correct ternary state for display.
/// </summary>
protected TernaryState GetStateFromSelection<T>(IEnumerable<T> selection, Func<T, bool> func)
{
if (selection.Any(func))
return selection.All(func) ? TernaryState.True : TernaryState.Indeterminate;
return TernaryState.False;
}
#endregion
#region Ternary state changes
/// <summary>
/// Adds a hit sample to all selected <see cref="HitObject"/>s.
/// </summary>
/// <param name="sampleName">The name of the hit sample.</param>
public void AddHitSample(string sampleName)
{
EditorBeatmap.PerformOnSelection(h =>
{
// Make sure there isn't already an existing sample
if (h.Samples.Any(s => s.Name == sampleName))
return;
h.Samples.Add(new HitSampleInfo(sampleName));
});
}
/// <summary>
/// Removes a hit sample from all selected <see cref="HitObject"/>s.
/// </summary>
/// <param name="sampleName">The name of the hit sample.</param>
public void RemoveHitSample(string sampleName)
{
EditorBeatmap.PerformOnSelection(h => h.SamplesBindable.RemoveAll(s => s.Name == sampleName));
}
/// <summary>
/// Set the new combo state of all selected <see cref="HitObject"/>s.
/// </summary>
/// <param name="state">Whether to set or unset.</param>
/// <exception cref="InvalidOperationException">Throws if any selected object doesn't implement <see cref="IHasComboInformation"/></exception>
public void SetNewCombo(bool state)
{
EditorBeatmap.PerformOnSelection(h =>
{
var comboInfo = h as IHasComboInformation;
if (comboInfo == null || comboInfo.NewCombo == state) return;
comboInfo.NewCombo = state;
EditorBeatmap.Update(h);
});
}
#endregion
#region Context Menu
/// <summary>
/// Provide context menu items relevant to current selection. Calling base is not required.
/// </summary>
/// <param name="selection">The current selection.</param>
/// <returns>The relevant menu items.</returns>
protected override IEnumerable<MenuItem> GetContextMenuItemsForSelection(IEnumerable<SelectionBlueprint<HitObject>> selection)
{
if (SelectedBlueprints.All(b => b.Item is IHasComboInformation))
{
yield return new TernaryStateMenuItem("New combo") { State = { BindTarget = SelectionNewComboState } };
}
yield return new OsuMenuItem("Sound")
{
Items = SelectionSampleStates.Select(kvp =>
new TernaryStateMenuItem(kvp.Value.Description) { State = { BindTarget = kvp.Value } }).ToArray()
};
}
#endregion
}
}

View File

@ -11,17 +11,17 @@ using osu.Game.Rulesets.Objects;
namespace osu.Game.Screens.Edit.Compose.Components
{
/// <summary>
/// A container for <see cref="SelectionBlueprint"/> ordered by their <see cref="HitObject"/> start times.
/// A container for <see cref="SelectionBlueprint{HitObject}"/> ordered by their <see cref="HitObject"/> start times.
/// </summary>
public sealed class HitObjectOrderedSelectionContainer : Container<SelectionBlueprint>
public sealed class HitObjectOrderedSelectionContainer : Container<SelectionBlueprint<HitObject>>
{
public override void Add(SelectionBlueprint drawable)
public override void Add(SelectionBlueprint<HitObject> drawable)
{
base.Add(drawable);
bindStartTime(drawable);
}
public override bool Remove(SelectionBlueprint drawable)
public override bool Remove(SelectionBlueprint<HitObject> drawable)
{
if (!base.Remove(drawable))
return false;
@ -36,11 +36,11 @@ namespace osu.Game.Screens.Edit.Compose.Components
unbindAllStartTimes();
}
private readonly Dictionary<SelectionBlueprint, IBindable> startTimeMap = new Dictionary<SelectionBlueprint, IBindable>();
private readonly Dictionary<SelectionBlueprint<HitObject>, IBindable> startTimeMap = new Dictionary<SelectionBlueprint<HitObject>, IBindable>();
private void bindStartTime(SelectionBlueprint blueprint)
private void bindStartTime(SelectionBlueprint<HitObject> blueprint)
{
var bindable = blueprint.HitObject.StartTimeBindable.GetBoundCopy();
var bindable = blueprint.Item.StartTimeBindable.GetBoundCopy();
bindable.BindValueChanged(_ =>
{
@ -51,7 +51,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
startTimeMap[blueprint] = bindable;
}
private void unbindStartTime(SelectionBlueprint blueprint)
private void unbindStartTime(SelectionBlueprint<HitObject> blueprint)
{
startTimeMap[blueprint].UnbindAll();
startTimeMap.Remove(blueprint);
@ -66,16 +66,16 @@ namespace osu.Game.Screens.Edit.Compose.Components
protected override int Compare(Drawable x, Drawable y)
{
var xObj = (SelectionBlueprint)x;
var yObj = (SelectionBlueprint)y;
var xObj = (SelectionBlueprint<HitObject>)x;
var yObj = (SelectionBlueprint<HitObject>)y;
// Put earlier blueprints towards the end of the list, so they handle input first
int i = yObj.HitObject.StartTime.CompareTo(xObj.HitObject.StartTime);
int i = yObj.Item.StartTime.CompareTo(xObj.Item.StartTime);
if (i != 0) return i;
// Fall back to end time if the start time is equal.
i = yObj.HitObject.GetEndTime().CompareTo(xObj.HitObject.GetEndTime());
i = yObj.Item.GetEndTime().CompareTo(xObj.Item.GetEndTime());
return i == 0 ? CompareReverseChildID(y, x) : i;
}

View File

@ -9,29 +9,22 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// <summary>
/// An event which occurs when a <see cref="OverlaySelectionBlueprint"/> is moved.
/// </summary>
public class MoveSelectionEvent
public class MoveSelectionEvent<T>
{
/// <summary>
/// The <see cref="SelectionBlueprint"/> that triggered this <see cref="MoveSelectionEvent"/>.
/// The <see cref="SelectionBlueprint{T}"/> that triggered this <see cref="MoveSelectionEvent{T}"/>.
/// </summary>
public readonly SelectionBlueprint Blueprint;
public readonly SelectionBlueprint<T> Blueprint;
/// <summary>
/// The expected screen-space position of the hitobject at the current cursor position.
/// The screen-space delta of this move event.
/// </summary>
public readonly Vector2 ScreenSpacePosition;
public readonly Vector2 ScreenSpaceDelta;
/// <summary>
/// The distance between <see cref="ScreenSpacePosition"/> and the hitobject's current position, in the coordinate-space of the hitobject's parent.
/// </summary>
public readonly Vector2 InstantDelta;
public MoveSelectionEvent(SelectionBlueprint blueprint, Vector2 screenSpacePosition)
public MoveSelectionEvent(SelectionBlueprint<T> blueprint, Vector2 screenSpaceDelta)
{
Blueprint = blueprint;
ScreenSpacePosition = screenSpacePosition;
InstantDelta = Blueprint.GetInstantDelta(ScreenSpacePosition);
ScreenSpaceDelta = screenSpaceDelta;
}
}
}

View File

@ -4,43 +4,44 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Humanizer;
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.Graphics.Primitives;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Audio;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types;
using osuTK;
using osuTK.Input;
namespace osu.Game.Screens.Edit.Compose.Components
{
/// <summary>
/// A component which outlines <see cref="DrawableHitObject"/>s and handles movement of selections.
/// A component which outlines items and handles movement of selections.
/// </summary>
public class SelectionHandler : CompositeDrawable, IKeyBindingHandler<PlatformAction>, IHasContextMenu
public abstract class SelectionHandler<T> : CompositeDrawable, IKeyBindingHandler<PlatformAction>, IHasContextMenu
{
/// <summary>
/// The currently selected blueprints.
/// Should be used when operations are dealing directly with the visible blueprints.
/// For more general selection operations, use <see cref="osu.Game.Screens.Edit.EditorBeatmap.SelectedHitObjects"/> instead.
/// For more general selection operations, use <see cref="SelectedItems"/> instead.
/// </summary>
public IEnumerable<SelectionBlueprint> SelectedBlueprints => selectedBlueprints;
public IReadOnlyList<SelectionBlueprint<T>> SelectedBlueprints => selectedBlueprints;
private readonly List<SelectionBlueprint> selectedBlueprints;
/// <summary>
/// The currently selected items.
/// </summary>
public readonly BindableList<T> SelectedItems = new BindableList<T>();
private readonly List<SelectionBlueprint<T>> selectedBlueprints;
private Drawable content;
@ -48,15 +49,12 @@ namespace osu.Game.Screens.Edit.Compose.Components
protected SelectionBox SelectionBox { get; private set; }
[Resolved]
protected EditorBeatmap EditorBeatmap { get; private set; }
[Resolved(CanBeNull = true)]
protected IEditorChangeHandler ChangeHandler { get; private set; }
public SelectionHandler()
protected SelectionHandler()
{
selectedBlueprints = new List<SelectionBlueprint>();
selectedBlueprints = new List<SelectionBlueprint<T>>();
RelativeSizeAxes = Axes.Both;
AlwaysPresent = true;
@ -66,8 +64,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
createStateBindables();
InternalChild = content = new Container
{
Children = new Drawable[]
@ -95,6 +91,11 @@ namespace osu.Game.Screens.Edit.Compose.Components
SelectionBox = CreateSelectionBox(),
}
};
SelectedItems.CollectionChanged += (sender, args) =>
{
Scheduler.AddOnce(updateVisibility);
};
}
public SelectionBox CreateSelectionBox()
@ -128,45 +129,44 @@ namespace osu.Game.Screens.Edit.Compose.Components
#region User Input Handling
/// <summary>
/// Handles the selected <see cref="DrawableHitObject"/>s being moved.
/// Handles the selected items being moved.
/// </summary>
/// <remarks>
/// Just returning true is enough to allow <see cref="HitObject.StartTime"/> updates to take place.
/// Just returning true is enough to allow default movement to take place.
/// Custom implementation is only required if other attributes are to be considered, like changing columns.
/// </remarks>
/// <param name="moveEvent">The move event.</param>
/// <returns>
/// Whether any <see cref="DrawableHitObject"/>s could be moved.
/// Returning true will also propagate StartTime changes provided by the closest <see cref="IPositionSnapProvider.SnapScreenSpacePositionToValidTime"/>.
/// Whether any items could be moved.
/// </returns>
public virtual bool HandleMovement(MoveSelectionEvent moveEvent) => false;
public virtual bool HandleMovement(MoveSelectionEvent<T> moveEvent) => false;
/// <summary>
/// Handles the selected <see cref="DrawableHitObject"/>s being rotated.
/// Handles the selected items being rotated.
/// </summary>
/// <param name="angle">The delta angle to apply to the selection.</param>
/// <returns>Whether any <see cref="DrawableHitObject"/>s could be rotated.</returns>
/// <returns>Whether any items could be rotated.</returns>
public virtual bool HandleRotation(float angle) => false;
/// <summary>
/// Handles the selected <see cref="DrawableHitObject"/>s being scaled.
/// Handles the selected items being scaled.
/// </summary>
/// <param name="scale">The delta scale to apply, in playfield local coordinates.</param>
/// <param name="scale">The delta scale to apply, in local coordinates.</param>
/// <param name="anchor">The point of reference where the scale is originating from.</param>
/// <returns>Whether any <see cref="DrawableHitObject"/>s could be scaled.</returns>
/// <returns>Whether any items could be scaled.</returns>
public virtual bool HandleScale(Vector2 scale, Anchor anchor) => false;
/// <summary>
/// Handles the selected <see cref="DrawableHitObject"/>s being flipped.
/// Handles the selected items being flipped.
/// </summary>
/// <param name="direction">The direction to flip</param>
/// <returns>Whether any <see cref="DrawableHitObject"/>s could be flipped.</returns>
/// <returns>Whether any items could be flipped.</returns>
public virtual bool HandleFlip(Direction direction) => false;
/// <summary>
/// Handles the selected <see cref="DrawableHitObject"/>s being reversed pattern-wise.
/// Handles the selected items being reversed pattern-wise.
/// </summary>
/// <returns>Whether any <see cref="DrawableHitObject"/>s could be reversed.</returns>
/// <returns>Whether any items could be reversed.</returns>
public virtual bool HandleReverse() => false;
public bool OnPressed(PlatformAction action)
@ -174,7 +174,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
switch (action.ActionMethod)
{
case PlatformActionMethod.Delete:
deleteSelected();
DeleteSelected();
return true;
}
@ -198,24 +198,23 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// Handle a blueprint becoming selected.
/// </summary>
/// <param name="blueprint">The blueprint.</param>
internal void HandleSelected(SelectionBlueprint blueprint)
internal virtual void HandleSelected(SelectionBlueprint<T> blueprint)
{
selectedBlueprints.Add(blueprint);
// there are potentially multiple SelectionHandlers active, but we only want to add items to the selected list once.
if (!SelectedItems.Contains(blueprint.Item))
SelectedItems.Add(blueprint.Item);
// there are potentially multiple SelectionHandlers active, but we only want to add hitobjects to the selected list once.
if (!EditorBeatmap.SelectedHitObjects.Contains(blueprint.HitObject))
EditorBeatmap.SelectedHitObjects.Add(blueprint.HitObject);
selectedBlueprints.Add(blueprint);
}
/// <summary>
/// Handle a blueprint becoming deselected.
/// </summary>
/// <param name="blueprint">The blueprint.</param>
internal void HandleDeselected(SelectionBlueprint blueprint)
internal virtual void HandleDeselected(SelectionBlueprint<T> blueprint)
{
SelectedItems.Remove(blueprint.Item);
selectedBlueprints.Remove(blueprint);
EditorBeatmap.SelectedHitObjects.Remove(blueprint.HitObject);
}
/// <summary>
@ -224,7 +223,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// <param name="blueprint">The blueprint.</param>
/// <param name="e">The mouse event responsible for selection.</param>
/// <returns>Whether a selection was performed.</returns>
internal bool MouseDownSelectionRequested(SelectionBlueprint blueprint, MouseButtonEvent e)
internal bool MouseDownSelectionRequested(SelectionBlueprint<T> blueprint, MouseButtonEvent e)
{
if (e.ShiftPressed && e.Button == MouseButton.Right)
{
@ -248,7 +247,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// <param name="blueprint">The blueprint.</param>
/// <param name="e">The mouse event responsible for deselection.</param>
/// <returns>Whether a deselection was performed.</returns>
internal bool MouseUpSelectionRequested(SelectionBlueprint blueprint, MouseButtonEvent e)
internal bool MouseUpSelectionRequested(SelectionBlueprint<T> blueprint, MouseButtonEvent e)
{
if (blueprint.IsSelected)
{
@ -259,23 +258,29 @@ namespace osu.Game.Screens.Edit.Compose.Components
return false;
}
private void handleQuickDeletion(SelectionBlueprint blueprint)
private void handleQuickDeletion(SelectionBlueprint<T> blueprint)
{
if (blueprint.HandleQuickDeletion())
return;
if (!blueprint.IsSelected)
EditorBeatmap.Remove(blueprint.HitObject);
DeleteItems(new[] { blueprint.Item });
else
deleteSelected();
DeleteSelected();
}
/// <summary>
/// Called whenever the deletion of items has been requested.
/// </summary>
/// <param name="items">The items to be deleted.</param>
protected abstract void DeleteItems(IEnumerable<T> items);
/// <summary>
/// Ensure the blueprint is in a selected state.
/// </summary>
/// <param name="blueprint">The blueprint to select.</param>
/// <returns>Whether selection state was changed.</returns>
private bool ensureSelected(SelectionBlueprint blueprint)
private bool ensureSelected(SelectionBlueprint<T> blueprint)
{
if (blueprint.IsSelected)
return false;
@ -285,9 +290,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
return true;
}
private void deleteSelected()
protected void DeleteSelected()
{
EditorBeatmap.RemoveRange(selectedBlueprints.Select(b => b.HitObject));
DeleteItems(selectedBlueprints.Select(b => b.Item));
}
#endregion
@ -295,11 +300,11 @@ namespace osu.Game.Screens.Edit.Compose.Components
#region Outline Display
/// <summary>
/// Updates whether this <see cref="SelectionHandler"/> is visible.
/// Updates whether this <see cref="SelectionHandler{T}"/> is visible.
/// </summary>
private void updateVisibility()
{
int count = EditorBeatmap.SelectedHitObjects.Count;
int count = SelectedItems.Count;
selectionDetailsText.Text = count > 0 ? count.ToString() : string.Empty;
@ -308,7 +313,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
}
/// <summary>
/// Triggered whenever the set of selected objects changes.
/// Triggered whenever the set of selected items changes.
/// Should update the selection box's state to match supported operations.
/// </summary>
protected virtual void OnSelectionChanged()
@ -322,159 +327,16 @@ namespace osu.Game.Screens.Edit.Compose.Components
if (selectedBlueprints.Count == 0)
return;
// Move the rectangle to cover the hitobjects
var topLeft = new Vector2(float.MaxValue, float.MaxValue);
var bottomRight = new Vector2(float.MinValue, float.MinValue);
// Move the rectangle to cover the items
RectangleF selectionRect = ToLocalSpace(selectedBlueprints[0].SelectionQuad).AABBFloat;
foreach (var blueprint in selectedBlueprints)
{
topLeft = Vector2.ComponentMin(topLeft, ToLocalSpace(blueprint.SelectionQuad.TopLeft));
bottomRight = Vector2.ComponentMax(bottomRight, ToLocalSpace(blueprint.SelectionQuad.BottomRight));
}
for (int i = 1; i < selectedBlueprints.Count; i++)
selectionRect = RectangleF.Union(selectionRect, ToLocalSpace(selectedBlueprints[i].SelectionQuad).AABBFloat);
topLeft -= new Vector2(5);
bottomRight += new Vector2(5);
selectionRect = selectionRect.Inflate(5f);
content.Size = bottomRight - topLeft;
content.Position = topLeft;
}
#endregion
#region Sample Changes
/// <summary>
/// Adds a hit sample to all selected <see cref="HitObject"/>s.
/// </summary>
/// <param name="sampleName">The name of the hit sample.</param>
public void AddHitSample(string sampleName)
{
EditorBeatmap.PerformOnSelection(h =>
{
// Make sure there isn't already an existing sample
if (h.Samples.Any(s => s.Name == sampleName))
return;
h.Samples.Add(new HitSampleInfo(sampleName));
});
}
/// <summary>
/// Set the new combo state of all selected <see cref="HitObject"/>s.
/// </summary>
/// <param name="state">Whether to set or unset.</param>
/// <exception cref="InvalidOperationException">Throws if any selected object doesn't implement <see cref="IHasComboInformation"/></exception>
public void SetNewCombo(bool state)
{
EditorBeatmap.PerformOnSelection(h =>
{
var comboInfo = h as IHasComboInformation;
if (comboInfo == null || comboInfo.NewCombo == state) return;
comboInfo.NewCombo = state;
EditorBeatmap.Update(h);
});
}
/// <summary>
/// Removes a hit sample from all selected <see cref="HitObject"/>s.
/// </summary>
/// <param name="sampleName">The name of the hit sample.</param>
public void RemoveHitSample(string sampleName)
{
EditorBeatmap.PerformOnSelection(h => h.SamplesBindable.RemoveAll(s => s.Name == sampleName));
}
#endregion
#region Selection State
/// <summary>
/// The state of "new combo" for all selected hitobjects.
/// </summary>
public readonly Bindable<TernaryState> SelectionNewComboState = new Bindable<TernaryState>();
/// <summary>
/// The state of each sample type for all selected hitobjects. Keys match with <see cref="HitSampleInfo"/> constant specifications.
/// </summary>
public readonly Dictionary<string, Bindable<TernaryState>> SelectionSampleStates = new Dictionary<string, Bindable<TernaryState>>();
/// <summary>
/// Set up ternary state bindables and bind them to selection/hitobject changes (in both directions)
/// </summary>
private void createStateBindables()
{
foreach (var sampleName in HitSampleInfo.AllAdditions)
{
var bindable = new Bindable<TernaryState>
{
Description = sampleName.Replace("hit", string.Empty).Titleize()
};
bindable.ValueChanged += state =>
{
switch (state.NewValue)
{
case TernaryState.False:
RemoveHitSample(sampleName);
break;
case TernaryState.True:
AddHitSample(sampleName);
break;
}
};
SelectionSampleStates[sampleName] = bindable;
}
// new combo
SelectionNewComboState.ValueChanged += state =>
{
switch (state.NewValue)
{
case TernaryState.False:
SetNewCombo(false);
break;
case TernaryState.True:
SetNewCombo(true);
break;
}
};
// bring in updates from selection changes
EditorBeatmap.HitObjectUpdated += _ => Scheduler.AddOnce(UpdateTernaryStates);
EditorBeatmap.SelectedHitObjects.CollectionChanged += (sender, args) =>
{
Scheduler.AddOnce(updateVisibility);
Scheduler.AddOnce(UpdateTernaryStates);
};
}
/// <summary>
/// Called when context menu ternary states may need to be recalculated (selection changed or hitobject updated).
/// </summary>
protected virtual void UpdateTernaryStates()
{
SelectionNewComboState.Value = GetStateFromSelection(EditorBeatmap.SelectedHitObjects.OfType<IHasComboInformation>(), h => h.NewCombo);
foreach (var (sampleName, bindable) in SelectionSampleStates)
{
bindable.Value = GetStateFromSelection(EditorBeatmap.SelectedHitObjects, h => h.Samples.Any(s => s.Name == sampleName));
}
}
/// <summary>
/// Given a selection target and a function of truth, retrieve the correct ternary state for display.
/// </summary>
protected TernaryState GetStateFromSelection<T>(IEnumerable<T> selection, Func<T, bool> func)
{
if (selection.Any(func))
return selection.All(func) ? TernaryState.True : TernaryState.Indeterminate;
return TernaryState.False;
content.Position = selectionRect.Location;
content.Size = selectionRect.Size;
}
#endregion
@ -485,30 +347,17 @@ namespace osu.Game.Screens.Edit.Compose.Components
{
get
{
if (!selectedBlueprints.Any(b => b.IsHovered))
if (!SelectedBlueprints.Any(b => b.IsHovered))
return Array.Empty<MenuItem>();
var items = new List<MenuItem>();
items.AddRange(GetContextMenuItemsForSelection(selectedBlueprints));
items.AddRange(GetContextMenuItemsForSelection(SelectedBlueprints));
if (selectedBlueprints.All(b => b.HitObject is IHasComboInformation))
{
items.Add(new TernaryStateMenuItem("New combo") { State = { BindTarget = SelectionNewComboState } });
}
if (SelectedBlueprints.Count == 1)
items.AddRange(SelectedBlueprints[0].ContextMenuItems);
if (selectedBlueprints.Count == 1)
items.AddRange(selectedBlueprints[0].ContextMenuItems);
items.AddRange(new[]
{
new OsuMenuItem("Sound")
{
Items = SelectionSampleStates.Select(kvp =>
new TernaryStateMenuItem(kvp.Value.Description) { State = { BindTarget = kvp.Value } }).ToArray()
},
new OsuMenuItem("Delete", MenuItemType.Destructive, deleteSelected),
});
items.Add(new OsuMenuItem("Delete", MenuItemType.Destructive, DeleteSelected));
return items.ToArray();
}
@ -519,7 +368,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// </summary>
/// <param name="selection">The current selection.</param>
/// <returns>The relevant menu items.</returns>
protected virtual IEnumerable<MenuItem> GetContextMenuItemsForSelection(IEnumerable<SelectionBlueprint> selection)
protected virtual IEnumerable<MenuItem> GetContextMenuItemsForSelection(IEnumerable<SelectionBlueprint<T>> selection)
=> Enumerable.Empty<MenuItem>();
#endregion

View File

@ -25,20 +25,17 @@ using osuTK.Graphics;
namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
internal class TimelineBlueprintContainer : BlueprintContainer
internal class TimelineBlueprintContainer : EditorBlueprintContainer
{
[Resolved(CanBeNull = true)]
private Timeline timeline { get; set; }
[Resolved]
private EditorBeatmap beatmap { get; set; }
[Resolved]
private OsuColour colours { get; set; }
private DragEvent lastDragEvent;
private Bindable<HitObject> placement;
private SelectionBlueprint placementBlueprint;
private SelectionBlueprint<HitObject> placementBlueprint;
private SelectableAreaBackground backgroundBox;
@ -76,7 +73,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
base.LoadComplete();
DragBox.Alpha = 0;
placement = beatmap.PlacementObject.GetBoundCopy();
placement = Beatmap.PlacementObject.GetBoundCopy();
placement.ValueChanged += placementChanged;
}
@ -100,7 +97,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
}
}
protected override Container<SelectionBlueprint> CreateSelectionBlueprintContainer() => new TimelineSelectionBlueprintContainer { RelativeSizeAxes = Axes.Both };
protected override Container<SelectionBlueprint<HitObject>> CreateSelectionBlueprintContainer() => new TimelineSelectionBlueprintContainer { RelativeSizeAxes = Axes.Both };
protected override bool OnHover(HoverEvent e)
{
@ -160,7 +157,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
// remove objects from the stack as long as their end time is in the past.
while (currentConcurrentObjects.TryPeek(out HitObject hitObject))
{
if (Precision.AlmostBigger(hitObject.GetEndTime(), b.HitObject.StartTime, 1))
if (Precision.AlmostBigger(hitObject.GetEndTime(), b.Item.StartTime, 1))
break;
currentConcurrentObjects.Pop();
@ -168,7 +165,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
// if the stack gets too high, we should have space below it to display the next batch of objects.
// importantly, we only do this if time has incremented, else a stack of hitobjects all at the same time value would start to overlap themselves.
if (currentConcurrentObjects.TryPeek(out HitObject h) && !Precision.AlmostEquals(h.StartTime, b.HitObject.StartTime, 1))
if (currentConcurrentObjects.TryPeek(out HitObject h) && !Precision.AlmostEquals(h.StartTime, b.Item.StartTime, 1))
{
if (currentConcurrentObjects.Count >= stack_reset_count)
currentConcurrentObjects.Clear();
@ -176,15 +173,15 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
b.Y = -(stack_offset * currentConcurrentObjects.Count);
currentConcurrentObjects.Push(b.HitObject);
currentConcurrentObjects.Push(b.Item);
}
}
protected override SelectionHandler CreateSelectionHandler() => new TimelineSelectionHandler();
protected override SelectionHandler<HitObject> CreateSelectionHandler() => new TimelineSelectionHandler();
protected override SelectionBlueprint CreateBlueprintFor(HitObject hitObject)
protected override SelectionBlueprint<HitObject> CreateBlueprintFor(HitObject item)
{
return new TimelineHitObjectBlueprint(hitObject)
return new TimelineHitObjectBlueprint(item)
{
OnDragHandled = handleScrollViaDrag
};
@ -239,10 +236,10 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
}
}
internal class TimelineSelectionHandler : SelectionHandler, IKeyBindingHandler<GlobalAction>
internal class TimelineSelectionHandler : EditorSelectionHandler, IKeyBindingHandler<GlobalAction>
{
// for now we always allow movement. snapping is provided by the Timeline's "distance" snap implementation
public override bool HandleMovement(MoveSelectionEvent moveEvent) => true;
public override bool HandleMovement(MoveSelectionEvent<HitObject> moveEvent) => true;
public bool OnPressed(GlobalAction action)
{
@ -344,13 +341,13 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
}
}
protected class TimelineSelectionBlueprintContainer : Container<SelectionBlueprint>
protected class TimelineSelectionBlueprintContainer : Container<SelectionBlueprint<HitObject>>
{
protected override Container<SelectionBlueprint> Content { get; }
protected override Container<SelectionBlueprint<HitObject>> Content { get; }
public TimelineSelectionBlueprintContainer()
{
AddInternal(new TimelinePart<SelectionBlueprint>(Content = new HitObjectOrderedSelectionContainer { RelativeSizeAxes = Axes.Both }) { RelativeSizeAxes = Axes.Both });
AddInternal(new TimelinePart<SelectionBlueprint<HitObject>>(Content = new HitObjectOrderedSelectionContainer { RelativeSizeAxes = Axes.Both }) { RelativeSizeAxes = Axes.Both });
}
}
}

View File

@ -26,7 +26,7 @@ using osuTK.Graphics;
namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
public class TimelineHitObjectBlueprint : SelectionBlueprint
public class TimelineHitObjectBlueprint : SelectionBlueprint<HitObject>
{
private const float circle_size = 38;
@ -49,13 +49,13 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
[Resolved]
private ISkinSource skin { get; set; }
public TimelineHitObjectBlueprint(HitObject hitObject)
: base(hitObject)
public TimelineHitObjectBlueprint(HitObject item)
: base(item)
{
Anchor = Anchor.CentreLeft;
Origin = Anchor.CentreLeft;
startTime = hitObject.StartTimeBindable.GetBoundCopy();
startTime = item.StartTimeBindable.GetBoundCopy();
startTime.BindValueChanged(time => X = (float)time.NewValue, true);
RelativePositionAxes = Axes.X;
@ -95,9 +95,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
},
});
if (hitObject is IHasDuration)
if (item is IHasDuration)
{
colouredComponents.Add(new DragArea(hitObject)
colouredComponents.Add(new DragArea(item)
{
OnDragHandled = e => OnDragHandled?.Invoke(e)
});
@ -108,7 +108,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
base.LoadComplete();
if (HitObject is IHasComboInformation comboInfo)
if (Item is IHasComboInformation comboInfo)
{
indexInCurrentComboBindable = comboInfo.IndexInCurrentComboBindable.GetBoundCopy();
indexInCurrentComboBindable.BindValueChanged(_ => updateComboIndex(), true);
@ -136,7 +136,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
private void updateComboColour()
{
if (!(HitObject is IHasComboInformation combo))
if (!(Item is IHasComboInformation combo))
return;
var comboColours = skin.GetConfig<GlobalSkinColours, IReadOnlyList<Color4>>(GlobalSkinColours.ComboColours)?.Value ?? Array.Empty<Color4>();
@ -152,7 +152,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
border.Hide();
}
if (HitObject is IHasDuration duration && duration.Duration > 0)
if (Item is IHasDuration duration && duration.Duration > 0)
circle.Colour = ColourInfo.GradientHorizontal(comboColour, comboColour.Lighten(0.4f));
else
circle.Colour = comboColour;
@ -166,14 +166,14 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
base.Update();
// no bindable so we perform this every update
float duration = (float)(HitObject.GetEndTime() - HitObject.StartTime);
float duration = (float)(Item.GetEndTime() - Item.StartTime);
if (Width != duration)
{
Width = duration;
// kind of haphazard but yeah, no bindables.
if (HitObject is IHasRepeats repeats)
if (Item is IHasRepeats repeats)
updateRepeats(repeats);
}
}

View File

@ -301,13 +301,7 @@ namespace osu.Game.Screens.Edit
return list.Count - 1;
}
public double SnapTime(double time, double? referenceTime)
{
var timingPoint = ControlPointInfo.TimingPointAt(referenceTime ?? time);
var beatLength = timingPoint.BeatLength / BeatDivisor;
return timingPoint.Time + (int)Math.Round((time - timingPoint.Time) / beatLength, MidpointRounding.AwayFromZero) * beatLength;
}
public double SnapTime(double time, double? referenceTime) => ControlPointInfo.GetClosestSnappedTime(time, BeatDivisor, referenceTime);
public double GetBeatLengthAtTime(double referenceTime) => ControlPointInfo.TimingPointAt(referenceTime).BeatLength / BeatDivisor;

View File

@ -144,9 +144,15 @@ namespace osu.Game.Users
[JsonProperty(@"unranked_beatmapset_count")]
public int UnrankedBeatmapsetCount;
[JsonProperty(@"scores_best_count")]
public int ScoresBestCount;
[JsonProperty(@"scores_first_count")]
public int ScoresFirstCount;
[JsonProperty(@"scores_recent_count")]
public int ScoresRecentCount;
[JsonProperty(@"beatmap_playcounts_count")]
public int BeatmapPlaycountsCount;

View File

@ -19,7 +19,7 @@
</ItemGroup>
<ItemGroup Label="Package References">
<PackageReference Include="DiffPlex" Version="1.7.0" />
<PackageReference Include="Humanizer" Version="2.9.9" />
<PackageReference Include="Humanizer" Version="2.8.26" />
<PackageReference Include="MessagePack" Version="2.2.85" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="5.0.5" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="5.0.5" />

View File

@ -89,7 +89,7 @@
<!-- Xamarin.iOS does not automatically handle transitive dependencies from NuGet packages. -->
<ItemGroup Label="Transitive Dependencies">
<PackageReference Include="DiffPlex" Version="1.6.3" />
<PackageReference Include="Humanizer" Version="2.9.9" />
<PackageReference Include="Humanizer" Version="2.8.26" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />