1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-15 06:42: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.ScrollTime, 1500.0, DrawableManiaRuleset.MIN_TIME_RANGE, DrawableManiaRuleset.MAX_TIME_RANGE, 5);
SetDefault(ManiaRulesetSetting.ScrollDirection, ManiaScrollingDirection.Down); SetDefault(ManiaRulesetSetting.ScrollDirection, ManiaScrollingDirection.Down);
SetDefault(ManiaRulesetSetting.TimingBasedNoteColouring, false);
} }
public override TrackedSettings CreateTrackedSettings() => new TrackedSettings public override TrackedSettings CreateTrackedSettings() => new TrackedSettings
@ -34,6 +35,7 @@ namespace osu.Game.Rulesets.Mania.Configuration
public enum ManiaRulesetSetting public enum ManiaRulesetSetting
{ {
ScrollTime, ScrollTime,
ScrollDirection ScrollDirection,
TimingBasedNoteColouring
} }
} }

View File

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

View File

@ -37,6 +37,11 @@ namespace osu.Game.Rulesets.Mania
Current = config.GetBindable<double>(ManiaRulesetSetting.ScrollTime), Current = config.GetBindable<double>(ManiaRulesetSetting.ScrollTime),
KeyboardStep = 5 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. // See the LICENCE file in the repository root for full licence text.
using System.Diagnostics; using System.Diagnostics;
using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Input.Bindings; 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.Mania.Skinning.Default;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Screens.Edit;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Mania.Objects.Drawables namespace osu.Game.Rulesets.Mania.Objects.Drawables
{ {
@ -17,6 +23,14 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
/// </summary> /// </summary>
public class DrawableNote : DrawableManiaHitObject<Note>, IKeyBindingHandler<ManiaAction> 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; protected virtual ManiaSkinComponents Component => ManiaSkinComponents.Note;
private readonly Drawable headPiece; 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) protected override void OnDirectionChanged(ValueChangedEvent<ScrollingDirection> e)
{ {
base.OnDirectionChanged(e); base.OnDirectionChanged(e);
@ -73,5 +99,14 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
public virtual void OnReleased(ManiaAction action) 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. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Game.Rulesets.Mania.Objects.Types;
using osu.Game.Rulesets.Mania.Scoring; using osu.Game.Rulesets.Mania.Scoring;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types; 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 public abstract class OsuSelectionBlueprint<T> : OverlaySelectionBlueprint
where T : OsuHitObject where T : OsuHitObject
{ {
protected new T HitObject => (T)DrawableObject.HitObject; protected T HitObject => (T)DrawableObject.HitObject;
protected override bool AlwaysShowWhenSelected => true; protected override bool AlwaysShowWhenSelected => true;

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles; using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders; 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) public override OverlaySelectionBlueprint CreateBlueprintFor(DrawableHitObject hitObject)
{ {

View File

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

View File

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

View File

@ -2,53 +2,15 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic; using System.Collections.Generic;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Game.Configuration;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.UI;
using osuTK;
namespace osu.Game.Rulesets.Osu.Mods 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) public void ApplyToDrawableHitObjects(IEnumerable<DrawableHitObject> drawables)
{ {
foreach (var d in drawables) foreach (var d in drawables)
@ -58,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Mods
switch (d) switch (d)
{ {
case DrawableHitCircle circle: case DrawableHitCircle circle:
circle.CirclePiece.Rotation = -currentRotation; circle.CirclePiece.Rotation = -CurrentRotation;
break; break;
} }
}; };

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Taiko.Edit.Blueprints; using osu.Game.Rulesets.Taiko.Edit.Blueprints;
using osu.Game.Screens.Edit.Compose.Components; 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) => public override OverlaySelectionBlueprint CreateBlueprintFor(DrawableHitObject hitObject) =>
new TaikoSelectionBlueprint(hitObject); new TaikoSelectionBlueprint(hitObject);

View File

@ -8,12 +8,13 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Screens.Edit.Compose.Components; using osu.Game.Screens.Edit.Compose.Components;
namespace osu.Game.Rulesets.Taiko.Edit 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> selectionRimState = new Bindable<TernaryState>();
private readonly Bindable<TernaryState> selectionStrongState = 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 } }; 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 } }; 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() 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); protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false);
private BlueprintContainer blueprintContainer private EditorBlueprintContainer blueprintContainer
=> Editor.ChildrenOfType<BlueprintContainer>().First(); => Editor.ChildrenOfType<EditorBlueprintContainer>().First();
[Test] [Test]
public void TestSelectedObjectHasPriorityWhenOverlapping() public void TestSelectedObjectHasPriorityWhenOverlapping()

View File

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

View File

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

View File

