diff --git a/osu.Android.props b/osu.Android.props index e5a1ec2f4e..25bde037db 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -54,6 +54,6 @@ - + diff --git a/osu.Game.Rulesets.Catch/Objects/BananaShower.cs b/osu.Game.Rulesets.Catch/Objects/BananaShower.cs index 267e6d12c7..0c754412e5 100644 --- a/osu.Game.Rulesets.Catch/Objects/BananaShower.cs +++ b/osu.Game.Rulesets.Catch/Objects/BananaShower.cs @@ -36,7 +36,11 @@ namespace osu.Game.Rulesets.Catch.Objects } } - public double EndTime => StartTime + Duration; + public double EndTime + { + get => StartTime + Duration; + set => Duration = value - StartTime; + } public double Duration { get; set; } } diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs index a4ed966abb..b014b32305 100644 --- a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs +++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs @@ -110,7 +110,11 @@ namespace osu.Game.Rulesets.Catch.Objects } } - public double EndTime => StartTime + this.SpanCount() * Path.Distance / Velocity; + public double EndTime + { + get => StartTime + this.SpanCount() * Path.Distance / Velocity; + set => throw new System.NotSupportedException($"Adjust via {nameof(RepeatCount)} instead"); // can be implemented if/when needed. + } public float EndX => X + this.CurvePositionAt(1).X / CatchPlayfield.BASE_WIDTH; diff --git a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs index bdba813eed..86d3d2b2b0 100644 --- a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs @@ -15,7 +15,11 @@ namespace osu.Game.Rulesets.Mania.Objects /// public class HoldNote : ManiaHitObject, IHasEndTime { - public double EndTime => StartTime + Duration; + public double EndTime + { + get => StartTime + Duration; + set => Duration = value - StartTime; + } private double duration; diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index fe65ab78d1..95fb6d9d48 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -18,7 +18,12 @@ namespace osu.Game.Rulesets.Osu.Objects { public class Slider : OsuHitObject, IHasCurve { - public double EndTime => StartTime + this.SpanCount() * Path.Distance / Velocity; + public double EndTime + { + get => StartTime + this.SpanCount() * Path.Distance / Velocity; + set => throw new System.NotSupportedException($"Adjust via {nameof(RepeatCount)} instead"); // can be implemented if/when needed. + } + public double Duration => EndTime - StartTime; private readonly Cached endPositionCache = new Cached(); @@ -81,7 +86,7 @@ namespace osu.Game.Rulesets.Osu.Objects set { repeatCount = value; - endPositionCache.Invalidate(); + updateNestedPositions(); } } diff --git a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs index 8956ca9c19..aacd78f176 100644 --- a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs @@ -18,7 +18,11 @@ namespace osu.Game.Rulesets.Taiko.Objects /// private const float base_distance = 100; - public double EndTime => StartTime + Duration; + public double EndTime + { + get => StartTime + Duration; + set => Duration = value - StartTime; + } public double Duration { get; set; } diff --git a/osu.Game.Rulesets.Taiko/Objects/Swell.cs b/osu.Game.Rulesets.Taiko/Objects/Swell.cs index e60984596d..2f06066a16 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Swell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Swell.cs @@ -11,7 +11,11 @@ namespace osu.Game.Rulesets.Taiko.Objects { public class Swell : TaikoHitObject, IHasEndTime { - public double EndTime => StartTime + Duration; + public double EndTime + { + get => StartTime + Duration; + set => Duration = value - StartTime; + } public double Duration { get; set; } diff --git a/osu.Game.Tests/Editor/TestSceneHitObjectComposerDistanceSnapping.cs b/osu.Game.Tests/Editor/TestSceneHitObjectComposerDistanceSnapping.cs index 5a4e76d586..3cb5909ba9 100644 --- a/osu.Game.Tests/Editor/TestSceneHitObjectComposerDistanceSnapping.cs +++ b/osu.Game.Tests/Editor/TestSceneHitObjectComposerDistanceSnapping.cs @@ -3,6 +3,8 @@ using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Testing; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Edit; @@ -23,15 +25,31 @@ namespace osu.Game.Tests.Editor [Cached(typeof(IBeatSnapProvider))] private readonly EditorBeatmap editorBeatmap; + protected override Container Content { get; } + public TestSceneHitObjectComposerDistanceSnapping() { - editorBeatmap = new EditorBeatmap(new OsuBeatmap(), BeatDivisor); + base.Content.Add(new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + editorBeatmap = new EditorBeatmap(new OsuBeatmap()), + Content = new Container + { + RelativeSizeAxes = Axes.Both, + } + }, + }); } [SetUp] public void Setup() => Schedule(() => { - Child = composer = new TestHitObjectComposer(); + Children = new Drawable[] + { + composer = new TestHitObjectComposer() + }; BeatDivisor.Value = 1; diff --git a/osu.Game.Tests/ScrollAlgorithms/ConstantScrollTest.cs b/osu.Game.Tests/ScrollAlgorithms/ConstantScrollTest.cs index d7f709dc03..a6e8622b6f 100644 --- a/osu.Game.Tests/ScrollAlgorithms/ConstantScrollTest.cs +++ b/osu.Game.Tests/ScrollAlgorithms/ConstantScrollTest.cs @@ -18,12 +18,21 @@ namespace osu.Game.Tests.ScrollAlgorithms } [Test] - public void TestDisplayStartTime() + public void TestPointDisplayStartTime() { - Assert.AreEqual(-8000, algorithm.GetDisplayStartTime(2000, 10000)); - Assert.AreEqual(-3000, algorithm.GetDisplayStartTime(2000, 5000)); - Assert.AreEqual(2000, algorithm.GetDisplayStartTime(7000, 5000)); - Assert.AreEqual(7000, algorithm.GetDisplayStartTime(17000, 10000)); + Assert.AreEqual(-8000, algorithm.GetDisplayStartTime(2000, 0, 10000, 1)); + Assert.AreEqual(-3000, algorithm.GetDisplayStartTime(2000, 0, 5000, 1)); + Assert.AreEqual(2000, algorithm.GetDisplayStartTime(7000, 0, 5000, 1)); + Assert.AreEqual(7000, algorithm.GetDisplayStartTime(17000, 0, 10000, 1)); + } + + [Test] + public void TestObjectDisplayStartTime() + { + Assert.AreEqual(900, algorithm.GetDisplayStartTime(2000, 50, 1000, 500)); // 2000 - (1 + 50 / 500) * 1000 + Assert.AreEqual(8900, algorithm.GetDisplayStartTime(10000, 50, 1000, 500)); // 10000 - (1 + 50 / 500) * 1000 + Assert.AreEqual(13500, algorithm.GetDisplayStartTime(15000, 250, 1000, 500)); // 15000 - (1 + 250 / 500) * 1000 + Assert.AreEqual(19000, algorithm.GetDisplayStartTime(25000, 100, 5000, 500)); // 25000 - (1 + 100 / 500) * 5000 } [Test] diff --git a/osu.Game.Tests/ScrollAlgorithms/OverlappingScrollTest.cs b/osu.Game.Tests/ScrollAlgorithms/OverlappingScrollTest.cs index 106aa88be3..1429d22c1a 100644 --- a/osu.Game.Tests/ScrollAlgorithms/OverlappingScrollTest.cs +++ b/osu.Game.Tests/ScrollAlgorithms/OverlappingScrollTest.cs @@ -27,11 +27,22 @@ namespace osu.Game.Tests.ScrollAlgorithms } [Test] - public void TestDisplayStartTime() + public void TestPointDisplayStartTime() { - Assert.AreEqual(1000, algorithm.GetDisplayStartTime(2000, 1000)); // Like constant - Assert.AreEqual(10000, algorithm.GetDisplayStartTime(10500, 1000)); // 10500 - (1000 * 0.5) - Assert.AreEqual(20000, algorithm.GetDisplayStartTime(22000, 1000)); // 23000 - (1000 / 0.5) + Assert.AreEqual(1000, algorithm.GetDisplayStartTime(2000, 0, 1000, 1)); // Like constant + Assert.AreEqual(10000, algorithm.GetDisplayStartTime(10500, 0, 1000, 1)); // 10500 - (1000 * 0.5) + Assert.AreEqual(20000, algorithm.GetDisplayStartTime(22000, 0, 1000, 1)); // 23000 - (1000 / 0.5) + } + + [Test] + public void TestObjectDisplayStartTime() + { + Assert.AreEqual(900, algorithm.GetDisplayStartTime(2000, 50, 1000, 500)); // 2000 - (1 + 50 / 500) * 1000 / 1 + Assert.AreEqual(9450, algorithm.GetDisplayStartTime(10000, 50, 1000, 500)); // 10000 - (1 + 50 / 500) * 1000 / 2 + Assert.AreEqual(14250, algorithm.GetDisplayStartTime(15000, 250, 1000, 500)); // 15000 - (1 + 250 / 500) * 1000 / 2 + Assert.AreEqual(16500, algorithm.GetDisplayStartTime(18000, 250, 2000, 500)); // 18000 - (1 + 250 / 500) * 2000 / 2 + Assert.AreEqual(17800, algorithm.GetDisplayStartTime(20000, 50, 1000, 500)); // 20000 - (1 + 50 / 500) * 1000 / 0.5 + Assert.AreEqual(19800, algorithm.GetDisplayStartTime(22000, 50, 1000, 500)); // 22000 - (1 + 50 / 500) * 1000 / 0.5 } [Test] diff --git a/osu.Game.Tests/Visual/Editor/TestSceneTimelineBlueprintContainer.cs b/osu.Game.Tests/Visual/Editor/TestSceneTimelineBlueprintContainer.cs index 3c75fd5310..4d8f877575 100644 --- a/osu.Game.Tests/Visual/Editor/TestSceneTimelineBlueprintContainer.cs +++ b/osu.Game.Tests/Visual/Editor/TestSceneTimelineBlueprintContainer.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using System.Collections.Generic; using NUnit.Framework; using osu.Framework.Graphics; using osu.Game.Screens.Edit.Compose.Components.Timeline; @@ -10,6 +12,17 @@ namespace osu.Game.Tests.Visual.Editor [TestFixture] public class TestSceneTimelineBlueprintContainer : TimelineTestScene { + public override IReadOnlyList RequiredTypes => new[] + { + typeof(TimelineHitObjectBlueprint), + }; + public override Drawable CreateTestComponent() => new TimelineBlueprintContainer(); + + protected override void LoadComplete() + { + base.LoadComplete(); + Clock.Seek(10000); + } } } diff --git a/osu.Game.Tests/Visual/Editor/TestSceneTimingScreen.cs b/osu.Game.Tests/Visual/Editor/TestSceneTimingScreen.cs index adfed9a299..ae09a7fa47 100644 --- a/osu.Game.Tests/Visual/Editor/TestSceneTimingScreen.cs +++ b/osu.Game.Tests/Visual/Editor/TestSceneTimingScreen.cs @@ -27,7 +27,12 @@ namespace osu.Game.Tests.Visual.Editor }; [Cached(typeof(EditorBeatmap))] - private readonly EditorBeatmap editorBeatmap = new EditorBeatmap(new OsuBeatmap()); + private readonly EditorBeatmap editorBeatmap; + + public TestSceneTimingScreen() + { + editorBeatmap = new EditorBeatmap(new OsuBeatmap()); + } [BackgroundDependencyLoader] private void load() diff --git a/osu.Game.Tests/Visual/Editor/TimelineTestScene.cs b/osu.Game.Tests/Visual/Editor/TimelineTestScene.cs index b5e526d3c2..7081eb3af5 100644 --- a/osu.Game.Tests/Visual/Editor/TimelineTestScene.cs +++ b/osu.Game.Tests/Visual/Editor/TimelineTestScene.cs @@ -13,7 +13,6 @@ using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Edit; -using osu.Game.Rulesets.Objects; using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Compose.Components.Timeline; using osuTK; @@ -38,13 +37,16 @@ namespace osu.Game.Tests.Visual.Editor { Beatmap.Value = new WaveformTestBeatmap(audio); - var editorBeatmap = new EditorBeatmap((Beatmap)Beatmap.Value.Beatmap, BeatDivisor); + var playable = Beatmap.Value.GetPlayableBeatmap(Beatmap.Value.BeatmapInfo.Ruleset); + + var editorBeatmap = new EditorBeatmap(playable); Dependencies.Cache(editorBeatmap); Dependencies.CacheAs(editorBeatmap); AddRange(new Drawable[] { + editorBeatmap, new FillFlowContainer { AutoSizeAxes = Axes.Both, diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs index 8629522dc2..d03716db2e 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Threading; +using osu.Framework.Utils; using osu.Game.Configuration; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; @@ -19,6 +20,7 @@ using osu.Game.Rulesets.Timing; using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; using osuTK; +using osuTK.Graphics; namespace osu.Game.Tests.Visual.Gameplay { @@ -30,7 +32,8 @@ namespace osu.Game.Tests.Visual.Gameplay [Cached(typeof(IReadOnlyList))] private IReadOnlyList mods { get; set; } = Array.Empty(); - private const int spawn_interval = 5000; + private const int time_range = 5000; + private const int spawn_rate = time_range / 10; private readonly ScrollingTestContainer[] scrollContainers = new ScrollingTestContainer[4]; private readonly TestPlayfield[] playfields = new TestPlayfield[4]; @@ -50,13 +53,13 @@ namespace osu.Game.Tests.Visual.Gameplay { RelativeSizeAxes = Axes.Both, Child = playfields[0] = new TestPlayfield(), - TimeRange = spawn_interval + TimeRange = time_range }, scrollContainers[1] = new ScrollingTestContainer(ScrollingDirection.Down) { RelativeSizeAxes = Axes.Both, Child = playfields[1] = new TestPlayfield(), - TimeRange = spawn_interval + TimeRange = time_range }, }, new Drawable[] @@ -65,13 +68,13 @@ namespace osu.Game.Tests.Visual.Gameplay { RelativeSizeAxes = Axes.Both, Child = playfields[2] = new TestPlayfield(), - TimeRange = spawn_interval + TimeRange = time_range }, scrollContainers[3] = new ScrollingTestContainer(ScrollingDirection.Right) { RelativeSizeAxes = Axes.Both, Child = playfields[3] = new TestPlayfield(), - TimeRange = spawn_interval + TimeRange = time_range } } } @@ -84,31 +87,55 @@ namespace osu.Game.Tests.Visual.Gameplay { scrollContainers.ForEach(c => c.ControlPoints.Add(new MultiplierControlPoint(0))); - for (int i = 0; i <= spawn_interval; i += 1000) + for (int i = spawn_rate / 2; i <= time_range; i += spawn_rate) addHitObject(Time.Current + i); hitObjectSpawnDelegate?.Cancel(); - hitObjectSpawnDelegate = Scheduler.AddDelayed(() => addHitObject(Time.Current + spawn_interval), 1000, true); + hitObjectSpawnDelegate = Scheduler.AddDelayed(() => addHitObject(Time.Current + time_range), spawn_rate, true); } + private IList testControlPoints => new List + { + new MultiplierControlPoint(time_range) { DifficultyPoint = { SpeedMultiplier = 1.25 } }, + new MultiplierControlPoint(1.5 * time_range) { DifficultyPoint = { SpeedMultiplier = 1 } }, + new MultiplierControlPoint(2 * time_range) { DifficultyPoint = { SpeedMultiplier = 1.5 } } + }; + [Test] public void TestScrollAlgorithms() { - AddStep("Constant scroll", () => setScrollAlgorithm(ScrollVisualisationMethod.Constant)); - AddStep("Overlapping scroll", () => setScrollAlgorithm(ScrollVisualisationMethod.Overlapping)); - AddStep("Sequential scroll", () => setScrollAlgorithm(ScrollVisualisationMethod.Sequential)); + AddStep("constant scroll", () => setScrollAlgorithm(ScrollVisualisationMethod.Constant)); + AddStep("overlapping scroll", () => setScrollAlgorithm(ScrollVisualisationMethod.Overlapping)); + AddStep("sequential scroll", () => setScrollAlgorithm(ScrollVisualisationMethod.Sequential)); - AddSliderStep("Time range", 100, 10000, spawn_interval, v => scrollContainers.Where(c => c != null).ForEach(c => c.TimeRange = v)); - AddStep("Add control point", () => addControlPoint(Time.Current + spawn_interval)); + AddSliderStep("time range", 100, 10000, time_range, v => scrollContainers.Where(c => c != null).ForEach(c => c.TimeRange = v)); + + AddStep("add control points", () => addControlPoints(testControlPoints, Time.Current)); } [Test] - public void TestScrollLifetime() + public void TestConstantScrollLifetime() { - AddStep("Set constant scroll", () => setScrollAlgorithm(ScrollVisualisationMethod.Constant)); + AddStep("set constant scroll", () => setScrollAlgorithm(ScrollVisualisationMethod.Constant)); // scroll container time range must be less than the rate of spawning hitobjects // otherwise the hitobjects will spawn already partly visible on screen and look wrong - AddStep("Set time range", () => scrollContainers.ForEach(c => c.TimeRange = spawn_interval / 2.0)); + AddStep("set time range", () => scrollContainers.ForEach(c => c.TimeRange = time_range / 2.0)); + } + + [Test] + public void TestSequentialScrollLifetime() + { + AddStep("set sequential scroll", () => setScrollAlgorithm(ScrollVisualisationMethod.Sequential)); + AddStep("set time range", () => scrollContainers.ForEach(c => c.TimeRange = time_range / 2.0)); + AddStep("add control points", () => addControlPoints(testControlPoints, Time.Current)); + } + + [Test] + public void TestOverlappingScrollLifetime() + { + AddStep("set overlapping scroll", () => setScrollAlgorithm(ScrollVisualisationMethod.Overlapping)); + AddStep("set time range", () => scrollContainers.ForEach(c => c.TimeRange = time_range / 2.0)); + AddStep("add control points", () => addControlPoints(testControlPoints, Time.Current)); } private void addHitObject(double time) @@ -122,28 +149,27 @@ namespace osu.Game.Tests.Visual.Gameplay }); } - private void addControlPoint(double time) + private TestDrawableControlPoint createDrawablePoint(TestPlayfield playfield, double t) { - scrollContainers.ForEach(c => + var obj = new TestDrawableControlPoint(playfield.Direction, t); + setAnchor(obj, playfield); + return obj; + } + + private void addControlPoints(IList controlPoints, double sequenceStartTime) + { + controlPoints.ForEach(point => point.StartTime += sequenceStartTime); + + scrollContainers.ForEach(container => { - c.ControlPoints.Add(new MultiplierControlPoint(time) { DifficultyPoint = { SpeedMultiplier = 3 } }); - c.ControlPoints.Add(new MultiplierControlPoint(time + 2000) { DifficultyPoint = { SpeedMultiplier = 2 } }); - c.ControlPoints.Add(new MultiplierControlPoint(time + 3000) { DifficultyPoint = { SpeedMultiplier = 1 } }); + container.ControlPoints.AddRange(controlPoints); }); - playfields.ForEach(p => + foreach (var playfield in playfields) { - TestDrawableControlPoint createDrawablePoint(double t) - { - var obj = new TestDrawableControlPoint(p.Direction, t); - setAnchor(obj, p); - return obj; - } - - p.Add(createDrawablePoint(time)); - p.Add(createDrawablePoint(time + 2000)); - p.Add(createDrawablePoint(time + 3000)); - }); + foreach (var controlPoint in controlPoints) + playfield.Add(createDrawablePoint(playfield, controlPoint.StartTime)); + } } private void setAnchor(DrawableHitObject obj, TestPlayfield playfield) @@ -236,7 +262,11 @@ namespace osu.Game.Tests.Visual.Gameplay AutoSizeAxes = Axes.Both; - AddInternal(new Box { Size = new Vector2(75) }); + AddInternal(new Box + { + Size = new Vector2(75), + Colour = new Color4(RNG.NextSingle(), RNG.NextSingle(), RNG.NextSingle(), 1) + }); } } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs index eb4dc909df..fe14a1ff0a 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs @@ -4,11 +4,16 @@ using System; using System.Collections.Generic; using System.Linq; +using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Online.Multiplayer; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Catch; +using osu.Game.Rulesets.Osu; using osu.Game.Screens.Multi; using osu.Game.Screens.Multi.Lounge.Components; using osu.Game.Users; @@ -27,11 +32,11 @@ namespace osu.Game.Tests.Visual.Multiplayer [Cached(Type = typeof(IRoomManager))] private TestRoomManager roomManager = new TestRoomManager(); + private RoomsContainer container; + [BackgroundDependencyLoader] private void load() { - RoomsContainer container; - Child = container = new RoomsContainer { Anchor = Anchor.Centre, @@ -39,24 +44,21 @@ namespace osu.Game.Tests.Visual.Multiplayer Width = 0.5f, JoinRequested = joinRequested }; + } + + public override void SetUpSteps() + { + base.SetUpSteps(); AddStep("clear rooms", () => roomManager.Rooms.Clear()); + } - AddStep("add rooms", () => - { - for (int i = 0; i < 3; i++) - { - roomManager.Rooms.Add(new Room - { - RoomID = { Value = i }, - Name = { Value = $"Room {i}" }, - Host = { Value = new User { Username = "Host" } }, - EndDate = { Value = DateTimeOffset.Now + TimeSpan.FromSeconds(10) } - }); - } - }); + [Test] + public void TestBasicListChanges() + { + addRooms(3); - AddAssert("has 2 rooms", () => container.Rooms.Count == 3); + AddAssert("has 3 rooms", () => container.Rooms.Count == 3); AddStep("remove first room", () => roomManager.Rooms.Remove(roomManager.Rooms.FirstOrDefault())); AddAssert("has 2 rooms", () => container.Rooms.Count == 2); AddAssert("first room removed", () => container.Rooms.All(r => r.Room.RoomID.Value != 0)); @@ -68,6 +70,70 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("first room joined", () => roomManager.Rooms.First().Status.Value is JoinedRoomStatus); } + [Test] + public void TestStringFiltering() + { + addRooms(4); + + AddUntilStep("4 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 4); + + AddStep("filter one room", () => container.Filter(new FilterCriteria { SearchString = "1" })); + + AddUntilStep("1 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 1); + + AddStep("remove filter", () => container.Filter(null)); + + AddUntilStep("4 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 4); + } + + [Test] + public void TestRulesetFiltering() + { + addRooms(2, new OsuRuleset().RulesetInfo); + addRooms(3, new CatchRuleset().RulesetInfo); + + AddUntilStep("5 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 5); + + AddStep("filter osu! rooms", () => container.Filter(new FilterCriteria { Ruleset = new OsuRuleset().RulesetInfo })); + + AddUntilStep("2 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 2); + + AddStep("filter catch rooms", () => container.Filter(new FilterCriteria { Ruleset = new CatchRuleset().RulesetInfo })); + + AddUntilStep("3 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 3); + } + + private void addRooms(int count, RulesetInfo ruleset = null) + { + AddStep("add rooms", () => + { + for (int i = 0; i < count; i++) + { + var room = new Room + { + RoomID = { Value = i }, + Name = { Value = $"Room {i}" }, + Host = { Value = new User { Username = "Host" } }, + EndDate = { Value = DateTimeOffset.Now + TimeSpan.FromSeconds(10) } + }; + + if (ruleset != null) + { + room.Playlist.Add(new PlaylistItem + { + Ruleset = ruleset, + Beatmap = new BeatmapInfo + { + Metadata = new BeatmapMetadata() + } + }); + } + + roomManager.Rooms.Add(room); + } + }); + } + private void joinRequested(Room room) => room.Status.Value = new JoinedRoomStatus(); private class TestRoomManager : IRoomManager diff --git a/osu.Game.Tests/Visual/Online/TestSceneCommentsContainer.cs b/osu.Game.Tests/Visual/Online/TestSceneCommentsContainer.cs index 9c526c4f81..2a43ba3f99 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneCommentsContainer.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneCommentsContainer.cs @@ -10,6 +10,8 @@ using osu.Framework.Graphics; using osu.Game.Overlays.Comments; using osu.Game.Overlays; using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Game.Users; namespace osu.Game.Tests.Visual.Online { @@ -37,23 +39,29 @@ namespace osu.Game.Tests.Visual.Online public TestSceneCommentsContainer() { BasicScrollContainer scroll; - CommentsContainer comments; + TestCommentsContainer comments; Add(scroll = new BasicScrollContainer { RelativeSizeAxes = Axes.Both, - Child = comments = new CommentsContainer() + Child = comments = new TestCommentsContainer() }); AddStep("Big Black comments", () => comments.ShowComments(CommentableType.Beatmapset, 41823)); AddStep("Airman comments", () => comments.ShowComments(CommentableType.Beatmapset, 24313)); AddStep("Lazer build comments", () => comments.ShowComments(CommentableType.Build, 4772)); AddStep("News comments", () => comments.ShowComments(CommentableType.NewsPost, 715)); + AddStep("Trigger user change", comments.User.TriggerChange); AddStep("Idle state", () => { scroll.Clear(); - scroll.Add(comments = new CommentsContainer()); + scroll.Add(comments = new TestCommentsContainer()); }); } + + private class TestCommentsContainer : CommentsContainer + { + public new Bindable User => base.User; + } } } diff --git a/osu.Game.Tests/Visual/UserInterface/TestScenePlaylistOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestScenePlaylistOverlay.cs new file mode 100644 index 0000000000..7476b52b49 --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestScenePlaylistOverlay.cs @@ -0,0 +1,66 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using NUnit.Framework; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Utils; +using osu.Game.Beatmaps; +using osu.Game.Overlays.Music; +using osuTK; + +namespace osu.Game.Tests.Visual.UserInterface +{ + public class TestScenePlaylistOverlay : OsuTestScene + { + public override IReadOnlyList RequiredTypes => new[] + { + typeof(PlaylistOverlay), + typeof(Playlist) + }; + + private readonly BindableList beatmapSets = new BindableList(); + + [SetUp] + public void Setup() => Schedule(() => + { + PlaylistOverlay overlay; + + Child = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(300, 500), + Child = overlay = new PlaylistOverlay + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.X, + State = { Value = Visibility.Visible } + } + }; + + beatmapSets.Clear(); + + for (int i = 0; i < 100; i++) + { + beatmapSets.Add(new BeatmapSetInfo + { + Metadata = new BeatmapMetadata + { + // Create random metadata, then we can check if sorting works based on these + Artist = "Some Artist " + RNG.Next(0, 9), + Title = $"Some Song {i + 1}", + AuthorString = "Some Guy " + RNG.Next(0, 9), + }, + DateAdded = DateTimeOffset.UtcNow, + }); + } + + overlay.BeatmapSets.BindTo(beatmapSets); + }); + } +} diff --git a/osu.Game.Tests/WaveformTestBeatmap.cs b/osu.Game.Tests/WaveformTestBeatmap.cs index b7d7bb1ee1..df6394ed34 100644 --- a/osu.Game.Tests/WaveformTestBeatmap.cs +++ b/osu.Game.Tests/WaveformTestBeatmap.cs @@ -11,6 +11,8 @@ using osu.Game.Beatmaps; using osu.Game.Beatmaps.Formats; using osu.Game.IO; using osu.Game.IO.Archives; +using osu.Game.Rulesets.Catch; +using osu.Game.Tests.Beatmaps; using osu.Game.Tests.Resources; namespace osu.Game.Tests @@ -20,11 +22,18 @@ namespace osu.Game.Tests /// public class WaveformTestBeatmap : WorkingBeatmap { + private readonly Beatmap beatmap; private readonly ITrackStore trackStore; public WaveformTestBeatmap(AudioManager audioManager) - : base(new BeatmapInfo(), audioManager) + : this(audioManager, new WaveformBeatmap()) { + } + + public WaveformTestBeatmap(AudioManager audioManager, Beatmap beatmap) + : base(beatmap.BeatmapInfo, audioManager) + { + this.beatmap = beatmap; trackStore = audioManager.GetTrackStore(getZipReader()); } @@ -34,11 +43,11 @@ namespace osu.Game.Tests trackStore?.Dispose(); } - private Stream getStream() => TestResources.GetTestBeatmapStream(); + private static Stream getStream() => TestResources.GetTestBeatmapStream(); - private ZipArchiveReader getZipReader() => new ZipArchiveReader(getStream()); + private static ZipArchiveReader getZipReader() => new ZipArchiveReader(getStream()); - protected override IBeatmap GetBeatmap() => createTestBeatmap(); + protected override IBeatmap GetBeatmap() => beatmap; protected override Texture GetBackground() => null; @@ -57,10 +66,16 @@ namespace osu.Game.Tests } } - private Beatmap createTestBeatmap() + private class WaveformBeatmap : TestBeatmap { - using (var reader = getZipReader()) + public WaveformBeatmap() + : base(new CatchRuleset().RulesetInfo) { + } + + protected override Beatmap CreateBeatmap() + { + using (var reader = getZipReader()) using (var beatmapStream = reader.GetStream(reader.Filenames.First(f => f.EndsWith(".osu")))) using (var beatmapReader = new LineBufferedReader(beatmapStream)) return Decoder.GetDecoder(beatmapReader).Decode(beatmapReader); diff --git a/osu.Game/Configuration/SettingSourceAttribute.cs b/osu.Game/Configuration/SettingSourceAttribute.cs index f859dccc80..a3788e4582 100644 --- a/osu.Game/Configuration/SettingSourceAttribute.cs +++ b/osu.Game/Configuration/SettingSourceAttribute.cs @@ -45,7 +45,8 @@ namespace osu.Game.Configuration yield return new SettingsSlider { LabelText = attr.Label, - Bindable = bNumber + Bindable = bNumber, + KeyboardStep = 0.1f, }; break; @@ -54,7 +55,8 @@ namespace osu.Game.Configuration yield return new SettingsSlider { LabelText = attr.Label, - Bindable = bNumber + Bindable = bNumber, + KeyboardStep = 0.1f, }; break; diff --git a/osu.Game/Overlays/Comments/CommentsContainer.cs b/osu.Game/Overlays/Comments/CommentsContainer.cs index 8761b88b1e..33a6be0d7a 100644 --- a/osu.Game/Overlays/Comments/CommentsContainer.cs +++ b/osu.Game/Overlays/Comments/CommentsContainer.cs @@ -12,6 +12,7 @@ using osu.Game.Online.API.Requests.Responses; using System.Threading; using System.Linq; using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Game.Users; namespace osu.Game.Overlays.Comments { @@ -23,6 +24,8 @@ namespace osu.Game.Overlays.Comments public readonly Bindable Sort = new Bindable(); public readonly BindableBool ShowDeleted = new BindableBool(); + protected readonly Bindable User = new Bindable(); + [Resolved] private IAPIProvider api { get; set; } @@ -109,10 +112,13 @@ namespace osu.Game.Overlays.Comments } } }); + + User.BindTo(api.LocalUser); } protected override void LoadComplete() { + User.BindValueChanged(_ => refetchComments()); Sort.BindValueChanged(_ => refetchComments(), true); base.LoadComplete(); } @@ -148,7 +154,7 @@ namespace osu.Game.Overlays.Comments loadCancellation?.Cancel(); request = new GetCommentsRequest(type, id.Value, Sort.Value, currentPage++); request.Success += onSuccess; - api.Queue(request); + api.PerformAsync(request); } private void clearComments() diff --git a/osu.Game/Overlays/Music/Playlist.cs b/osu.Game/Overlays/Music/Playlist.cs new file mode 100644 index 0000000000..1ba568443d --- /dev/null +++ b/osu.Game/Overlays/Music/Playlist.cs @@ -0,0 +1,52 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Linq; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Beatmaps; +using osu.Game.Graphics.Containers; +using osuTK; + +namespace osu.Game.Overlays.Music +{ + public class Playlist : RearrangeableListContainer + { + public Action RequestSelection; + + public readonly Bindable SelectedSet = new Bindable(); + + /// + /// Whether any item is currently being dragged. Used to hide other items' drag handles. + /// + private readonly BindableBool playlistDragActive = new BindableBool(); + + public new MarginPadding Padding + { + get => base.Padding; + set => base.Padding = value; + } + + public void Filter(string searchTerm) => ((SearchContainer>)ListContainer).SearchTerm = searchTerm; + + public BeatmapSetInfo FirstVisibleSet => Items.FirstOrDefault(i => ((PlaylistItem)ItemMap[i]).MatchingFilter); + + protected override RearrangeableListItem CreateDrawable(BeatmapSetInfo item) => new PlaylistItem(item) + { + SelectedSet = { BindTarget = SelectedSet }, + PlaylistDragActive = { BindTarget = playlistDragActive }, + RequestSelection = set => RequestSelection?.Invoke(set) + }; + + protected override ScrollContainer CreateScrollContainer() => new OsuScrollContainer(); + + protected override FillFlowContainer> CreateListFillFlowContainer() => new SearchContainer> + { + Spacing = new Vector2(0, 3), + LayoutDuration = 200, + LayoutEasing = Easing.OutQuint, + }; + } +} diff --git a/osu.Game/Overlays/Music/PlaylistItem.cs b/osu.Game/Overlays/Music/PlaylistItem.cs index d40f391982..0569261867 100644 --- a/osu.Game/Overlays/Music/PlaylistItem.cs +++ b/osu.Game/Overlays/Music/PlaylistItem.cs @@ -4,8 +4,8 @@ using System; using System.Collections.Generic; using System.Linq; -using osuTK.Graphics; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; @@ -15,64 +15,38 @@ using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osuTK; +using osuTK.Graphics; namespace osu.Game.Overlays.Music { - public class PlaylistItem : Container, IFilterable, IDraggable + public class PlaylistItem : RearrangeableListItem, IFilterable { private const float fade_duration = 100; - private Color4 hoverColour; - private Color4 artistColour; + public BindableBool PlaylistDragActive = new BindableBool(); - private SpriteIcon handle; + public readonly Bindable SelectedSet = new Bindable(); + + public Action RequestSelection; + + private PlaylistItemHandle handle; private TextFlowContainer text; private IEnumerable titleSprites; private ILocalisedBindableString titleBind; private ILocalisedBindableString artistBind; - public readonly BeatmapSetInfo BeatmapSetInfo; + private Color4 hoverColour; + private Color4 artistColour; - public Action OnSelect; - - public bool IsDraggable { get; private set; } - - protected override bool OnMouseDown(MouseDownEvent e) + public PlaylistItem(BeatmapSetInfo item) + : base(item) { - IsDraggable = handle.IsHovered; - return base.OnMouseDown(e); - } - - protected override void OnMouseUp(MouseUpEvent e) - { - IsDraggable = false; - base.OnMouseUp(e); - } - - private bool selected; - - public bool Selected - { - get => selected; - set - { - if (value == selected) return; - - selected = value; - - FinishTransforms(true); - foreach (Drawable s in titleSprites) - s.FadeColour(Selected ? hoverColour : Color4.White, fade_duration); - } - } - - public PlaylistItem(BeatmapSetInfo setInfo) - { - BeatmapSetInfo = setInfo; - RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; - Padding = new MarginPadding { Top = 3, Bottom = 3 }; + + Padding = new MarginPadding { Left = 5 }; + + FilterTerms = item.Metadata.SearchableTerms; } [BackgroundDependencyLoader] @@ -81,30 +55,55 @@ namespace osu.Game.Overlays.Music hoverColour = colours.Yellow; artistColour = colours.Gray9; - var metadata = BeatmapSetInfo.Metadata; - FilterTerms = metadata.SearchableTerms; - - Children = new Drawable[] + InternalChild = new GridContainer { - handle = new PlaylistItemHandle + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Content = new[] { - Colour = colours.Gray5 - }, - text = new OsuTextFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Left = 20 }, - ContentIndent = 10f, + new Drawable[] + { + handle = new PlaylistItemHandle + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Size = new Vector2(12), + Colour = colours.Gray5, + AlwaysPresent = true, + Alpha = 0 + }, + text = new OsuTextFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Left = 5 }, + }, + } }, + ColumnDimensions = new[] { new Dimension(GridSizeMode.AutoSize) }, + RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) } }; - titleBind = localisation.GetLocalisedString(new LocalisedString((metadata.TitleUnicode, metadata.Title))); - artistBind = localisation.GetLocalisedString(new LocalisedString((metadata.ArtistUnicode, metadata.Artist))); + titleBind = localisation.GetLocalisedString(new LocalisedString((Model.Metadata.TitleUnicode, Model.Metadata.Title))); + artistBind = localisation.GetLocalisedString(new LocalisedString((Model.Metadata.ArtistUnicode, Model.Metadata.Artist))); artistBind.BindValueChanged(_ => recreateText(), true); } + protected override void LoadComplete() + { + base.LoadComplete(); + + SelectedSet.BindValueChanged(set => + { + if (set.OldValue != Model && set.NewValue != Model) + return; + + foreach (Drawable s in titleSprites) + s.FadeColour(set.NewValue == Model ? hoverColour : Color4.White, fade_duration); + }, true); + } + private void recreateText() { text.Clear(); @@ -120,25 +119,38 @@ namespace osu.Game.Overlays.Music }); } - protected override bool OnHover(HoverEvent e) - { - handle.FadeIn(fade_duration); - - return base.OnHover(e); - } - - protected override void OnHoverLost(HoverLostEvent e) - { - handle.FadeOut(fade_duration); - } - protected override bool OnClick(ClickEvent e) { - OnSelect?.Invoke(BeatmapSetInfo); + RequestSelection?.Invoke(Model); return true; } - public IEnumerable FilterTerms { get; private set; } + protected override bool OnDragStart(DragStartEvent e) + { + if (!base.OnDragStart(e)) + return false; + + PlaylistDragActive.Value = true; + return true; + } + + protected override void OnDragEnd(DragEndEvent e) + { + PlaylistDragActive.Value = false; + base.OnDragEnd(e); + } + + protected override bool IsDraggableAt(Vector2 screenSpacePos) => handle.HandlingDrag; + + protected override bool OnHover(HoverEvent e) + { + handle.UpdateHoverState(IsDragged || !PlaylistDragActive.Value); + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) => handle.UpdateHoverState(false); + + public IEnumerable FilterTerms { get; } private bool matching = true; @@ -159,25 +171,41 @@ namespace osu.Game.Overlays.Music private class PlaylistItemHandle : SpriteIcon { + public bool HandlingDrag { get; private set; } + private bool isHovering; + public PlaylistItemHandle() { - Anchor = Anchor.CentreLeft; - Origin = Anchor.CentreLeft; - Size = new Vector2(12); Icon = FontAwesome.Solid.Bars; - Alpha = 0f; - Margin = new MarginPadding { Left = 5 }; } - public override bool HandlePositionalInput => IsPresent; + protected override bool OnMouseDown(MouseDownEvent e) + { + base.OnMouseDown(e); + + HandlingDrag = true; + UpdateHoverState(isHovering); + + return false; + } + + protected override void OnMouseUp(MouseUpEvent e) + { + base.OnMouseUp(e); + + HandlingDrag = false; + UpdateHoverState(isHovering); + } + + public void UpdateHoverState(bool hovering) + { + isHovering = hovering; + + if (isHovering || HandlingDrag) + this.FadeIn(fade_duration); + else + this.FadeOut(fade_duration); + } } } - - public interface IDraggable : IDrawable - { - /// - /// Whether this can be dragged in its current state. - /// - bool IsDraggable { get; } - } } diff --git a/osu.Game/Overlays/Music/PlaylistList.cs b/osu.Game/Overlays/Music/PlaylistList.cs deleted file mode 100644 index 7bdcab6dff..0000000000 --- a/osu.Game/Overlays/Music/PlaylistList.cs +++ /dev/null @@ -1,268 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using System.Collections.Generic; -using System.Linq; -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Extensions.IEnumerableExtensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Input.Events; -using osu.Game.Beatmaps; -using osu.Game.Graphics.Containers; -using osuTK; - -namespace osu.Game.Overlays.Music -{ - public class PlaylistList : CompositeDrawable - { - public Action Selected; - - private readonly ItemsScrollContainer items; - - public PlaylistList() - { - InternalChild = items = new ItemsScrollContainer - { - RelativeSizeAxes = Axes.Both, - Selected = set => Selected?.Invoke(set), - }; - } - - public new MarginPadding Padding - { - get => base.Padding; - set => base.Padding = value; - } - - public BeatmapSetInfo FirstVisibleSet => items.FirstVisibleSet; - - public void Filter(string searchTerm) => items.SearchTerm = searchTerm; - - private class ItemsScrollContainer : OsuScrollContainer - { - public Action Selected; - - private readonly SearchContainer search; - private readonly FillFlowContainer items; - - private readonly IBindable beatmapBacking = new Bindable(); - - private IBindableList beatmaps; - - [Resolved] - private MusicController musicController { get; set; } - - public ItemsScrollContainer() - { - Children = new Drawable[] - { - search = new SearchContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Children = new Drawable[] - { - items = new ItemSearchContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - }, - } - } - }; - } - - [BackgroundDependencyLoader] - private void load(IBindable beatmap) - { - beatmaps = musicController.BeatmapSets.GetBoundCopy(); - beatmaps.ItemsAdded += i => i.ForEach(addBeatmapSet); - beatmaps.ItemsRemoved += i => i.ForEach(removeBeatmapSet); - beatmaps.ForEach(addBeatmapSet); - - beatmapBacking.BindTo(beatmap); - beatmapBacking.ValueChanged += _ => Scheduler.AddOnce(updateSelectedSet); - } - - private void addBeatmapSet(BeatmapSetInfo obj) - { - if (obj == draggedItem?.BeatmapSetInfo) return; - - Schedule(() => items.Insert(items.Count - 1, new PlaylistItem(obj) { OnSelect = set => Selected?.Invoke(set) })); - } - - private void removeBeatmapSet(BeatmapSetInfo obj) - { - if (obj == draggedItem?.BeatmapSetInfo) return; - - Schedule(() => - { - var itemToRemove = items.FirstOrDefault(i => i.BeatmapSetInfo.ID == obj.ID); - if (itemToRemove != null) - items.Remove(itemToRemove); - }); - } - - private void updateSelectedSet() - { - foreach (PlaylistItem s in items.Children) - { - s.Selected = s.BeatmapSetInfo.ID == beatmapBacking.Value.BeatmapSetInfo?.ID; - if (s.Selected) - ScrollIntoView(s); - } - } - - public string SearchTerm - { - get => search.SearchTerm; - set => search.SearchTerm = value; - } - - public BeatmapSetInfo FirstVisibleSet => items.FirstOrDefault(i => i.MatchingFilter)?.BeatmapSetInfo; - - private Vector2 nativeDragPosition; - private PlaylistItem draggedItem; - - private int? dragDestination; - - protected override bool OnDragStart(DragStartEvent e) - { - nativeDragPosition = e.ScreenSpaceMousePosition; - draggedItem = items.FirstOrDefault(d => d.IsDraggable); - return draggedItem != null || base.OnDragStart(e); - } - - protected override void OnDrag(DragEvent e) - { - nativeDragPosition = e.ScreenSpaceMousePosition; - - if (draggedItem == null) - base.OnDrag(e); - } - - protected override void OnDragEnd(DragEndEvent e) - { - nativeDragPosition = e.ScreenSpaceMousePosition; - - if (draggedItem == null) - { - base.OnDragEnd(e); - return; - } - - if (dragDestination != null) - musicController.ChangeBeatmapSetPosition(draggedItem.BeatmapSetInfo, dragDestination.Value); - - draggedItem = null; - dragDestination = null; - } - - protected override void Update() - { - base.Update(); - - if (draggedItem == null) - return; - - updateScrollPosition(); - updateDragPosition(); - } - - private void updateScrollPosition() - { - const float start_offset = 10; - const double max_power = 50; - const double exp_base = 1.05; - - var localPos = ToLocalSpace(nativeDragPosition); - - if (localPos.Y < start_offset) - { - if (Current <= 0) - return; - - var power = Math.Min(max_power, Math.Abs(start_offset - localPos.Y)); - ScrollBy(-(float)Math.Pow(exp_base, power)); - } - else if (localPos.Y > DrawHeight - start_offset) - { - if (IsScrolledToEnd()) - return; - - var power = Math.Min(max_power, Math.Abs(DrawHeight - start_offset - localPos.Y)); - ScrollBy((float)Math.Pow(exp_base, power)); - } - } - - private void updateDragPosition() - { - var itemsPos = items.ToLocalSpace(nativeDragPosition); - - int srcIndex = (int)items.GetLayoutPosition(draggedItem); - - // Find the last item with position < mouse position. Note we can't directly use - // the item positions as they are being transformed - float heightAccumulator = 0; - int dstIndex = 0; - - for (; dstIndex < items.Count; dstIndex++) - { - // Using BoundingBox here takes care of scale, paddings, etc... - heightAccumulator += items[dstIndex].BoundingBox.Height; - if (heightAccumulator > itemsPos.Y) - break; - } - - dstIndex = Math.Clamp(dstIndex, 0, items.Count - 1); - - if (srcIndex == dstIndex) - return; - - if (srcIndex < dstIndex) - { - for (int i = srcIndex + 1; i <= dstIndex; i++) - items.SetLayoutPosition(items[i], i - 1); - } - else - { - for (int i = dstIndex; i < srcIndex; i++) - items.SetLayoutPosition(items[i], i + 1); - } - - items.SetLayoutPosition(draggedItem, dstIndex); - dragDestination = dstIndex; - } - - private class ItemSearchContainer : FillFlowContainer, IHasFilterableChildren - { - public IEnumerable FilterTerms => Array.Empty(); - - public bool MatchingFilter - { - set - { - if (value) - InvalidateLayout(); - } - } - - public bool FilteringActive - { - set { } - } - - public IEnumerable FilterableChildren => Children; - - public ItemSearchContainer() - { - LayoutDuration = 200; - LayoutEasing = Easing.OutQuint; - } - } - } - } -} diff --git a/osu.Game/Overlays/Music/PlaylistOverlay.cs b/osu.Game/Overlays/Music/PlaylistOverlay.cs index b89a577282..7c391e27f9 100644 --- a/osu.Game/Overlays/Music/PlaylistOverlay.cs +++ b/osu.Game/Overlays/Music/PlaylistOverlay.cs @@ -21,11 +21,15 @@ namespace osu.Game.Overlays.Music private const float transition_duration = 600; private const float playlist_height = 510; + public IBindableList BeatmapSets => beatmapSets; + + private readonly BindableList beatmapSets = new BindableList(); + private readonly Bindable beatmap = new Bindable(); private BeatmapManager beatmaps; private FilterControl filter; - private PlaylistList list; + private Playlist list; [BackgroundDependencyLoader] private void load(OsuColour colours, Bindable beatmap, BeatmapManager beatmaps) @@ -53,11 +57,11 @@ namespace osu.Game.Overlays.Music Colour = colours.Gray3, RelativeSizeAxes = Axes.Both, }, - list = new PlaylistList + list = new Playlist { RelativeSizeAxes = Axes.Both, Padding = new MarginPadding { Top = 95, Bottom = 10, Right = 10 }, - Selected = itemSelected, + RequestSelection = itemSelected }, filter = new FilterControl { @@ -70,6 +74,8 @@ namespace osu.Game.Overlays.Music }, }; + list.Items.BindTo(beatmapSets); + filter.Search.OnCommit = (sender, newText) => { BeatmapInfo toSelect = list.FirstVisibleSet?.Beatmaps?.FirstOrDefault(); @@ -80,6 +86,8 @@ namespace osu.Game.Overlays.Music beatmap.Value.Track.Restart(); } }; + + beatmap.BindValueChanged(working => list.SelectedSet.Value = working.NewValue.BeatmapSetInfo, true); } protected override void PopIn() diff --git a/osu.Game/Overlays/NowPlayingOverlay.cs b/osu.Game/Overlays/NowPlayingOverlay.cs index 042e95c6d7..dfcf99d30c 100644 --- a/osu.Game/Overlays/NowPlayingOverlay.cs +++ b/osu.Game/Overlays/NowPlayingOverlay.cs @@ -183,6 +183,7 @@ namespace osu.Game.Overlays } }; + playlist.BeatmapSets.BindTo(musicController.BeatmapSets); playlist.State.ValueChanged += s => playlistButton.FadeColour(s.NewValue == Visibility.Visible ? colours.Yellow : Color4.White, 200, Easing.OutQuint); } diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index 5ac1d4ecc4..6c81d70190 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -11,7 +11,6 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Input; using osu.Framework.Input.Events; using osu.Framework.Logging; -using osu.Framework.Threading; using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; @@ -50,8 +49,6 @@ namespace osu.Game.Rulesets.Edit [Resolved] private IBeatSnapProvider beatSnapProvider { get; set; } - private IBeatmapProcessor beatmapProcessor; - private DrawableEditRulesetWrapper drawableRulesetWrapper; private ComposeBlueprintContainer blueprintContainer; private Container distanceSnapGridContainer; @@ -71,8 +68,6 @@ namespace osu.Game.Rulesets.Edit [BackgroundDependencyLoader] private void load(IFrameBasedClock framedClock) { - beatmapProcessor = Ruleset.CreateBeatmapProcessor(EditorBeatmap.PlayableBeatmap); - EditorBeatmap.HitObjectAdded += addHitObject; EditorBeatmap.HitObjectRemoved += removeHitObject; EditorBeatmap.StartTimeChanged += UpdateHitObject; @@ -179,7 +174,7 @@ namespace osu.Game.Rulesets.Edit { base.Update(); - if (EditorClock.CurrentTime != lastGridUpdateTime && blueprintContainer.CurrentTool != null) + if (EditorClock.CurrentTime != lastGridUpdateTime && !(blueprintContainer.CurrentTool is SelectTool)) showGridFor(Enumerable.Empty()); } @@ -240,19 +235,6 @@ namespace osu.Game.Rulesets.Edit lastGridUpdateTime = EditorClock.CurrentTime; } - private ScheduledDelegate scheduledUpdate; - - public override void UpdateHitObject(HitObject hitObject) - { - scheduledUpdate?.Cancel(); - scheduledUpdate = Schedule(() => - { - beatmapProcessor?.PreProcess(); - hitObject?.ApplyDefaults(EditorBeatmap.ControlPointInfo, EditorBeatmap.BeatmapInfo.BaseDifficulty); - beatmapProcessor?.PostProcess(); - }); - } - private void addHitObject(HitObject hitObject) => UpdateHitObject(hitObject); private void removeHitObject(HitObject hitObject) => UpdateHitObject(null); @@ -309,6 +291,8 @@ namespace osu.Game.Rulesets.Edit public override float GetSnappedDistanceFromDistance(double referenceTime, float distance) => DurationToDistance(referenceTime, beatSnapProvider.SnapTime(DistanceToDuration(referenceTime, distance), referenceTime)); + public override void UpdateHitObject(HitObject hitObject) => EditorBeatmap.UpdateHitObject(hitObject); + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); @@ -344,7 +328,7 @@ namespace osu.Game.Rulesets.Edit /// Creates the applicable for a selection. /// /// The selection. - /// The for . + /// The for . If empty, a grid is returned for the current point in time. [CanBeNull] protected virtual DistanceSnapGrid CreateDistanceSnapGrid([NotNull] IEnumerable selectedHitObjects) => null; diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs index 8d523022d6..53cdf457c4 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs @@ -26,7 +26,12 @@ namespace osu.Game.Rulesets.Objects.Legacy public List> NodeSamples { get; set; } public int RepeatCount { get; set; } - public double EndTime => StartTime + this.SpanCount() * Distance / Velocity; + public double EndTime + { + get => StartTime + this.SpanCount() * Distance / Velocity; + set => throw new System.NotSupportedException($"Adjust via {nameof(RepeatCount)} instead"); // can be implemented if/when needed. + } + public double Duration => EndTime - StartTime; public double Velocity = 1; diff --git a/osu.Game/Rulesets/Objects/Types/IHasEndTime.cs b/osu.Game/Rulesets/Objects/Types/IHasEndTime.cs index 516f1002a4..bc7103c60d 100644 --- a/osu.Game/Rulesets/Objects/Types/IHasEndTime.cs +++ b/osu.Game/Rulesets/Objects/Types/IHasEndTime.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using Newtonsoft.Json; + namespace osu.Game.Rulesets.Objects.Types { /// @@ -11,7 +13,8 @@ namespace osu.Game.Rulesets.Objects.Types /// /// The time at which the HitObject ends. /// - double EndTime { get; } + [JsonIgnore] + double EndTime { get; set; } /// /// The duration of the HitObject. diff --git a/osu.Game/Rulesets/Objects/Types/IHasRepeats.cs b/osu.Game/Rulesets/Objects/Types/IHasRepeats.cs index b22752e902..256b1f3963 100644 --- a/osu.Game/Rulesets/Objects/Types/IHasRepeats.cs +++ b/osu.Game/Rulesets/Objects/Types/IHasRepeats.cs @@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Objects.Types /// /// The amount of times the HitObject repeats. /// - int RepeatCount { get; } + int RepeatCount { get; set; } /// /// The samples to be played when each node of the is hit.
diff --git a/osu.Game/Rulesets/UI/Scrolling/Algorithms/ConstantScrollAlgorithm.cs b/osu.Game/Rulesets/UI/Scrolling/Algorithms/ConstantScrollAlgorithm.cs index 75ea3efdf2..0d4283e319 100644 --- a/osu.Game/Rulesets/UI/Scrolling/Algorithms/ConstantScrollAlgorithm.cs +++ b/osu.Game/Rulesets/UI/Scrolling/Algorithms/ConstantScrollAlgorithm.cs @@ -5,7 +5,11 @@ namespace osu.Game.Rulesets.UI.Scrolling.Algorithms { public class ConstantScrollAlgorithm : IScrollAlgorithm { - public double GetDisplayStartTime(double time, double timeRange) => time - timeRange; + public double GetDisplayStartTime(double originTime, float offset, double timeRange, float scrollLength) + { + var adjustedTime = TimeAt(-offset, originTime, timeRange, scrollLength); + return adjustedTime - timeRange; + } public float GetLength(double startTime, double endTime, double timeRange, float scrollLength) { diff --git a/osu.Game/Rulesets/UI/Scrolling/Algorithms/IScrollAlgorithm.cs b/osu.Game/Rulesets/UI/Scrolling/Algorithms/IScrollAlgorithm.cs index 5f053975c7..c394a05bcc 100644 --- a/osu.Game/Rulesets/UI/Scrolling/Algorithms/IScrollAlgorithm.cs +++ b/osu.Game/Rulesets/UI/Scrolling/Algorithms/IScrollAlgorithm.cs @@ -6,15 +6,33 @@ namespace osu.Game.Rulesets.UI.Scrolling.Algorithms public interface IScrollAlgorithm { /// - /// Given a point in time, computes the time at which it enters the time range. + /// Given a point in time associated with an object's origin + /// and the spatial distance between the edge and the origin of the object along the scrolling axis, + /// computes the time at which the object initially enters the time range. /// - /// - /// E.g. For a constant time range of 5000ms, the time at which t=7000ms enters the time range is 2000ms. - /// - /// The point in time. + /// + /// Let's assume the following parameters: + /// + /// = 7000ms, + /// = 100px, + /// = 5000ms, + /// = 1000px + /// + /// and a constant scrolling rate. + /// To arrive at the end of the scrolling container, the object's origin has to cover + /// 1000 + 100 = 1100px + /// so that the edge starts at the end of the scrolling container. + /// One scroll length of 1000px covers 5000ms of time, so the time required to cover 1100px is equal to + /// 5000 * (1100 / 1000) = 5500ms, + /// and therefore the object should start being visible at + /// 7000 - 5500 = 1500ms. + /// + /// The time point at which the object origin should enter the time range. + /// The spatial distance between the object's edge and its origin along the scrolling axis. /// The amount of visible time. - /// The time at which enters . - double GetDisplayStartTime(double time, double timeRange); + /// The absolute spatial length through . + /// The time at which the object should enter the time range. + double GetDisplayStartTime(double originTime, float offset, double timeRange, float scrollLength); /// /// Computes the spatial length within a start and end time. diff --git a/osu.Game/Rulesets/UI/Scrolling/Algorithms/OverlappingScrollAlgorithm.cs b/osu.Game/Rulesets/UI/Scrolling/Algorithms/OverlappingScrollAlgorithm.cs index fe22a86fad..7b827e0c63 100644 --- a/osu.Game/Rulesets/UI/Scrolling/Algorithms/OverlappingScrollAlgorithm.cs +++ b/osu.Game/Rulesets/UI/Scrolling/Algorithms/OverlappingScrollAlgorithm.cs @@ -20,11 +20,12 @@ namespace osu.Game.Rulesets.UI.Scrolling.Algorithms searchPoint = new MultiplierControlPoint(); } - public double GetDisplayStartTime(double time, double timeRange) + public double GetDisplayStartTime(double originTime, float offset, double timeRange, float scrollLength) { + var controlPoint = controlPointAt(originTime); // The total amount of time that the hitobject will remain visible within the timeRange, which decreases as the speed multiplier increases - double visibleDuration = timeRange / controlPointAt(time).Multiplier; - return time - visibleDuration; + double visibleDuration = (scrollLength + offset) * timeRange / controlPoint.Multiplier / scrollLength; + return originTime - visibleDuration; } public float GetLength(double startTime, double endTime, double timeRange, float scrollLength) diff --git a/osu.Game/Rulesets/UI/Scrolling/Algorithms/SequentialScrollAlgorithm.cs b/osu.Game/Rulesets/UI/Scrolling/Algorithms/SequentialScrollAlgorithm.cs index 3c9a205412..41f9ebdb82 100644 --- a/osu.Game/Rulesets/UI/Scrolling/Algorithms/SequentialScrollAlgorithm.cs +++ b/osu.Game/Rulesets/UI/Scrolling/Algorithms/SequentialScrollAlgorithm.cs @@ -20,7 +20,11 @@ namespace osu.Game.Rulesets.UI.Scrolling.Algorithms positionCache = new Dictionary(); } - public double GetDisplayStartTime(double time, double timeRange) => time - timeRange - 1000; + public double GetDisplayStartTime(double originTime, float offset, double timeRange, float scrollLength) + { + double adjustedTime = TimeAt(-offset, originTime, timeRange, scrollLength); + return adjustedTime - timeRange - 1000; + } public float GetLength(double startTime, double endTime, double timeRange, float scrollLength) { diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs index 04b4374fc4..83a7f7289f 100644 --- a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs @@ -133,8 +133,7 @@ namespace osu.Game.Rulesets.UI.Scrolling break; } - var adjustedStartTime = scrollingInfo.Algorithm.TimeAt(-originAdjustment, hitObject.HitObject.StartTime, timeRange.Value, scrollLength); - return scrollingInfo.Algorithm.GetDisplayStartTime(adjustedStartTime, timeRange.Value); + return scrollingInfo.Algorithm.GetDisplayStartTime(hitObject.HitObject.StartTime, originAdjustment, timeRange.Value, scrollLength); } // Cant use AddOnce() since the delegate is re-constructed every invocation diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs index a33040f400..0475e68e42 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs @@ -179,11 +179,14 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline [Resolved] private IBeatSnapProvider beatSnapProvider { get; set; } - public (Vector2 position, double time) GetSnappedPosition(Vector2 position, double time) - { - var targetTime = (position.X / Content.DrawWidth) * track.Length; - return (position, beatSnapProvider.SnapTime(targetTime)); - } + public double GetTimeFromScreenSpacePosition(Vector2 position) + => getTimeFromPosition(Content.ToLocalSpace(position)); + + public (Vector2 position, double time) GetSnappedPosition(Vector2 position, double time) => + (position, beatSnapProvider.SnapTime(getTimeFromPosition(position))); + + private double getTimeFromPosition(Vector2 localPosition) => + (localPosition.X / Content.DrawWidth) * track.Length; public float GetBeatSnapDistanceAt(double referenceTime) => throw new NotImplementedException(); diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs index 3b9cb1df24..9f3d776e5c 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs @@ -49,20 +49,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline protected override void OnDrag(DragEvent e) { - if (timeline != null) - { - var timelineQuad = timeline.ScreenSpaceDrawQuad; - var mouseX = e.ScreenSpaceMousePosition.X; - - // scroll if in a drag and dragging outside visible extents - if (mouseX > timelineQuad.TopRight.X) - timeline.ScrollBy((float)((mouseX - timelineQuad.TopRight.X) / 10 * Clock.ElapsedFrameTime)); - else if (mouseX < timelineQuad.TopLeft.X) - timeline.ScrollBy((float)((mouseX - timelineQuad.TopLeft.X) / 10 * Clock.ElapsedFrameTime)); - } + handleScrollViaDrag(e); base.OnDrag(e); - lastDragEvent = e; } protected override void OnDragEnd(DragEndEvent e) @@ -74,7 +63,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline protected override void Update() { // trigger every frame so drags continue to update selection while playback is scrolling the timeline. - if (IsDragged) + if (lastDragEvent != null) OnDrag(lastDragEvent); base.Update(); @@ -82,10 +71,33 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline protected override SelectionHandler CreateSelectionHandler() => new TimelineSelectionHandler(); - protected override SelectionBlueprint CreateBlueprintFor(HitObject hitObject) => new TimelineHitObjectBlueprint(hitObject); + protected override SelectionBlueprint CreateBlueprintFor(HitObject hitObject) => new TimelineHitObjectBlueprint(hitObject) + { + OnDragHandled = handleScrollViaDrag + }; protected override DragBox CreateDragBox(Action performSelect) => new TimelineDragBox(performSelect); + private void handleScrollViaDrag(DragEvent e) + { + lastDragEvent = e; + + if (lastDragEvent == null) + return; + + if (timeline != null) + { + var timelineQuad = timeline.ScreenSpaceDrawQuad; + var mouseX = e.ScreenSpaceMousePosition.X; + + // scroll if in a drag and dragging outside visible extents + if (mouseX > timelineQuad.TopRight.X) + timeline.ScrollBy((float)((mouseX - timelineQuad.TopRight.X) / 10 * Clock.ElapsedFrameTime)); + else if (mouseX < timelineQuad.TopLeft.X) + timeline.ScrollBy((float)((mouseX - timelineQuad.TopLeft.X) / 10 * Clock.ElapsedFrameTime)); + } + } + internal class TimelineSelectionHandler : SelectionHandler { // for now we always allow movement. snapping is provided by the Timeline's "distance" snap implementation diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs index 2ed5471444..8f12c2f0ed 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs @@ -1,12 +1,18 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using System.Collections.Generic; using JetBrains.Annotations; +using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Events; +using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; @@ -19,17 +25,21 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { private readonly Circle circle; - private readonly Container extensionBar; - [UsedImplicitly] private readonly Bindable startTime; - public const float THICKNESS = 3; + public Action OnDragHandled; + + private readonly DragBar dragBar; + + private readonly List shadowComponents = new List(); + + private const float thickness = 5; + + private const float shadow_radius = 5; private const float circle_size = 16; - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => base.ReceivePositionalInputAt(screenSpacePos) || circle.ReceivePositionalInputAt(screenSpacePos); - public TimelineHitObjectBlueprint(HitObject hitObject) : base(hitObject) { @@ -44,26 +54,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; - if (hitObject is IHasEndTime) - { - AddInternal(extensionBar = new Container - { - CornerRadius = 2, - Masking = true, - Size = new Vector2(1, THICKNESS), - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - RelativePositionAxes = Axes.X, - RelativeSizeAxes = Axes.X, - Colour = Color4.Black, - Child = new Box - { - RelativeSizeAxes = Axes.Both, - } - }); - } - - AddInternal(circle = new Circle + circle = new Circle { Size = new Vector2(circle_size), Anchor = Anchor.CentreLeft, @@ -71,9 +62,65 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline RelativePositionAxes = Axes.X, AlwaysPresent = true, Colour = Color4.White, - BorderColour = Color4.Black, - BorderThickness = THICKNESS, - }); + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Shadow, + Radius = shadow_radius, + Colour = Color4.Black + }, + }; + + shadowComponents.Add(circle); + + if (hitObject is IHasEndTime) + { + DragBar dragBarUnderlay; + Container extensionBar; + + AddRangeInternal(new Drawable[] + { + extensionBar = new Container + { + Masking = true, + Size = new Vector2(1, thickness), + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + RelativePositionAxes = Axes.X, + RelativeSizeAxes = Axes.X, + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Shadow, + Radius = shadow_radius, + Colour = Color4.Black + }, + Child = new Box + { + RelativeSizeAxes = Axes.Both, + } + }, + circle, + // only used for drawing the shadow + dragBarUnderlay = new DragBar(null), + // cover up the shadow on the join + new Box + { + Height = thickness, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + RelativeSizeAxes = Axes.X, + }, + dragBar = new DragBar(hitObject) { OnDragHandled = e => OnDragHandled?.Invoke(e) }, + }); + + shadowComponents.Add(dragBarUnderlay); + shadowComponents.Add(extensionBar); + } + else + { + AddInternal(circle); + } + + updateShadows(); } protected override void Update() @@ -84,18 +131,46 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline Width = (float)(HitObject.GetEndTime() - HitObject.StartTime); } + protected override bool ShouldBeConsideredForInput(Drawable child) => true; + + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => + base.ReceivePositionalInputAt(screenSpacePos) || + circle.ReceivePositionalInputAt(screenSpacePos) || + dragBar?.ReceivePositionalInputAt(screenSpacePos) == true; + protected override void OnSelected() { - circle.BorderColour = Color4.Orange; - if (extensionBar != null) - extensionBar.Colour = Color4.Orange; + updateShadows(); + } + + private void updateShadows() + { + foreach (var s in shadowComponents) + { + if (State == SelectionState.Selected) + { + s.EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Shadow, + Radius = shadow_radius / 2, + Colour = Color4.Orange, + }; + } + else + { + s.EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Shadow, + Radius = shadow_radius, + Colour = State == SelectionState.Selected ? Color4.Orange : Color4.Black + }; + } + } } protected override void OnDeselected() { - circle.BorderColour = Color4.Black; - if (extensionBar != null) - extensionBar.Colour = Color4.Black; + updateShadows(); } public override Quad SelectionQuad @@ -103,14 +178,130 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline get { // correctly include the circle in the selection quad region, as it is usually outside the blueprint itself. - var circleQuad = circle.ScreenSpaceDrawQuad; - var actualQuad = ScreenSpaceDrawQuad; + var leftQuad = circle.ScreenSpaceDrawQuad; + var rightQuad = dragBar?.ScreenSpaceDrawQuad ?? ScreenSpaceDrawQuad; - return new Quad(circleQuad.TopLeft, Vector2.ComponentMax(actualQuad.TopRight, circleQuad.TopRight), - circleQuad.BottomLeft, Vector2.ComponentMax(actualQuad.BottomRight, circleQuad.BottomRight)); + return new Quad(leftQuad.TopLeft, Vector2.ComponentMax(rightQuad.TopRight, leftQuad.TopRight), + leftQuad.BottomLeft, Vector2.ComponentMax(rightQuad.BottomRight, leftQuad.BottomRight)); } } public override Vector2 SelectionPoint => ScreenSpaceDrawQuad.TopLeft; + + public class DragBar : Container + { + private readonly HitObject hitObject; + + [Resolved] + private Timeline timeline { get; set; } + + public Action OnDragHandled; + + public override bool HandlePositionalInput => hitObject != null; + + public DragBar(HitObject hitObject) + { + this.hitObject = hitObject; + + CornerRadius = 2; + Masking = true; + Size = new Vector2(5, 1); + Anchor = Anchor.CentreRight; + Origin = Anchor.Centre; + RelativePositionAxes = Axes.X; + RelativeSizeAxes = Axes.Y; + + InternalChildren = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + } + }; + } + + protected override bool OnHover(HoverEvent e) + { + updateState(); + + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + updateState(); + base.OnHoverLost(e); + } + + private bool hasMouseDown; + + protected override bool OnMouseDown(MouseDownEvent e) + { + hasMouseDown = true; + updateState(); + return true; + } + + protected override void OnMouseUp(MouseUpEvent e) + { + hasMouseDown = false; + updateState(); + base.OnMouseUp(e); + } + + private void updateState() + { + Colour = IsHovered || hasMouseDown ? Color4.OrangeRed : Color4.White; + } + + protected override bool OnDragStart(DragStartEvent e) => true; + + [Resolved] + private EditorBeatmap beatmap { get; set; } + + [Resolved] + private IBeatSnapProvider beatSnapProvider { get; set; } + + protected override void OnDrag(DragEvent e) + { + base.OnDrag(e); + + OnDragHandled?.Invoke(e); + + var time = timeline.GetTimeFromScreenSpacePosition(e.ScreenSpaceMousePosition); + + switch (hitObject) + { + case IHasRepeats repeatHitObject: + // find the number of repeats which can fit in the requested time. + var lengthOfOneRepeat = repeatHitObject.Duration / (repeatHitObject.RepeatCount + 1); + var proposedCount = Math.Max(0, (int)((time - hitObject.StartTime) / lengthOfOneRepeat) - 1); + + if (proposedCount == repeatHitObject.RepeatCount) + return; + + repeatHitObject.RepeatCount = proposedCount; + break; + + case IHasEndTime endTimeHitObject: + var snappedTime = Math.Max(hitObject.StartTime, beatSnapProvider.SnapTime(time)); + + if (endTimeHitObject.EndTime == snappedTime) + return; + + endTimeHitObject.EndTime = snappedTime; + break; + } + + beatmap.UpdateHitObject(hitObject); + } + + protected override void OnDragEnd(DragEndEvent e) + { + base.OnDragEnd(e); + + OnDragHandled?.Invoke(null); + } + } } } diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 8c7270d3a2..3bec4b8f6f 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -80,15 +80,15 @@ namespace osu.Game.Screens.Edit clock = new EditorClock(Beatmap.Value, beatDivisor) { IsCoupled = false }; clock.ChangeSource(sourceClock); - playableBeatmap = Beatmap.Value.GetPlayableBeatmap(Beatmap.Value.BeatmapInfo.Ruleset); - editorBeatmap = new EditorBeatmap(playableBeatmap, beatDivisor); - dependencies.CacheAs(clock); dependencies.CacheAs(clock); // todo: remove caching of this and consume via editorBeatmap? dependencies.Cache(beatDivisor); + playableBeatmap = Beatmap.Value.GetPlayableBeatmap(Beatmap.Value.BeatmapInfo.Ruleset); + AddInternal(editorBeatmap = new EditorBeatmap(playableBeatmap)); + dependencies.CacheAs(editorBeatmap); EditorMenuBar menuBar; @@ -104,7 +104,7 @@ namespace osu.Game.Screens.Edit fileMenuItems.Add(new EditorMenuItem("Exit", MenuItemType.Standard, this.Exit)); - InternalChild = new OsuContextMenuContainer + AddInternal(new OsuContextMenuContainer { RelativeSizeAxes = Axes.Both, Children = new[] @@ -189,7 +189,7 @@ namespace osu.Game.Screens.Edit } }, } - }; + }); menuBar.Mode.ValueChanged += onModeChanged; diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index 6edd62fa67..cacb539891 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -4,7 +4,10 @@ using System; using System.Collections; using System.Collections.Generic; +using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Threading; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.Timing; @@ -13,7 +16,7 @@ using osu.Game.Rulesets.Objects; namespace osu.Game.Screens.Edit { - public class EditorBeatmap : IBeatmap, IBeatSnapProvider + public class EditorBeatmap : Component, IBeatmap, IBeatSnapProvider { /// /// Invoked when a is added to this . @@ -34,19 +37,40 @@ namespace osu.Game.Screens.Edit public readonly IBeatmap PlayableBeatmap; - private readonly BindableBeatDivisor beatDivisor; + [Resolved] + private BindableBeatDivisor beatDivisor { get; set; } + + private readonly IBeatmapProcessor beatmapProcessor; private readonly Dictionary> startTimeBindables = new Dictionary>(); - public EditorBeatmap(IBeatmap playableBeatmap, BindableBeatDivisor beatDivisor = null) + public EditorBeatmap(IBeatmap playableBeatmap) { PlayableBeatmap = playableBeatmap; - this.beatDivisor = beatDivisor; + + beatmapProcessor = playableBeatmap.BeatmapInfo.Ruleset?.CreateInstance().CreateBeatmapProcessor(PlayableBeatmap); foreach (var obj in HitObjects) trackStartTime(obj); } + private ScheduledDelegate scheduledUpdate; + + /// + /// Updates a , invoking and re-processing the beatmap. + /// + /// The to update. + public void UpdateHitObject(HitObject hitObject) + { + scheduledUpdate?.Cancel(); + scheduledUpdate = Scheduler.AddDelayed(() => + { + beatmapProcessor?.PreProcess(); + hitObject?.ApplyDefaults(ControlPointInfo, BeatmapInfo.BaseDifficulty); + beatmapProcessor?.PostProcess(); + }, 0); + } + public BeatmapInfo BeatmapInfo { get => PlayableBeatmap.BeatmapInfo; diff --git a/osu.Game/Screens/Multi/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/Multi/Lounge/Components/DrawableRoom.cs index f6cbe300f3..d45dac1ae6 100644 --- a/osu.Game/Screens/Multi/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/Multi/Lounge/Components/DrawableRoom.cs @@ -75,8 +75,13 @@ namespace osu.Game.Screens.Multi.Lounge.Components { matchingFilter = value; - if (IsLoaded) - this.FadeTo(MatchingFilter ? 1 : 0, 200); + if (!IsLoaded) + return; + + if (matchingFilter) + this.FadeIn(200); + else + Hide(); } } diff --git a/osu.Game/Screens/Multi/Lounge/Components/FilterControl.cs b/osu.Game/Screens/Multi/Lounge/Components/FilterControl.cs index 29d41132a7..9f93afec9d 100644 --- a/osu.Game/Screens/Multi/Lounge/Components/FilterControl.cs +++ b/osu.Game/Screens/Multi/Lounge/Components/FilterControl.cs @@ -7,6 +7,7 @@ using osu.Framework.Bindables; using osu.Framework.Threading; using osu.Game.Graphics; using osu.Game.Overlays.SearchableList; +using osu.Game.Rulesets; using osuTK.Graphics; namespace osu.Game.Screens.Multi.Lounge.Components @@ -22,6 +23,9 @@ namespace osu.Game.Screens.Multi.Lounge.Components [Resolved(CanBeNull = true)] private Bindable filter { get; set; } + [Resolved] + private IBindable ruleset { get; set; } + public FilterControl() { DisplayStyleControl.Hide(); @@ -38,6 +42,7 @@ namespace osu.Game.Screens.Multi.Lounge.Components { base.LoadComplete(); + ruleset.BindValueChanged(_ => updateFilter()); Search.Current.BindValueChanged(_ => scheduleUpdateFilter()); Tabs.Current.BindValueChanged(_ => updateFilter(), true); } @@ -58,7 +63,8 @@ namespace osu.Game.Screens.Multi.Lounge.Components { SearchString = Search.Current.Value ?? string.Empty, PrimaryFilter = Tabs.Current.Value, - SecondaryFilter = DisplayStyleControl.Dropdown.Current.Value + SecondaryFilter = DisplayStyleControl.Dropdown.Current.Value, + Ruleset = ruleset.Value }; } } diff --git a/osu.Game/Screens/Multi/Lounge/Components/FilterCriteria.cs b/osu.Game/Screens/Multi/Lounge/Components/FilterCriteria.cs index 666bc44a8d..26d445e151 100644 --- a/osu.Game/Screens/Multi/Lounge/Components/FilterCriteria.cs +++ b/osu.Game/Screens/Multi/Lounge/Components/FilterCriteria.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Game.Rulesets; + namespace osu.Game.Screens.Multi.Lounge.Components { public class FilterCriteria @@ -8,5 +10,6 @@ namespace osu.Game.Screens.Multi.Lounge.Components public string SearchString; public PrimaryFilter PrimaryFilter; public SecondaryFilter SecondaryFilter; + public RulesetInfo Ruleset; } } diff --git a/osu.Game/Screens/Multi/Lounge/Components/RoomsContainer.cs b/osu.Game/Screens/Multi/Lounge/Components/RoomsContainer.cs index 607b081653..063957d816 100644 --- a/osu.Game/Screens/Multi/Lounge/Components/RoomsContainer.cs +++ b/osu.Game/Screens/Multi/Lounge/Components/RoomsContainer.cs @@ -47,22 +47,15 @@ namespace osu.Game.Screens.Multi.Lounge.Components }; } - [BackgroundDependencyLoader] - private void load() - { - rooms.BindTo(roomManager.Rooms); - - rooms.ItemsAdded += addRooms; - rooms.ItemsRemoved += removeRooms; - - roomManager.RoomsUpdated += updateSorting; - - addRooms(rooms); - } - protected override void LoadComplete() { - filter?.BindValueChanged(f => Filter(f.NewValue), true); + rooms.ItemsAdded += addRooms; + rooms.ItemsRemoved += removeRooms; + roomManager.RoomsUpdated += updateSorting; + + rooms.BindTo(roomManager.Rooms); + + filter?.BindValueChanged(criteria => Filter(criteria.NewValue)); } public void Filter(FilterCriteria criteria) @@ -74,7 +67,11 @@ namespace osu.Game.Screens.Multi.Lounge.Components else { bool matchingFilter = true; - matchingFilter &= r.FilterTerms.Any(term => term.IndexOf(criteria.SearchString, StringComparison.InvariantCultureIgnoreCase) >= 0); + + matchingFilter &= r.Room.Playlist.Count == 0 || r.Room.Playlist.Any(i => i.Ruleset.Equals(criteria.Ruleset)); + + if (!string.IsNullOrEmpty(criteria.SearchString)) + matchingFilter &= r.FilterTerms.Any(term => term.IndexOf(criteria.SearchString, StringComparison.InvariantCultureIgnoreCase) >= 0); switch (criteria.SecondaryFilter) { @@ -94,8 +91,7 @@ namespace osu.Game.Screens.Multi.Lounge.Components foreach (var r in rooms) roomFlow.Add(new DrawableRoom(r) { Action = () => selectRoom(r) }); - if (filter != null) - Filter(filter.Value); + Filter(filter?.Value); } private void removeRooms(IEnumerable rooms) diff --git a/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs index 0a48f761cf..3709b85fcb 100644 --- a/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs @@ -91,6 +91,22 @@ namespace osu.Game.Screens.Multi.Lounge public override void OnEntering(IScreen last) { base.OnEntering(last); + + onReturning(); + } + + public override void OnResuming(IScreen last) + { + base.OnResuming(last); + + if (currentRoom.Value?.RoomID.Value == null) + currentRoom.Value = new Room(); + + onReturning(); + } + + private void onReturning() + { Filter.Search.HoldFocus = true; } @@ -106,14 +122,6 @@ namespace osu.Game.Screens.Multi.Lounge Filter.Search.HoldFocus = false; } - public override void OnResuming(IScreen last) - { - base.OnResuming(last); - - if (currentRoom.Value?.RoomID.Value == null) - currentRoom.Value = new Room(); - } - private void joinRequested(Room room) { processingOverlay.Show(); diff --git a/osu.Game/Screens/Multi/Multiplayer.cs b/osu.Game/Screens/Multi/Multiplayer.cs index 9d6a459d14..2277157134 100644 --- a/osu.Game/Screens/Multi/Multiplayer.cs +++ b/osu.Game/Screens/Multi/Multiplayer.cs @@ -32,6 +32,8 @@ namespace osu.Game.Screens.Multi { public override bool CursorVisible => (screenStack.CurrentScreen as IMultiplayerSubScreen)?.CursorVisible ?? true; + // this is required due to PlayerLoader eventually being pushed to the main stack + // while leases may be taken out by a subscreen. public override bool DisallowExternalBeatmapRulesetChanges => true; private readonly MultiplayerWaveContainer waves; @@ -96,7 +98,7 @@ namespace osu.Game.Screens.Multi { RelativeSizeAxes = Axes.Both, Padding = new MarginPadding { Top = Header.HEIGHT }, - Child = screenStack = new OsuScreenStack { RelativeSizeAxes = Axes.Both } + Child = screenStack = new MultiplayerSubScreenStack { RelativeSizeAxes = Axes.Both } }, new Header(screenStack), createButton = new HeaderButton @@ -277,11 +279,7 @@ namespace osu.Game.Screens.Multi private void updateTrack(ValueChangedEvent _ = null) { - bool isMatch = screenStack.CurrentScreen is MatchSubScreen; - - Beatmap.Disabled = isMatch; - - if (isMatch) + if (screenStack.CurrentScreen is MatchSubScreen) { var track = Beatmap.Value?.Track; diff --git a/osu.Game/Screens/Multi/MultiplayerSubScreenStack.cs b/osu.Game/Screens/Multi/MultiplayerSubScreenStack.cs new file mode 100644 index 0000000000..3b0ed0dba1 --- /dev/null +++ b/osu.Game/Screens/Multi/MultiplayerSubScreenStack.cs @@ -0,0 +1,24 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Screens; + +namespace osu.Game.Screens.Multi +{ + public class MultiplayerSubScreenStack : OsuScreenStack + { + protected override void ScreenChanged(IScreen prev, IScreen next) + { + base.ScreenChanged(prev, next); + + // because this is a screen stack within a screen stack, let's manually handle disabled changes to simplify things. + var osuScreen = ((OsuScreen)next); + + bool disallowChanges = osuScreen.DisallowExternalBeatmapRulesetChanges; + + osuScreen.Beatmap.Disabled = disallowChanges; + osuScreen.Ruleset.Disabled = disallowChanges; + osuScreen.Mods.Disabled = disallowChanges; + } + } +} diff --git a/osu.Game/Screens/OsuScreenStack.cs b/osu.Game/Screens/OsuScreenStack.cs index a05933ef0e..e2a0414df7 100644 --- a/osu.Game/Screens/OsuScreenStack.cs +++ b/osu.Game/Screens/OsuScreenStack.cs @@ -26,7 +26,7 @@ namespace osu.Game.Screens }; ScreenPushed += screenPushed; - ScreenExited += screenExited; + ScreenExited += ScreenChanged; } private void screenPushed(IScreen prev, IScreen next) @@ -42,10 +42,10 @@ namespace osu.Game.Screens // create dependencies synchronously to ensure leases are in a sane state. ((OsuScreen)next).CreateLeasedDependencies((prev as OsuScreen)?.Dependencies ?? Dependencies); - setParallax(next); + ScreenChanged(prev, next); } - private void screenExited(IScreen prev, IScreen next) + protected virtual void ScreenChanged(IScreen prev, IScreen next) { setParallax(next); } diff --git a/osu.Game/Screens/Select/MatchSongSelect.cs b/osu.Game/Screens/Select/MatchSongSelect.cs index a78477c771..6ba4157797 100644 --- a/osu.Game/Screens/Select/MatchSongSelect.cs +++ b/osu.Game/Screens/Select/MatchSongSelect.cs @@ -65,20 +65,7 @@ namespace osu.Game.Screens.Select Mods.Value = CurrentItem.Value.RequiredMods?.ToArray() ?? Array.Empty(); } - Beatmap.Disabled = true; - Ruleset.Disabled = true; - Mods.Disabled = true; - return false; } - - public override void OnEntering(IScreen last) - { - base.OnEntering(last); - - Beatmap.Disabled = false; - Ruleset.Disabled = false; - Mods.Disabled = false; - } } } diff --git a/osu.Game/Tests/Beatmaps/TestBeatmap.cs b/osu.Game/Tests/Beatmaps/TestBeatmap.cs index d6f92ba086..96e3c037a3 100644 --- a/osu.Game/Tests/Beatmaps/TestBeatmap.cs +++ b/osu.Game/Tests/Beatmaps/TestBeatmap.cs @@ -15,7 +15,7 @@ namespace osu.Game.Tests.Beatmaps { public TestBeatmap(RulesetInfo ruleset) { - var baseBeatmap = createTestBeatmap(); + var baseBeatmap = CreateBeatmap(); BeatmapInfo = baseBeatmap.BeatmapInfo; ControlPointInfo = baseBeatmap.ControlPointInfo; @@ -37,6 +37,8 @@ namespace osu.Game.Tests.Beatmaps }; } + protected virtual Beatmap CreateBeatmap() => createTestBeatmap(); + private static Beatmap createTestBeatmap() { using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(test_beatmap_data))) diff --git a/osu.Game/Tests/Visual/ScrollingTestContainer.cs b/osu.Game/Tests/Visual/ScrollingTestContainer.cs index 161ebe7030..18326a78ad 100644 --- a/osu.Game/Tests/Visual/ScrollingTestContainer.cs +++ b/osu.Game/Tests/Visual/ScrollingTestContainer.cs @@ -86,8 +86,8 @@ namespace osu.Game.Tests.Visual } } - public double GetDisplayStartTime(double time, double timeRange) - => implementation.GetDisplayStartTime(time, timeRange); + public double GetDisplayStartTime(double originTime, float offset, double timeRange, float scrollLength) + => implementation.GetDisplayStartTime(originTime, offset, timeRange, scrollLength); public float GetLength(double startTime, double endTime, double timeRange, float scrollLength) => implementation.GetLength(startTime, endTime, timeRange, scrollLength); diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index ce58be52ee..21c9eab4c6 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -23,7 +23,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 6ab3c0f2d2..3ed25360c5 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -74,7 +74,7 @@ - + @@ -82,7 +82,7 @@ - +