@ -14,4 +14,10 @@ namespace osu.Game.Beatmaps
Qualified = 3, Qualified = 3,
Loved = 4, 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 Newtonsoft.Json;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Lists; using osu.Framework.Lists;
using osu.Framework.Utils;
using osu.Game.Screens.Edit;
namespace osu.Game.Beatmaps.ControlPoints namespace osu.Game.Beatmaps.ControlPoints
{ {
@ -160,6 +162,58 @@ namespace osu.Game.Beatmaps.ControlPoints
groups.Remove(group); 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> /// <summary>
/// Binary searches one of the control point lists to find the active control point at <paramref name="time"/>. /// 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. /// 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. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using osu.Framework.Graphics;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Framework.Threading; using osu.Framework.Threading;
using osuTK;
namespace osu.Game.Extensions namespace osu.Game.Extensions
{ {
@ -32,5 +34,14 @@ namespace osu.Game.Extensions
scheduler.Add(repeatDelegate); scheduler.Add(repeatDelegate);
return 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; 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, RelativeSizeAxes = Axes.Both,
Masking = true,
CornerRadius = 5,
Children = new Drawable[] Children = new Drawable[]
{ {
button = new HeaderButton { RelativeSizeAxes = Axes.Both }, textSprites = new FillFlowContainer
new Container
{ {
// cannot nest inside here due to the structure of button (putting things in its own content). Anchor = Anchor.CentreLeft,
// requires framework fix. Origin = Anchor.CentreLeft,
Padding = new MarginPadding { Horizontal = 10 }, AutoSizeAxes = Axes.Both,
RelativeSizeAxes = Axes.Both, AutoSizeDuration = 500,
Children = new Drawable[] AutoSizeEasing = Easing.OutQuint,
{ Direction = FillDirection.Vertical,
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),
},
}
}, },
new DownloadProgressBar(BeatmapSet.Value) new SpriteIcon
{ {
Depth = -2, Anchor = Anchor.CentreRight,
Anchor = Anchor.BottomLeft, Origin = Anchor.CentreRight,
Origin = Anchor.BottomLeft, 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 scoreInfos = value.Scores.Select(s => s.CreateScoreInfo(rulesets)).ToList();
var topScore = scoreInfos.First(); var topScore = scoreInfos.First();
scoreTable.DisplayScores(scoreInfos, topScore.Beatmap?.Status == BeatmapSetOnlineStatus.Ranked); scoreTable.DisplayScores(scoreInfos, topScore.Beatmap?.Status.GrantsPerformancePoints() == true);
scoreTable.Show(); scoreTable.Show();
var userScore = value.UserScore; var userScore = value.UserScore;

View File

@ -111,7 +111,8 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
accuracyColumn.Text = value.DisplayAccuracy; accuracyColumn.Text = value.DisplayAccuracy;
maxComboColumn.Text = $@"{value.MaxCombo:N0}x"; 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}"; ppColumn.Text = $@"{value.PP:N0}";
statisticsColumns.ChildrenEnumerable = value.GetStatisticsForDisplay().Select(createStatisticsColumn); statisticsColumns.ChildrenEnumerable = value.GetStatisticsForDisplay().Select(createStatisticsColumn);

View File

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

View File

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

View File

@ -20,7 +20,7 @@ namespace osu.Game.Overlays.Profile.Sections
{ {
new PlayHistorySubsection(User), new PlayHistorySubsection(User),
new PaginatedMostPlayedBeatmapContainer(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) new ReplaysSubsection(User)
}; };
} }

View File

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

View File

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

View File

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

View File

@ -22,7 +22,11 @@ namespace osu.Game.Rulesets.Edit
// Audio // Audio
new CheckAudioPresence(), new CheckAudioPresence(),
new CheckAudioQuality() new CheckAudioQuality(),
// Compose
new CheckUnsnappedObjects(),
new CheckConcurrentObjects()
}; };
public IEnumerable<Issue> Run(IBeatmap playableBeatmap, WorkingBeatmap workingBeatmap) 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> /// <summary>
/// Construct a relevant blueprint container. This will manage hitobject selection/placement input handling and display logic. /// Construct a relevant blueprint container. This will manage hitobject selection/placement input handling and display logic.
/// </summary> /// </summary>
protected virtual ComposeBlueprintContainer CreateBlueprintContainer() protected virtual ComposeBlueprintContainer CreateBlueprintContainer() => new ComposeBlueprintContainer(this);
=> new ComposeBlueprintContainer(this);
/// <summary> /// <summary>
/// Construct a drawable ruleset for the provided ruleset. /// Construct a drawable ruleset for the provided ruleset.

View File

@ -3,12 +3,13 @@
using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Primitives;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osuTK; using osuTK;
namespace osu.Game.Rulesets.Edit namespace osu.Game.Rulesets.Edit
{ {
public abstract class OverlaySelectionBlueprint : SelectionBlueprint public abstract class OverlaySelectionBlueprint : SelectionBlueprint<HitObject>
{ {
/// <summary> /// <summary>
/// The <see cref="DrawableHitObject"/> which this <see cref="OverlaySelectionBlueprint"/> applies to. /// 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 Vector2 ScreenSpaceSelectionPoint => DrawableObject.ScreenSpaceDrawQuad.Centre;
public override Quad SelectionQuad => DrawableObject.ScreenSpaceDrawQuad; 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.Primitives;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osuTK; using osuTK;
namespace osu.Game.Rulesets.Edit namespace osu.Game.Rulesets.Edit
{ {
/// <summary> /// <summary>
/// A blueprint placed above a <see cref="DrawableHitObject"/> adding editing functionality. /// A blueprint placed above a displaying item adding editing functionality.
/// </summary> /// </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> /// <summary>
/// Invoked when this <see cref="SelectionBlueprint"/> has been selected. /// Invoked when this <see cref="SelectionBlueprint{T}"/> has been selected.
/// </summary> /// </summary>
public event Action<SelectionBlueprint> Selected; public event Action<SelectionBlueprint<T>> Selected;
/// <summary> /// <summary>
/// Invoked when this <see cref="SelectionBlueprint"/> has been deselected. /// Invoked when this <see cref="SelectionBlueprint{T}"/> has been deselected.
/// </summary> /// </summary>
public event Action<SelectionBlueprint> Deselected; public event Action<SelectionBlueprint<T>> Deselected;
public override bool HandlePositionalInput => ShouldBeAlive; public override bool HandlePositionalInput => ShouldBeAlive;
public override bool RemoveWhenNotAlive => false; public override bool RemoveWhenNotAlive => false;
protected SelectionBlueprint(HitObject hitObject) protected SelectionBlueprint(T item)
{ {
HitObject = hitObject; Item = item;
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
AlwaysPresent = true; AlwaysPresent = true;
@ -87,7 +85,7 @@ namespace osu.Game.Rulesets.Edit
protected virtual void OnDeselected() 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. // set the body piece's alpha directly to avoid arbitrarily rendering frame buffers etc. of children.
foreach (var d in InternalChildren) foreach (var d in InternalChildren)
d.Hide(); d.Hide();
@ -129,7 +127,7 @@ namespace osu.Game.Rulesets.Edit
public virtual MenuItem[] ContextMenuItems => Array.Empty<MenuItem>(); public virtual MenuItem[] ContextMenuItems => Array.Empty<MenuItem>();
/// <summary> /// <summary>
/// The screen-space point that causes this <see cref="OverlaySelectionBlueprint"/> to be selected. /// The screen-space point that causes this <see cref="OverlaySelectionBlueprint"/> to be selected via a drag.
/// </summary> /// </summary>
public virtual Vector2 ScreenSpaceSelectionPoint => ScreenSpaceDrawQuad.Centre; public virtual Vector2 ScreenSpaceSelectionPoint => ScreenSpaceDrawQuad.Centre;
@ -138,8 +136,6 @@ namespace osu.Game.Rulesets.Edit
/// </summary> /// </summary>
public virtual Quad SelectionQuad => ScreenSpaceDrawQuad; public virtual Quad SelectionQuad => ScreenSpaceDrawQuad;
public virtual Vector2 GetInstantDelta(Vector2 screenSpacePosition) => Parent.ToLocalSpace(screenSpacePosition) - Position;
/// <summary> /// <summary>
/// Handle to perform a partial deletion when the user requests a quick delete (Shift+Right Click). /// Handle to perform a partial deletion when the user requests a quick delete (Shift+Right Click).
/// </summary> /// </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. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
namespace osu.Game.Rulesets.Mania.Objects.Types namespace osu.Game.Rulesets.Objects.Types
{ {
/// <summary> /// <summary>
/// A type of hit object which lies in one of a number of predetermined columns. /// 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> /// <summary>
/// The beatmap. /// The beatmap.
/// </summary> /// </summary>
[Cached(typeof(IBeatmap))]
public readonly Beatmap<TObject> Beatmap; public readonly Beatmap<TObject> Beatmap;
public override IEnumerable<HitObject> Objects => Beatmap.HitObjects; public override IEnumerable<HitObject> Objects => Beatmap.HitObjects;

View File

@ -8,7 +8,6 @@ using System.Text;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.IO.Legacy; using osu.Game.IO.Legacy;
using osu.Game.Replays.Legacy;
using osu.Game.Rulesets.Replays.Types; using osu.Game.Rulesets.Replays.Types;
using SharpCompress.Compressors.LZMA; using SharpCompress.Compressors.LZMA;
@ -91,12 +90,14 @@ namespace osu.Game.Scoring.Legacy
if (score.Replay != null) 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))) 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},")); // Rounding because stable could only parse integral values
lastF = f; 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 namespace osu.Game.Screens.Edit.Components.RadioButtons
{ {
public class DrawableRadioButton : TriangleButton public class DrawableRadioButton : OsuButton
{ {
/// <summary> /// <summary>
/// Invoked when this <see cref="DrawableRadioButton"/> has been selected. /// Invoked when this <see cref="DrawableRadioButton"/> has been selected.
@ -49,8 +49,6 @@ namespace osu.Game.Screens.Edit.Components.RadioButtons
selectedBackgroundColour = colours.BlueDark; selectedBackgroundColour = colours.BlueDark;
selectedBubbleColour = selectedBackgroundColour.Lighten(0.5f); selectedBubbleColour = selectedBackgroundColour.Lighten(0.5f);
Triangles.Alpha = 0;
Content.EdgeEffect = new EdgeEffectParameters Content.EdgeEffect = new EdgeEffectParameters
{ {
Type = EdgeEffectType.Shadow, Type = EdgeEffectType.Shadow,

View File

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

View File

@ -3,11 +3,9 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Primitives;
@ -16,46 +14,33 @@ using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osuTK; using osuTK;
using osuTK.Input; using osuTK.Input;
namespace osu.Game.Screens.Edit.Compose.Components namespace osu.Game.Screens.Edit.Compose.Components
{ {
/// <summary> /// <summary>
/// A container which provides a "blueprint" display of hitobjects. /// A container which provides a "blueprint" display of items.
/// Includes selection and manipulation support via a <see cref="Components.SelectionHandler"/>. /// Includes selection and manipulation support via a <see cref="Components.SelectionHandler{T}"/>.
/// </summary> /// </summary>
public abstract class BlueprintContainer : CompositeDrawable, IKeyBindingHandler<PlatformAction> public abstract class BlueprintContainer<T> : CompositeDrawable, IKeyBindingHandler<PlatformAction>
{ {
protected DragBox DragBox { get; private set; } 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; private readonly Dictionary<T, SelectionBlueprint<T>> blueprintMap = new Dictionary<T, SelectionBlueprint<T>>();
[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>();
[Resolved(canBeNull: true)] [Resolved(canBeNull: true)]
private IPositionSnapProvider snapProvider { get; set; } private IPositionSnapProvider snapProvider { get; set; }
protected BlueprintContainer(HitObjectComposer composer) [Resolved(CanBeNull = true)]
{ private IEditorChangeHandler changeHandler { get; set; }
Composer = composer;
protected BlueprintContainer()
{
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
} }
@ -73,66 +58,28 @@ namespace osu.Game.Screens.Edit.Compose.Components
SelectionHandler.CreateProxy(), SelectionHandler.CreateProxy(),
DragBox.CreateProxy().With(p => p.Depth = float.MinValue) 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() protected virtual Container<SelectionBlueprint<T>> CreateSelectionBlueprintContainer() => new Container<SelectionBlueprint<T>> { RelativeSizeAxes = Axes.Both };
{
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 };
/// <summary> /// <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> /// </summary>
protected virtual SelectionHandler CreateSelectionHandler() => new SelectionHandler(); protected abstract SelectionHandler<T> CreateSelectionHandler();
/// <summary> /// <summary>
/// Creates a <see cref="SelectionBlueprint"/> for a specific <see cref="DrawableHitObject"/>. /// Creates a <see cref="SelectionBlueprint{T}"/> for a specific item.
/// </summary> /// </summary>
/// <param name="hitObject">The <see cref="DrawableHitObject"/> to create the overlay for.</param> /// <param name="item">The item to create the overlay for.</param>
protected virtual SelectionBlueprint CreateBlueprintFor(HitObject hitObject) => null; protected virtual SelectionBlueprint<T> CreateBlueprintFor(T item) => null;
protected virtual DragBox CreateDragBox(Action<RectangleF> performSelect) => new DragBox(performSelect); 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) protected override bool OnMouseDown(MouseDownEvent e)
{ {
bool selectionPerformed = performMouseDownActions(e); bool selectionPerformed = performMouseDownActions(e);
@ -143,7 +90,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
return selectionPerformed || e.Button == MouseButton.Left; return selectionPerformed || e.Button == MouseButton.Left;
} }
private SelectionBlueprint clickedBlueprint; protected SelectionBlueprint<T> ClickedBlueprint { get; private set; }
protected override bool OnClick(ClickEvent e) protected override bool OnClick(ClickEvent e)
{ {
@ -151,11 +98,11 @@ namespace osu.Game.Screens.Edit.Compose.Components
return false; return false;
// store for double-click handling // 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 // 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 // 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) if (endClickSelection(e) || ClickedBlueprint != null)
return true; return true;
deselectAll(); deselectAll();
@ -168,10 +115,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
return false; return false;
// ensure the blueprint which was hovered for the first click is still the hovered blueprint. // 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; return false;
EditorClock?.SeekSmoothlyTo(clickedBlueprint.HitObject.StartTime);
return true; return true;
} }
@ -227,10 +173,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
if (isDraggingBlueprint) if (isDraggingBlueprint)
{ {
// handle positional change etc. DragOperationCompleted();
foreach (var obj in selectedHitObjects)
Beatmap.Update(obj);
changeHandler?.EndChange(); changeHandler?.EndChange();
} }
@ -238,6 +181,13 @@ namespace osu.Game.Screens.Edit.Compose.Components
DragBox.Hide(); 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) protected override bool OnKeyDown(KeyDownEvent e)
{ {
switch (e.Key) switch (e.Key)
@ -258,7 +208,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
switch (action.ActionType) switch (action.ActionType)
{ {
case PlatformActionType.SelectAll: case PlatformActionType.SelectAll:
selectAll(); SelectAll();
return true; return true;
} }
@ -271,61 +221,58 @@ namespace osu.Game.Screens.Edit.Compose.Components
#region Blueprint Addition/Removal #region Blueprint Addition/Removal
private void addBlueprintFor(HitObject hitObject) protected virtual void AddBlueprintFor(T item)
{ {
if (hitObject is IBarLine) if (blueprintMap.ContainsKey(item))
return; return;
if (blueprintMap.ContainsKey(hitObject)) var blueprint = CreateBlueprintFor(item);
return;
var blueprint = CreateBlueprintFor(hitObject);
if (blueprint == null) if (blueprint == null)
return; return;
blueprintMap[hitObject] = blueprint; blueprintMap[item] = blueprint;
blueprint.Selected += onBlueprintSelected; blueprint.Selected += OnBlueprintSelected;
blueprint.Deselected += onBlueprintDeselected; blueprint.Deselected += OnBlueprintDeselected;
if (Beatmap.SelectedHitObjects.Contains(hitObject))
blueprint.Select();
SelectionBlueprints.Add(blueprint); 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; return;
blueprint.Deselect(); blueprint.Deselect();
blueprint.Selected -= onBlueprintSelected; blueprint.Selected -= OnBlueprintSelected;
blueprint.Deselected -= onBlueprintDeselected; blueprint.Deselected -= OnBlueprintDeselected;
SelectionBlueprints.Remove(blueprint); SelectionBlueprints.Remove(blueprint);
if (movementBlueprints?.Contains(blueprint) == true) if (movementBlueprints?.Contains(blueprint) == true)
finishSelectionMovement(); finishSelectionMovement();
OnBlueprintRemoved(hitObject); OnBlueprintRemoved(blueprint.Item);
} }
/// <summary> /// <summary>
/// Called after a <see cref="HitObject"/> blueprint has been added. /// Called after an item's blueprint has been added.
/// </summary> /// </summary>
/// <param name="hitObject">The <see cref="HitObject"/> for which the blueprint has been added.</param> /// <param name="item">The item for which the blueprint has been added.</param>
protected virtual void OnBlueprintAdded(HitObject hitObject) protected virtual void OnBlueprintAdded(T item)
{ {
} }
/// <summary> /// <summary>
/// Called after a <see cref="HitObject"/> blueprint has been removed. /// Called after an item's blueprint has been removed.
/// </summary> /// </summary>
/// <param name="hitObject">The <see cref="HitObject"/> for which the blueprint has been removed.</param> /// <param name="item">The item for which the blueprint has been removed.</param>
protected virtual void OnBlueprintRemoved(HitObject hitObject) 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). // Iterate from the top of the input stack (blueprints closest to the front of the screen first).
// Priority is given to already-selected blueprints. // 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; 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). // Iterate from the top of the input stack (blueprints closest to the front of the screen first).
// Priority is given to already-selected blueprints. // 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; if (!blueprint.IsHovered) continue;
@ -404,8 +351,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
break; break;
case SelectionState.Selected: case SelectionState.Selected:
// if the editor is playing, we generally don't want to deselect objects even if outside the selection area. if (AllowDeselectionDuringDrag && !isValidForSelection())
if (!EditorClock.IsRunning && !isValidForSelection())
blueprint.Deselect(); blueprint.Deselect();
break; break;
} }
@ -413,35 +359,29 @@ namespace osu.Game.Screens.Edit.Compose.Components
} }
/// <summary> /// <summary>
/// Selects all <see cref="SelectionBlueprint"/>s. /// Selects all <see cref="SelectionBlueprint{T}"/>s.
/// </summary> /// </summary>
private void selectAll() protected virtual void SelectAll()
{ {
Composer.Playfield.KeepAllAlive();
// Scheduled to allow the change in lifetime to take place. // Scheduled to allow the change in lifetime to take place.
Schedule(() => SelectionBlueprints.ToList().ForEach(m => m.Select())); Schedule(() => SelectionBlueprints.ToList().ForEach(m => m.Select()));
} }
/// <summary> /// <summary>
/// Deselects all selected <see cref="SelectionBlueprint"/>s. /// Deselects all selected <see cref="SelectionBlueprint{T}"/>s.
/// </summary> /// </summary>
private void deselectAll() => SelectionHandler.SelectedBlueprints.ToList().ForEach(m => m.Deselect()); 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); SelectionHandler.HandleSelected(blueprint);
SelectionBlueprints.ChangeChildDepth(blueprint, 1); 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); SelectionBlueprints.ChangeChildDepth(blueprint, 0);
SelectionHandler.HandleDeselected(blueprint); SelectionHandler.HandleDeselected(blueprint);
Composer.Playfield.SetKeepAlive(blueprint.HitObject, false);
} }
#endregion #endregion
@ -449,7 +389,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
#region Selection Movement #region Selection Movement
private Vector2[] movementBlueprintOriginalPositions; private Vector2[] movementBlueprintOriginalPositions;
private SelectionBlueprint[] movementBlueprints; private SelectionBlueprint<T>[] movementBlueprints;
private bool isDraggingBlueprint; private bool isDraggingBlueprint;
/// <summary> /// <summary>
@ -460,16 +400,23 @@ namespace osu.Game.Screens.Edit.Compose.Components
if (!SelectionHandler.SelectedBlueprints.Any()) if (!SelectionHandler.SelectedBlueprints.Any())
return; 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 // A special case is added for when a click selection occurred before the drag
if (!clickSelectionBegan && !SelectionHandler.SelectedBlueprints.Any(b => b.IsHovered)) if (!clickSelectionBegan && !SelectionHandler.SelectedBlueprints.Any(b => b.IsHovered))
return; return;
// Movement is tracked from the blueprint of the earliest hitobject, since it only makes sense to distance snap from that hitobject // Movement is tracked from the blueprint of the earliest item, since it only makes sense to distance snap from that item
movementBlueprints = SelectionHandler.SelectedBlueprints.OrderBy(b => b.HitObject.StartTime).ToArray(); movementBlueprints = SortForMovement(SelectionHandler.SelectedBlueprints).ToArray();
movementBlueprintOriginalPositions = movementBlueprints.Select(m => m.ScreenSpaceSelectionPoint).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> /// <summary>
/// Moves the current selected blueprints. /// Moves the current selected blueprints.
/// </summary> /// </summary>
@ -480,52 +427,50 @@ namespace osu.Game.Screens.Edit.Compose.Components
if (movementBlueprints == null) if (movementBlueprints == null)
return false; return false;
if (snapProvider == null)
return true;
Debug.Assert(movementBlueprintOriginalPositions != null); Debug.Assert(movementBlueprintOriginalPositions != null);
Vector2 distanceTravelled = e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition; Vector2 distanceTravelled = e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition;
// check for positional snap for every object in selection (for things like object-object snapping) if (snapProvider != null)
for (var i = 0; i < movementBlueprintOriginalPositions.Length; i++)
{ {
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. var delta = positionalResult.ScreenSpacePosition - movementBlueprints[i].ScreenSpaceSelectionPoint;
if (SelectionHandler.HandleMovement(new MoveSelectionEvent(movementBlueprints[i], positionalResult.ScreenSpacePosition)))
return true; // 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 // 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. // The final movement position, relative to movementBlueprintOriginalPosition.
Vector2 movePosition = movementBlueprintOriginalPositions.First() + distanceTravelled; Vector2 movePosition = movementBlueprintOriginalPositions.First() + distanceTravelled;
// Retrieve a snapped position. // Retrieve a snapped position.
var result = snapProvider.SnapScreenSpacePositionToValidTime(movePosition); var result = snapProvider?.SnapScreenSpacePositionToValidTime(movePosition);
// Move the hitobjects. if (result == null)
if (!SelectionHandler.HandleMovement(new MoveSelectionEvent(movementBlueprints.First(), result.ScreenSpacePosition)))
return true;
if (result.Time.HasValue)
{ {
// Apply the start time at the newly snapped-to position return SelectionHandler.HandleMovement(new MoveSelectionEvent<T>(movementBlueprints.First(), movePosition - movementBlueprints.First().ScreenSpaceSelectionPoint));
double offset = result.Time.Value - movementBlueprints.First().HitObject.StartTime;
if (offset != 0)
Beatmap.PerformOnSelection(obj => obj.StartTime += offset);
} }
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> /// <summary>
/// Finishes the current movement of selected blueprints. /// Finishes the current movement of selected blueprints.
/// </summary> /// </summary>
@ -542,22 +487,5 @@ namespace osu.Game.Screens.Edit.Compose.Components
} }
#endregion #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> /// <summary>
/// A blueprint container generally displayed as an overlay to a ruleset's playfield. /// A blueprint container generally displayed as an overlay to a ruleset's playfield.
/// </summary> /// </summary>
public class ComposeBlueprintContainer : BlueprintContainer public class ComposeBlueprintContainer : EditorBlueprintContainer
{ {
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
private readonly Container<PlacementBlueprint> placementBlueprintContainer; private readonly Container<PlacementBlueprint> placementBlueprintContainer;
protected new EditorSelectionHandler SelectionHandler => (EditorSelectionHandler)base.SelectionHandler;
private PlacementBlueprint currentPlacement; private PlacementBlueprint currentPlacement;
private InputManager inputManager; private InputManager inputManager;
@ -113,7 +115,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
// convert to game space coordinates // convert to game space coordinates
delta = firstBlueprint.ToScreenSpace(delta) - firstBlueprint.ToScreenSpace(Vector2.Zero); 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() private void updatePlacementNewCombo()
@ -237,9 +239,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
updatePlacementPosition(); 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) if (drawable == null)
return null; return null;
@ -249,9 +251,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
public virtual OverlaySelectionBlueprint CreateBlueprintFor(DrawableHitObject hitObject) => null; 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(); 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 namespace osu.Game.Screens.Edit.Compose.Components
{ {
/// <summary> /// <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> /// </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); base.Add(drawable);
bindStartTime(drawable); bindStartTime(drawable);
} }
public override bool Remove(SelectionBlueprint drawable) public override bool Remove(SelectionBlueprint<HitObject> drawable)
{ {
if (!base.Remove(drawable)) if (!base.Remove(drawable))
return false; return false;
@ -36,11 +36,11 @@ namespace osu.Game.Screens.Edit.Compose.Components
unbindAllStartTimes(); 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(_ => bindable.BindValueChanged(_ =>
{ {
@ -51,7 +51,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
startTimeMap[blueprint] = bindable; startTimeMap[blueprint] = bindable;
} }
private void unbindStartTime(SelectionBlueprint blueprint) private void unbindStartTime(SelectionBlueprint<HitObject> blueprint)
{ {
startTimeMap[blueprint].UnbindAll(); startTimeMap[blueprint].UnbindAll();
startTimeMap.Remove(blueprint); startTimeMap.Remove(blueprint);
@ -66,16 +66,16 @@ namespace osu.Game.Screens.Edit.Compose.Components
protected override int Compare(Drawable x, Drawable y) protected override int Compare(Drawable x, Drawable y)
{ {
var xObj = (SelectionBlueprint)x; var xObj = (SelectionBlueprint<HitObject>)x;
var yObj = (SelectionBlueprint)y; var yObj = (SelectionBlueprint<HitObject>)y;
// Put earlier blueprints towards the end of the list, so they handle input first // 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; if (i != 0) return i;
// Fall back to end time if the start time is equal. // 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; return i == 0 ? CompareReverseChildID(y, x) : i;
} }

View File

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

View File

@ -4,43 +4,44 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Humanizer;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input; using osu.Framework.Input;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Audio;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit; 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;
using osuTK.Input; using osuTK.Input;
namespace osu.Game.Screens.Edit.Compose.Components namespace osu.Game.Screens.Edit.Compose.Components
{ {
/// <summary> /// <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> /// </summary>
public class SelectionHandler : CompositeDrawable, IKeyBindingHandler<PlatformAction>, IHasContextMenu public abstract class SelectionHandler<T> : CompositeDrawable, IKeyBindingHandler<PlatformAction>, IHasContextMenu
{ {
/// <summary> /// <summary>
/// The currently selected blueprints. /// The currently selected blueprints.
/// Should be used when operations are dealing directly with the visible 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> /// </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; private Drawable content;
@ -48,15 +49,12 @@ namespace osu.Game.Screens.Edit.Compose.Components
protected SelectionBox SelectionBox { get; private set; } protected SelectionBox SelectionBox { get; private set; }
[Resolved]
protected EditorBeatmap EditorBeatmap { get; private set; }
[Resolved(CanBeNull = true)] [Resolved(CanBeNull = true)]
protected IEditorChangeHandler ChangeHandler { get; private set; } protected IEditorChangeHandler ChangeHandler { get; private set; }
public SelectionHandler() protected SelectionHandler()
{ {
selectedBlueprints = new List<SelectionBlueprint>(); selectedBlueprints = new List<SelectionBlueprint<T>>();
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
AlwaysPresent = true; AlwaysPresent = true;
@ -66,8 +64,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colours) private void load(OsuColour colours)
{ {
createStateBindables();
InternalChild = content = new Container InternalChild = content = new Container
{ {
Children = new Drawable[] Children = new Drawable[]
@ -95,6 +91,11 @@ namespace osu.Game.Screens.Edit.Compose.Components
SelectionBox = CreateSelectionBox(), SelectionBox = CreateSelectionBox(),
} }
}; };
SelectedItems.CollectionChanged += (sender, args) =>
{
Scheduler.AddOnce(updateVisibility);
};
} }
public SelectionBox CreateSelectionBox() public SelectionBox CreateSelectionBox()
@ -128,45 +129,44 @@ namespace osu.Game.Screens.Edit.Compose.Components
#region User Input Handling #region User Input Handling
/// <summary> /// <summary>
/// Handles the selected <see cref="DrawableHitObject"/>s being moved. /// Handles the selected items being moved.
/// </summary> /// </summary>
/// <remarks> /// <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. /// Custom implementation is only required if other attributes are to be considered, like changing columns.
/// </remarks> /// </remarks>
/// <param name="moveEvent">The move event.</param> /// <param name="moveEvent">The move event.</param>
/// <returns> /// <returns>
/// Whether any <see cref="DrawableHitObject"/>s could be moved. /// Whether any items could be moved.
/// Returning true will also propagate StartTime changes provided by the closest <see cref="IPositionSnapProvider.SnapScreenSpacePositionToValidTime"/>.
/// </returns> /// </returns>
public virtual bool HandleMovement(MoveSelectionEvent moveEvent) => false; public virtual bool HandleMovement(MoveSelectionEvent<T> moveEvent) => false;
/// <summary> /// <summary>
/// Handles the selected <see cref="DrawableHitObject"/>s being rotated. /// Handles the selected items being rotated.
/// </summary> /// </summary>
/// <param name="angle">The delta angle to apply to the selection.</param> /// <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; public virtual bool HandleRotation(float angle) => false;
/// <summary> /// <summary>
/// Handles the selected <see cref="DrawableHitObject"/>s being scaled. /// Handles the selected items being scaled.
/// </summary> /// </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> /// <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; public virtual bool HandleScale(Vector2 scale, Anchor anchor) => false;
/// <summary> /// <summary>
/// Handles the selected <see cref="DrawableHitObject"/>s being flipped. /// Handles the selected items being flipped.
/// </summary> /// </summary>
/// <param name="direction">The direction to flip</param> /// <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; public virtual bool HandleFlip(Direction direction) => false;
/// <summary> /// <summary>
/// Handles the selected <see cref="DrawableHitObject"/>s being reversed pattern-wise. /// Handles the selected items being reversed pattern-wise.
/// </summary> /// </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 virtual bool HandleReverse() => false;
public bool OnPressed(PlatformAction action) public bool OnPressed(PlatformAction action)
@ -174,7 +174,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
switch (action.ActionMethod) switch (action.ActionMethod)
{ {
case PlatformActionMethod.Delete: case PlatformActionMethod.Delete:
deleteSelected(); DeleteSelected();
return true; return true;
} }
@ -198,24 +198,23 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// Handle a blueprint becoming selected. /// Handle a blueprint becoming selected.
/// </summary> /// </summary>
/// <param name="blueprint">The blueprint.</param> /// <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. selectedBlueprints.Add(blueprint);
if (!EditorBeatmap.SelectedHitObjects.Contains(blueprint.HitObject))
EditorBeatmap.SelectedHitObjects.Add(blueprint.HitObject);
} }
/// <summary> /// <summary>
/// Handle a blueprint becoming deselected. /// Handle a blueprint becoming deselected.
/// </summary> /// </summary>
/// <param name="blueprint">The blueprint.</param> /// <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); selectedBlueprints.Remove(blueprint);
EditorBeatmap.SelectedHitObjects.Remove(blueprint.HitObject);
} }
/// <summary> /// <summary>
@ -224,7 +223,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// <param name="blueprint">The blueprint.</param> /// <param name="blueprint">The blueprint.</param>
/// <param name="e">The mouse event responsible for selection.</param> /// <param name="e">The mouse event responsible for selection.</param>
/// <returns>Whether a selection was performed.</returns> /// <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) 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="blueprint">The blueprint.</param>
/// <param name="e">The mouse event responsible for deselection.</param> /// <param name="e">The mouse event responsible for deselection.</param>
/// <returns>Whether a deselection was performed.</returns> /// <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) if (blueprint.IsSelected)
{ {
@ -259,23 +258,29 @@ namespace osu.Game.Screens.Edit.Compose.Components
return false; return false;
} }
private void handleQuickDeletion(SelectionBlueprint blueprint) private void handleQuickDeletion(SelectionBlueprint<T> blueprint)
{ {
if (blueprint.HandleQuickDeletion()) if (blueprint.HandleQuickDeletion())
return; return;
if (!blueprint.IsSelected) if (!blueprint.IsSelected)
EditorBeatmap.Remove(blueprint.HitObject); DeleteItems(new[] { blueprint.Item });
else 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> /// <summary>
/// Ensure the blueprint is in a selected state. /// Ensure the blueprint is in a selected state.
/// </summary> /// </summary>
/// <param name="blueprint">The blueprint to select.</param> /// <param name="blueprint">The blueprint to select.</param>
/// <returns>Whether selection state was changed.</returns> /// <returns>Whether selection state was changed.</returns>
private bool ensureSelected(SelectionBlueprint blueprint) private bool ensureSelected(SelectionBlueprint<T> blueprint)
{ {
if (blueprint.IsSelected) if (blueprint.IsSelected)
return false; return false;
@ -285,9 +290,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
return true; return true;
} }
private void deleteSelected() protected void DeleteSelected()
{ {
EditorBeatmap.RemoveRange(selectedBlueprints.Select(b => b.HitObject)); DeleteItems(selectedBlueprints.Select(b => b.Item));
} }
#endregion #endregion
@ -295,11 +300,11 @@ namespace osu.Game.Screens.Edit.Compose.Components
#region Outline Display #region Outline Display
/// <summary> /// <summary>
/// Updates whether this <see cref="SelectionHandler"/> is visible. /// Updates whether this <see cref="SelectionHandler{T}"/> is visible.
/// </summary> /// </summary>
private void updateVisibility() private void updateVisibility()
{ {
int count = EditorBeatmap.SelectedHitObjects.Count; int count = SelectedItems.Count;
selectionDetailsText.Text = count > 0 ? count.ToString() : string.Empty; selectionDetailsText.Text = count > 0 ? count.ToString() : string.Empty;
@ -308,7 +313,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
} }
/// <summary> /// <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. /// Should update the selection box's state to match supported operations.
/// </summary> /// </summary>
protected virtual void OnSelectionChanged() protected virtual void OnSelectionChanged()
@ -322,159 +327,16 @@ namespace osu.Game.Screens.Edit.Compose.Components
if (selectedBlueprints.Count == 0) if (selectedBlueprints.Count == 0)
return; return;
// Move the rectangle to cover the hitobjects // Move the rectangle to cover the items
var topLeft = new Vector2(float.MaxValue, float.MaxValue); RectangleF selectionRect = ToLocalSpace(selectedBlueprints[0].SelectionQuad).AABBFloat;
var bottomRight = new Vector2(float.MinValue, float.MinValue);
foreach (var blueprint in selectedBlueprints) for (int i = 1; i < selectedBlueprints.Count; i++)
{ selectionRect = RectangleF.Union(selectionRect, ToLocalSpace(selectedBlueprints[i].SelectionQuad).AABBFloat);
topLeft = Vector2.ComponentMin(topLeft, ToLocalSpace(blueprint.SelectionQuad.TopLeft));
bottomRight = Vector2.ComponentMax(bottomRight, ToLocalSpace(blueprint.SelectionQuad.BottomRight));
}
topLeft -= new Vector2(5); selectionRect = selectionRect.Inflate(5f);
bottomRight += new Vector2(5);
content.Size = bottomRight - topLeft; content.Position = selectionRect.Location;
content.Position = topLeft; content.Size = selectionRect.Size;
}
#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;
} }
#endregion #endregion
@ -485,30 +347,17 @@ namespace osu.Game.Screens.Edit.Compose.Components
{ {
get get
{ {
if (!selectedBlueprints.Any(b => b.IsHovered)) if (!SelectedBlueprints.Any(b => b.IsHovered))
return Array.Empty<MenuItem>(); return Array.Empty<MenuItem>();
var items = new List<MenuItem>(); var items = new List<MenuItem>();
items.AddRange(GetContextMenuItemsForSelection(selectedBlueprints)); items.AddRange(GetContextMenuItemsForSelection(SelectedBlueprints));
if (selectedBlueprints.All(b => b.HitObject is IHasComboInformation)) if (SelectedBlueprints.Count == 1)
{ items.AddRange(SelectedBlueprints[0].ContextMenuItems);
items.Add(new TernaryStateMenuItem("New combo") { State = { BindTarget = SelectionNewComboState } });
}
if (selectedBlueprints.Count == 1) items.Add(new OsuMenuItem("Delete", MenuItemType.Destructive, DeleteSelected));
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),
});
return items.ToArray(); return items.ToArray();
} }
@ -519,7 +368,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// </summary> /// </summary>
/// <param name="selection">The current selection.</param> /// <param name="selection">The current selection.</param>
/// <returns>The relevant menu items.</returns> /// <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>(); => Enumerable.Empty<MenuItem>();
#endregion #endregion

View File

@ -25,20 +25,17 @@ using osuTK.Graphics;
namespace osu.Game.Screens.Edit.Compose.Components.Timeline namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{ {
internal class TimelineBlueprintContainer : BlueprintContainer internal class TimelineBlueprintContainer : EditorBlueprintContainer
{ {
[Resolved(CanBeNull = true)] [Resolved(CanBeNull = true)]
private Timeline timeline { get; set; } private Timeline timeline { get; set; }
[Resolved]
private EditorBeatmap beatmap { get; set; }
[Resolved] [Resolved]
private OsuColour colours { get; set; } private OsuColour colours { get; set; }
private DragEvent lastDragEvent; private DragEvent lastDragEvent;
private Bindable<HitObject> placement; private Bindable<HitObject> placement;
private SelectionBlueprint placementBlueprint; private SelectionBlueprint<HitObject> placementBlueprint;
private SelectableAreaBackground backgroundBox; private SelectableAreaBackground backgroundBox;
@ -76,7 +73,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
base.LoadComplete(); base.LoadComplete();
DragBox.Alpha = 0; DragBox.Alpha = 0;
placement = beatmap.PlacementObject.GetBoundCopy(); placement = Beatmap.PlacementObject.GetBoundCopy();
placement.ValueChanged += placementChanged; 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) 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. // remove objects from the stack as long as their end time is in the past.
while (currentConcurrentObjects.TryPeek(out HitObject hitObject)) 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; break;
currentConcurrentObjects.Pop(); 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. // 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. // 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) if (currentConcurrentObjects.Count >= stack_reset_count)
currentConcurrentObjects.Clear(); currentConcurrentObjects.Clear();
@ -176,15 +173,15 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
b.Y = -(stack_offset * currentConcurrentObjects.Count); 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 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 // 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) 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() 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 namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{ {
public class TimelineHitObjectBlueprint : SelectionBlueprint public class TimelineHitObjectBlueprint : SelectionBlueprint<HitObject>
{ {
private const float circle_size = 38; private const float circle_size = 38;
@ -49,13 +49,13 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
[Resolved] [Resolved]
private ISkinSource skin { get; set; } private ISkinSource skin { get; set; }
public TimelineHitObjectBlueprint(HitObject hitObject) public TimelineHitObjectBlueprint(HitObject item)
: base(hitObject) : base(item)
{ {
Anchor = Anchor.CentreLeft; Anchor = Anchor.CentreLeft;
Origin = Anchor.CentreLeft; Origin = Anchor.CentreLeft;
startTime = hitObject.StartTimeBindable.GetBoundCopy(); startTime = item.StartTimeBindable.GetBoundCopy();
startTime.BindValueChanged(time => X = (float)time.NewValue, true); startTime.BindValueChanged(time => X = (float)time.NewValue, true);
RelativePositionAxes = Axes.X; 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) OnDragHandled = e => OnDragHandled?.Invoke(e)
}); });
@ -108,7 +108,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{ {
base.LoadComplete(); base.LoadComplete();
if (HitObject is IHasComboInformation comboInfo) if (Item is IHasComboInformation comboInfo)
{ {
indexInCurrentComboBindable = comboInfo.IndexInCurrentComboBindable.GetBoundCopy(); indexInCurrentComboBindable = comboInfo.IndexInCurrentComboBindable.GetBoundCopy();
indexInCurrentComboBindable.BindValueChanged(_ => updateComboIndex(), true); indexInCurrentComboBindable.BindValueChanged(_ => updateComboIndex(), true);
@ -136,7 +136,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
private void updateComboColour() private void updateComboColour()
{ {
if (!(HitObject is IHasComboInformation combo)) if (!(Item is IHasComboInformation combo))
return; return;
var comboColours = skin.GetConfig<GlobalSkinColours, IReadOnlyList<Color4>>(GlobalSkinColours.ComboColours)?.Value ?? Array.Empty<Color4>(); 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(); 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)); circle.Colour = ColourInfo.GradientHorizontal(comboColour, comboColour.Lighten(0.4f));
else else
circle.Colour = comboColour; circle.Colour = comboColour;
@ -166,14 +166,14 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
base.Update(); base.Update();
// no bindable so we perform this every 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) if (Width != duration)
{ {
Width = duration; Width = duration;
// kind of haphazard but yeah, no bindables. // kind of haphazard but yeah, no bindables.
if (HitObject is IHasRepeats repeats) if (Item is IHasRepeats repeats)
updateRepeats(repeats); updateRepeats(repeats);
} }
} }

View File

@ -301,13 +301,7 @@ namespace osu.Game.Screens.Edit
return list.Count - 1; return list.Count - 1;
} }
public double SnapTime(double time, double? referenceTime) public double SnapTime(double time, double? referenceTime) => ControlPointInfo.GetClosestSnappedTime(time, BeatDivisor, 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 GetBeatLengthAtTime(double referenceTime) => ControlPointInfo.TimingPointAt(referenceTime).BeatLength / BeatDivisor; public double GetBeatLengthAtTime(double referenceTime) => ControlPointInfo.TimingPointAt(referenceTime).BeatLength / BeatDivisor;

View File

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

View File

@ -19,7 +19,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="DiffPlex" Version="1.7.0" /> <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="MessagePack" Version="2.2.85" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="5.0.5" /> <PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="5.0.5" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" 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. --> <!-- Xamarin.iOS does not automatically handle transitive dependencies from NuGet packages. -->
<ItemGroup Label="Transitive Dependencies"> <ItemGroup Label="Transitive Dependencies">
<PackageReference Include="DiffPlex" Version="1.6.3" /> <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" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />