diff --git a/osu.Android.props b/osu.Android.props index 5078fee1cf..6e3d5eec1f 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,6 +52,6 @@ - + diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaBlueprintContainer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaBlueprintContainer.cs index cea27498c3..2fa3f378ff 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaBlueprintContainer.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaBlueprintContainer.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Generic; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Mania.Edit.Blueprints; using osu.Game.Rulesets.Mania.Objects.Drawables; @@ -12,8 +11,8 @@ namespace osu.Game.Rulesets.Mania.Edit { public class ManiaBlueprintContainer : ComposeBlueprintContainer { - public ManiaBlueprintContainer(IEnumerable drawableHitObjects) - : base(drawableHitObjects) + public ManiaBlueprintContainer(HitObjectComposer composer) + : base(composer) { } diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs index 7e2469a794..01d572447b 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs @@ -13,7 +13,6 @@ using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces; using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; -using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Screens.Edit.Compose.Components; @@ -89,8 +88,8 @@ namespace osu.Game.Rulesets.Mania.Edit return drawableRuleset; } - protected override ComposeBlueprintContainer CreateBlueprintContainer(IEnumerable hitObjects) - => new ManiaBlueprintContainer(hitObjects); + protected override ComposeBlueprintContainer CreateBlueprintContainer() + => new ManiaBlueprintContainer(this); protected override IReadOnlyList CompositionTools => new HitObjectCompositionTool[] { diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleApplication.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleApplication.cs index 8b3fead366..5fc1082743 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleApplication.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleApplication.cs @@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Osu.Tests { Position = new Vector2(128, 128), ComboIndex = 1, - }))); + }), null)); } private HitCircle prepareObject(HitCircle circle) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs index f76c7e2a3e..fb1ebbb0d0 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs @@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Osu.Tests new Vector2(300, 0), }), RepeatCount = 1 - }))); + }), null)); } private Slider prepareObject(Slider slider) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerApplication.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerApplication.cs index 5951574079..0558dad30d 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerApplication.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerApplication.cs @@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Osu.Tests Position = new Vector2(256, 192), ComboIndex = 1, Duration = 1000, - }))); + }), null)); } private Spinner prepareObject(Spinner circle) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuBlueprintContainer.cs b/osu.Game.Rulesets.Osu/Edit/OsuBlueprintContainer.cs index 330f34b85c..a68ed34e6b 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuBlueprintContainer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuBlueprintContainer.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Generic; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles; @@ -14,8 +13,8 @@ namespace osu.Game.Rulesets.Osu.Edit { public class OsuBlueprintContainer : ComposeBlueprintContainer { - public OsuBlueprintContainer(IEnumerable drawableHitObjects) - : base(drawableHitObjects) + public OsuBlueprintContainer(HitObjectComposer composer) + : base(composer) { } diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index edd684d886..bfa8ab4431 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -16,7 +16,6 @@ using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; -using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.UI; using osu.Game.Screens.Edit.Components.TernaryButtons; @@ -80,8 +79,8 @@ namespace osu.Game.Rulesets.Osu.Edit updateDistanceSnapGrid(); } - protected override ComposeBlueprintContainer CreateBlueprintContainer(IEnumerable hitObjects) - => new OsuBlueprintContainer(hitObjects); + protected override ComposeBlueprintContainer CreateBlueprintContainer() + => new OsuBlueprintContainer(this); private DistanceSnapGrid distanceSnapGrid; private Container distanceSnapGridContainer; diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index fd7ea050b9..24bf79f9ae 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -45,12 +45,12 @@ namespace osu.Game.Rulesets.Osu.Edit public override bool HandleReverse() { - var hitObjects = selectedMovableObjects; + var hitObjects = EditorBeatmap.SelectedHitObjects; double endTime = hitObjects.Max(h => h.GetEndTime()); double startTime = hitObjects.Min(h => h.StartTime); - bool moreThanOneObject = hitObjects.Length > 1; + bool moreThanOneObject = hitObjects.Count > 1; foreach (var h in hitObjects) { diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/MainCirclePiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/MainCirclePiece.cs index 98432eb4fe..bf2236c945 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/MainCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/MainCirclePiece.cs @@ -51,9 +51,15 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces var drawableOsuObject = (DrawableOsuHitObject)drawableObject; state.BindTo(drawableObject.State); - state.BindValueChanged(updateState, true); - accentColour.BindTo(drawableObject.AccentColour); + indexInCurrentCombo.BindTo(drawableOsuObject.IndexInCurrentComboBindable); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + state.BindValueChanged(updateState, true); accentColour.BindValueChanged(colour => { explode.Colour = colour.NewValue; @@ -61,7 +67,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces circle.Colour = colour.NewValue; }, true); - indexInCurrentCombo.BindTo(drawableOsuObject.IndexInCurrentComboBindable); indexInCurrentCombo.BindValueChanged(index => number.Text = (index.NewValue + 1).ToString(), true); } diff --git a/osu.Game.Rulesets.Taiko/Edit/TaikoBlueprintContainer.cs b/osu.Game.Rulesets.Taiko/Edit/TaikoBlueprintContainer.cs index 35227b3c64..8b41448c9d 100644 --- a/osu.Game.Rulesets.Taiko/Edit/TaikoBlueprintContainer.cs +++ b/osu.Game.Rulesets.Taiko/Edit/TaikoBlueprintContainer.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Generic; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Taiko.Edit.Blueprints; @@ -11,8 +10,8 @@ namespace osu.Game.Rulesets.Taiko.Edit { public class TaikoBlueprintContainer : ComposeBlueprintContainer { - public TaikoBlueprintContainer(IEnumerable hitObjects) - : base(hitObjects) + public TaikoBlueprintContainer(HitObjectComposer composer) + : base(composer) { } diff --git a/osu.Game.Rulesets.Taiko/Edit/TaikoHitObjectComposer.cs b/osu.Game.Rulesets.Taiko/Edit/TaikoHitObjectComposer.cs index cdc9672a8e..161799c980 100644 --- a/osu.Game.Rulesets.Taiko/Edit/TaikoHitObjectComposer.cs +++ b/osu.Game.Rulesets.Taiko/Edit/TaikoHitObjectComposer.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit.Tools; -using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Screens.Edit.Compose.Components; @@ -24,7 +23,7 @@ namespace osu.Game.Rulesets.Taiko.Edit new SwellCompositionTool() }; - protected override ComposeBlueprintContainer CreateBlueprintContainer(IEnumerable hitObjects) - => new TaikoBlueprintContainer(hitObjects); + protected override ComposeBlueprintContainer CreateBlueprintContainer() + => new TaikoBlueprintContainer(this); } } diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimelineBlueprintContainer.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimelineBlueprintContainer.cs index e931be044c..6b54bcb4f0 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneTimelineBlueprintContainer.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneTimelineBlueprintContainer.cs @@ -10,7 +10,7 @@ namespace osu.Game.Tests.Visual.Editing [TestFixture] public class TestSceneTimelineBlueprintContainer : TimelineTestScene { - public override Drawable CreateTestComponent() => new TimelineBlueprintContainer(); + public override Drawable CreateTestComponent() => new TimelineBlueprintContainer(Composer); protected override void LoadComplete() { diff --git a/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs b/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs index fdb8781563..63bb018d6e 100644 --- a/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs +++ b/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs @@ -21,21 +21,25 @@ namespace osu.Game.Tests.Visual.Editing { protected TimelineArea TimelineArea { get; private set; } + protected HitObjectComposer Composer { get; private set; } + [BackgroundDependencyLoader] private void load(AudioManager audio) { Beatmap.Value = new WaveformTestBeatmap(audio); var playable = Beatmap.Value.GetPlayableBeatmap(Beatmap.Value.BeatmapInfo.Ruleset); - var editorBeatmap = new EditorBeatmap(playable); Dependencies.Cache(editorBeatmap); Dependencies.CacheAs(editorBeatmap); + Composer = playable.BeatmapInfo.Ruleset.CreateInstance().CreateHitObjectComposer().With(d => d.Alpha = 0); + AddRange(new Drawable[] { editorBeatmap, + Composer, new FillFlowContainer { AutoSizeAxes = Axes.Both, diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs index d80efb2c6e..745932315c 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs @@ -6,6 +6,7 @@ using System.Linq; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; using osu.Game.Screens.Play; namespace osu.Game.Tests.Visual.Gameplay @@ -21,8 +22,14 @@ namespace osu.Game.Tests.Visual.Gameplay protected override void AddCheckSteps() { AddUntilStep("wait for fail", () => Player.HasFailed); - AddUntilStep("wait for multiple judged objects", () => ((FailPlayer)Player).DrawableRuleset.Playfield.AllHitObjects.Count(h => h.AllJudged) > 1); - AddAssert("total judgements == 1", () => ((FailPlayer)Player).HealthProcessor.JudgedHits >= 1); + AddUntilStep("wait for multiple judgements", () => ((FailPlayer)Player).ScoreProcessor.JudgedHits > 1); + AddAssert("total number of results == 1", () => + { + var score = new ScoreInfo(); + ((FailPlayer)Player).ScoreProcessor.PopulateScore(score); + + return score.Statistics.Values.Sum() == 1; + }); } private class FailPlayer : TestPlayer diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs new file mode 100644 index 0000000000..242eaf7b7d --- /dev/null +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs @@ -0,0 +1,247 @@ +// 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 System.Threading; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input; +using osu.Framework.Testing; +using osu.Framework.Timing; +using osu.Framework.Utils; +using osu.Game.Beatmaps; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Difficulty; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Objects.Legacy; +using osu.Game.Rulesets.UI; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Tests.Visual.Gameplay +{ + public class TestScenePoolingRuleset : OsuTestScene + { + private const double time_between_objects = 1000; + + private TestDrawablePoolingRuleset drawableRuleset; + + [Test] + public void TestReusedWithHitObjectsSpacedFarApart() + { + ManualClock clock = null; + + createTest(new Beatmap + { + HitObjects = + { + new HitObject(), + new HitObject { StartTime = time_between_objects } + } + }, 1, () => new FramedClock(clock = new ManualClock())); + + DrawableTestHitObject firstObject = null; + AddUntilStep("first object shown", () => this.ChildrenOfType().SingleOrDefault()?.HitObject == drawableRuleset.Beatmap.HitObjects[0]); + AddStep("get DHO", () => firstObject = this.ChildrenOfType().Single()); + + AddStep("fast forward to second object", () => clock.CurrentTime = drawableRuleset.Beatmap.HitObjects[1].StartTime); + + AddUntilStep("second object shown", () => this.ChildrenOfType().SingleOrDefault()?.HitObject == drawableRuleset.Beatmap.HitObjects[1]); + AddAssert("DHO reused", () => this.ChildrenOfType().Single() == firstObject); + } + + [Test] + public void TestNotReusedWithHitObjectsSpacedClose() + { + ManualClock clock = null; + + createTest(new Beatmap + { + HitObjects = + { + new HitObject(), + new HitObject { StartTime = 250 } + } + }, 2, () => new FramedClock(clock = new ManualClock())); + + AddStep("fast forward to second object", () => clock.CurrentTime = drawableRuleset.Beatmap.HitObjects[1].StartTime); + + AddUntilStep("two DHOs shown", () => this.ChildrenOfType().Count() == 2); + AddAssert("DHOs have different hitobjects", + () => this.ChildrenOfType().ElementAt(0).HitObject != this.ChildrenOfType().ElementAt(1).HitObject); + } + + [Test] + public void TestManyHitObjects() + { + var beatmap = new Beatmap(); + + for (int i = 0; i < 500; i++) + beatmap.HitObjects.Add(new HitObject { StartTime = i * 10 }); + + createTest(beatmap, 100); + + AddUntilStep("any DHOs shown", () => this.ChildrenOfType().Any()); + AddUntilStep("no DHOs shown", () => !this.ChildrenOfType().Any()); + } + + private void createTest(IBeatmap beatmap, int poolSize, Func createClock = null) => AddStep("create test", () => + { + var ruleset = new TestPoolingRuleset(); + + drawableRuleset = (TestDrawablePoolingRuleset)ruleset.CreateDrawableRulesetWith(CreateWorkingBeatmap(beatmap).GetPlayableBeatmap(ruleset.RulesetInfo)); + drawableRuleset.FrameStablePlayback = true; + drawableRuleset.PoolSize = poolSize; + + Child = new Container + { + RelativeSizeAxes = Axes.Both, + Clock = createClock?.Invoke() ?? new FramedOffsetClock(Clock, false) { Offset = -Clock.CurrentTime }, + Child = drawableRuleset + }; + }); + + #region Ruleset + + private class TestPoolingRuleset : Ruleset + { + public override IEnumerable GetModsFor(ModType type) => throw new NotImplementedException(); + + public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => new TestDrawablePoolingRuleset(this, beatmap, mods); + + public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new TestBeatmapConverter(beatmap, this); + + public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => throw new NotImplementedException(); + + public override string Description { get; } = string.Empty; + + public override string ShortName { get; } = string.Empty; + } + + private class TestDrawablePoolingRuleset : DrawableRuleset + { + public int PoolSize; + + public TestDrawablePoolingRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null) + : base(ruleset, beatmap, mods) + { + } + + [BackgroundDependencyLoader] + private void load() + { + RegisterPool(PoolSize); + } + + protected override HitObjectLifetimeEntry CreateLifetimeEntry(TestHitObject hitObject) => new TestHitObjectLifetimeEntry(hitObject); + + public override DrawableHitObject CreateDrawableRepresentation(TestHitObject h) => null; + + protected override PassThroughInputManager CreateInputManager() => new PassThroughInputManager(); + + protected override Playfield CreatePlayfield() => new TestPlayfield(); + + private class TestHitObjectLifetimeEntry : HitObjectLifetimeEntry + { + public TestHitObjectLifetimeEntry(HitObject hitObject) + : base(hitObject) + { + } + + protected override double InitialLifetimeOffset => 0; + } + } + + private class TestPlayfield : Playfield + { + public TestPlayfield() + { + AddInternal(HitObjectContainer); + } + + protected override GameplayCursorContainer CreateCursor() => null; + } + + private class TestBeatmapConverter : BeatmapConverter + { + public TestBeatmapConverter(IBeatmap beatmap, Ruleset ruleset) + : base(beatmap, ruleset) + { + } + + public override bool CanConvert() => true; + + protected override IEnumerable ConvertHitObject(HitObject original, IBeatmap beatmap, CancellationToken cancellationToken) + { + yield return new TestHitObject + { + StartTime = original.StartTime, + Duration = 250 + }; + } + } + + #endregion + + #region HitObject + + private class TestHitObject : ConvertHitObject + { + public double EndTime => StartTime + Duration; + + public double Duration { get; set; } + } + + private class DrawableTestHitObject : DrawableHitObject + { + public DrawableTestHitObject() + : base(null) + { + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + + Position = new Vector2(RNG.Next(-200, 200), RNG.Next(-200, 200)); + Size = new Vector2(50, 50); + + Colour = new Color4(RNG.NextSingle(), RNG.NextSingle(), RNG.NextSingle(), 1f); + } + + [BackgroundDependencyLoader] + private void load() + { + AddInternal(new Circle + { + RelativeSizeAxes = Axes.Both, + }); + } + + protected override void CheckForResult(bool userTriggered, double timeOffset) + { + if (timeOffset > HitObject.Duration) + ApplyResult(r => r.Type = r.Judgement.MaxResult); + } + + protected override void UpdateHitStateTransforms(ArmedState state) + { + base.UpdateHitStateTransforms(state); + + switch (state) + { + case ArmedState.Hit: + case ArmedState.Miss: + this.FadeOut(250); + break; + } + } + } + + #endregion + } +} diff --git a/osu.Game.Tests/Visual/Navigation/TestScenePerformFromScreen.cs b/osu.Game.Tests/Visual/Navigation/TestScenePerformFromScreen.cs index a4190e0b84..21d3bdaae3 100644 --- a/osu.Game.Tests/Visual/Navigation/TestScenePerformFromScreen.cs +++ b/osu.Game.Tests/Visual/Navigation/TestScenePerformFromScreen.cs @@ -1,9 +1,12 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Graphics.Containers; using osu.Framework.Screens; +using osu.Framework.Testing; using osu.Game.Overlays; using osu.Game.Screens; using osu.Game.Screens.Menu; @@ -73,6 +76,22 @@ namespace osu.Game.Tests.Visual.Navigation AddAssert("did perform", () => actionPerformed); } + [Test] + public void TestOverlaysAlwaysClosed() + { + ChatOverlay chat = null; + AddUntilStep("is at menu", () => Game.ScreenStack.CurrentScreen is MainMenu); + AddUntilStep("wait for chat load", () => (chat = Game.ChildrenOfType().SingleOrDefault()) != null); + + AddStep("show chat", () => InputManager.Key(Key.F8)); + + AddStep("try to perform", () => Game.PerformFromScreen(_ => actionPerformed = true)); + + AddUntilStep("still at menu", () => Game.ScreenStack.CurrentScreen is MainMenu); + AddAssert("did perform", () => actionPerformed); + AddAssert("chat closed", () => chat.State.Value == Visibility.Hidden); + } + [TestCase(true)] [TestCase(false)] public void TestPerformBlockedByDialog(bool confirmed) diff --git a/osu.Game.Tests/WaveformTestBeatmap.cs b/osu.Game.Tests/WaveformTestBeatmap.cs index f9613d9e25..8c8c827404 100644 --- a/osu.Game.Tests/WaveformTestBeatmap.cs +++ b/osu.Game.Tests/WaveformTestBeatmap.cs @@ -8,10 +8,9 @@ using osu.Framework.Audio; using osu.Framework.Audio.Track; using osu.Framework.Graphics.Textures; 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.Rulesets; +using osu.Game.Rulesets.Osu; using osu.Game.Tests.Beatmaps; using osu.Game.Tests.Resources; @@ -25,8 +24,8 @@ namespace osu.Game.Tests private readonly Beatmap beatmap; private readonly ITrackStore trackStore; - public WaveformTestBeatmap(AudioManager audioManager) - : this(audioManager, new WaveformBeatmap()) + public WaveformTestBeatmap(AudioManager audioManager, RulesetInfo rulesetInfo = null) + : this(audioManager, new TestBeatmap(rulesetInfo ?? new OsuRuleset().RulesetInfo)) { } @@ -63,21 +62,5 @@ namespace osu.Game.Tests return reader.Filenames.First(f => f.EndsWith(".mp3", StringComparison.Ordinal)); } } - - private class WaveformBeatmap : TestBeatmap - { - 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", StringComparison.Ordinal)))) - using (var beatmapReader = new LineBufferedReader(beatmapStream)) - return Decoder.GetDecoder(beatmapReader).Decode(beatmapReader); - } - } } } diff --git a/osu.Game/PerformFromMenuRunner.cs b/osu.Game/PerformFromMenuRunner.cs index 9afe87f74f..5898c116dd 100644 --- a/osu.Game/PerformFromMenuRunner.cs +++ b/osu.Game/PerformFromMenuRunner.cs @@ -76,6 +76,8 @@ namespace osu.Game // a dialog may be blocking the execution for now. if (checkForDialog(current)) return; + game.CloseAllOverlays(false); + // we may already be at the target screen type. if (validScreens.Contains(getCurrentScreen().GetType()) && !beatmap.Disabled) { @@ -83,8 +85,6 @@ namespace osu.Game return; } - game.CloseAllOverlays(false); - while (current != null) { if (validScreens.Contains(current.GetType())) diff --git a/osu.Game/Rulesets/Edit/DrawableEditRulesetWrapper.cs b/osu.Game/Rulesets/Edit/DrawableEditRulesetWrapper.cs index 8ed7885101..c60d4c7834 100644 --- a/osu.Game/Rulesets/Edit/DrawableEditRulesetWrapper.cs +++ b/osu.Game/Rulesets/Edit/DrawableEditRulesetWrapper.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -65,17 +64,13 @@ namespace osu.Game.Rulesets.Edit private void addHitObject(HitObject hitObject) { - var drawableObject = drawableRuleset.CreateDrawableRepresentation((TObject)hitObject); - - drawableRuleset.Playfield.Add(drawableObject); + drawableRuleset.AddHitObject((TObject)hitObject); drawableRuleset.Playfield.PostProcess(); } private void removeHitObject(HitObject hitObject) { - var drawableObject = Playfield.AllHitObjects.Single(d => d.HitObject == hitObject); - - drawableRuleset.Playfield.Remove(drawableObject); + drawableRuleset.RemoveHitObject((TObject)hitObject); drawableRuleset.Playfield.PostProcess(); } diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index c9dd061b48..b90aa6863a 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -104,7 +104,7 @@ namespace osu.Game.Rulesets.Edit drawableRulesetWrapper, // layers above playfield drawableRulesetWrapper.CreatePlayfieldAdjustmentContainer() - .WithChild(BlueprintContainer = CreateBlueprintContainer(HitObjects)) + .WithChild(BlueprintContainer = CreateBlueprintContainer()) } }, new FillFlowContainer @@ -182,9 +182,8 @@ namespace osu.Game.Rulesets.Edit /// /// Construct a relevant blueprint container. This will manage hitobject selection/placement input handling and display logic. /// - /// A live collection of all s in the editor beatmap. - protected virtual ComposeBlueprintContainer CreateBlueprintContainer(IEnumerable hitObjects) - => new ComposeBlueprintContainer(hitObjects); + protected virtual ComposeBlueprintContainer CreateBlueprintContainer() + => new ComposeBlueprintContainer(this); /// /// Construct a drawable ruleset for the provided ruleset. diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 62709b2900..3e3936b45a 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -10,6 +10,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Primitives; using osu.Framework.Logging; using osu.Framework.Threading; @@ -19,6 +20,7 @@ using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Scoring; using osu.Game.Skinning; using osu.Game.Configuration; +using osu.Game.Rulesets.UI; using osuTK.Graphics; namespace osu.Game.Rulesets.Objects.Drawables @@ -108,7 +110,7 @@ namespace osu.Game.Rulesets.Objects.Drawables /// protected virtual float SamplePlaybackPosition => 0.5f; - private readonly Bindable startTimeBindable = new Bindable(); + public readonly Bindable StartTimeBindable = new Bindable(); private readonly BindableList samplesBindable = new BindableList(); private readonly Bindable userPositionalHitSounds = new Bindable(); private readonly Bindable comboIndexBindable = new Bindable(); @@ -128,6 +130,17 @@ namespace osu.Game.Rulesets.Objects.Drawables /// private bool hasHitObjectApplied; + /// + /// The controlling the lifetime of the currently-attached . + /// + [CanBeNull] + private HitObjectLifetimeEntry lifetimeEntry; + + [Resolved(CanBeNull = true)] + private DrawableRuleset drawableRuleset { get; set; } + + private Container samplesContainer; + /// /// Creates a new . /// @@ -144,6 +157,9 @@ namespace osu.Game.Rulesets.Objects.Drawables private void load(OsuConfigManager config) { config.BindWith(OsuSetting.PositionalHitSounds, userPositionalHitSounds); + + // Explicit non-virtual function call. + base.AddInternal(samplesContainer = new Container { RelativeSizeAxes = Axes.Both }); } protected override void LoadAsyncComplete() @@ -151,14 +167,13 @@ namespace osu.Game.Rulesets.Objects.Drawables base.LoadAsyncComplete(); if (HitObject != null) - Apply(HitObject); + Apply(HitObject, lifetimeEntry); } protected override void LoadComplete() { base.LoadComplete(); - startTimeBindable.BindValueChanged(_ => updateState(State.Value, true)); comboIndexBindable.BindValueChanged(_ => updateComboColour(), true); updateState(ArmedState.Idle, true); @@ -168,19 +183,38 @@ namespace osu.Game.Rulesets.Objects.Drawables /// Applies a new to be represented by this . /// /// The to apply. - public void Apply(HitObject hitObject) + /// The controlling the lifetime of . + public void Apply([NotNull] HitObject hitObject, [CanBeNull] HitObjectLifetimeEntry lifetimeEntry) { free(); HitObject = hitObject ?? throw new InvalidOperationException($"Cannot apply a null {nameof(HitObject)}."); + this.lifetimeEntry = lifetimeEntry; + + if (lifetimeEntry != null) + { + // Transfer lifetime from the entry. + LifetimeStart = lifetimeEntry.LifetimeStart; + LifetimeEnd = lifetimeEntry.LifetimeEnd; + + // Copy any existing result from the entry (required for rewind / judgement revert). + Result = lifetimeEntry.Result; + } + // Ensure this DHO has a result. Result ??= CreateResult(HitObject.CreateJudgement()) ?? throw new InvalidOperationException($"{GetType().ReadableName()} must provide a {nameof(JudgementResult)} through {nameof(CreateResult)}."); + // Copy back the result to the entry for potential future retrieval. + if (lifetimeEntry != null) + lifetimeEntry.Result = Result; + foreach (var h in HitObject.NestedHitObjects) { - var drawableNested = CreateNestedHitObject(h) ?? throw new InvalidOperationException($"{nameof(CreateNestedHitObject)} returned null for {h.GetType().ReadableName()}."); + var drawableNested = drawableRuleset?.GetPooledDrawableRepresentation(h) + ?? CreateNestedHitObject(h) + ?? throw new InvalidOperationException($"{nameof(CreateNestedHitObject)} returned null for {h.GetType().ReadableName()}."); drawableNested.OnNewResult += onNewResult; drawableNested.OnRevertResult += onRevertResult; @@ -188,9 +222,13 @@ namespace osu.Game.Rulesets.Objects.Drawables nestedHitObjects.Value.Add(drawableNested); AddNestedHitObject(drawableNested); + + drawableNested.OnParentReceived(this); } - startTimeBindable.BindTo(HitObject.StartTimeBindable); + StartTimeBindable.BindTo(HitObject.StartTimeBindable); + StartTimeBindable.BindValueChanged(onStartTimeChanged); + if (HitObject is IHasComboInformation combo) comboIndexBindable.BindTo(combo.ComboIndexBindable); @@ -217,12 +255,14 @@ namespace osu.Game.Rulesets.Objects.Drawables if (!hasHitObjectApplied) return; - startTimeBindable.UnbindFrom(HitObject.StartTimeBindable); + StartTimeBindable.UnbindFrom(HitObject.StartTimeBindable); if (HitObject is IHasComboInformation combo) comboIndexBindable.UnbindFrom(combo.ComboIndexBindable); - samplesBindable.UnbindFrom(HitObject.SamplesBindable); + // Changes in start time trigger state updates. When a new hitobject is applied, OnApply() automatically performs a state update anyway. + StartTimeBindable.ValueChanged -= onStartTimeChanged; + // When a new hitobject is applied, the samples will be cleared before re-populating. // In order to stop this needless update, the event is unbound and re-bound as late as possible in Apply(). samplesBindable.CollectionChanged -= onSamplesChanged; @@ -245,6 +285,8 @@ namespace osu.Game.Rulesets.Objects.Drawables OnFree(HitObject); HitObject = null; + lifetimeEntry = null; + hasHitObjectApplied = false; } @@ -275,16 +317,21 @@ namespace osu.Game.Rulesets.Objects.Drawables { } + /// + /// Invoked when this receives a new parenting . + /// + /// The parenting . + protected virtual void OnParentReceived(DrawableHitObject parent) + { + } + /// /// Invoked by the base to populate samples, once on initial load and potentially again on any change to the samples collection. /// protected virtual void LoadSamples() { - if (Samples != null) - { - RemoveInternal(Samples); - Samples = null; - } + samplesContainer.Clear(); + Samples = null; var samples = GetSamples().ToArray(); @@ -297,12 +344,13 @@ namespace osu.Game.Rulesets.Objects.Drawables + $" This is an indication that {nameof(HitObject.ApplyDefaults)} has not been invoked on {this}."); } - Samples = new PausableSkinnableSound(samples.Select(s => HitObject.SampleControlPoint.ApplyTo(s))); - AddInternal(Samples); + samplesContainer.Add(Samples = new PausableSkinnableSound(samples.Select(s => HitObject.SampleControlPoint.ApplyTo(s)))); } private void onSamplesChanged(object sender, NotifyCollectionChangedEventArgs e) => LoadSamples(); + private void onStartTimeChanged(ValueChangedEvent startTime) => updateState(State.Value, true); + private void onNewResult(DrawableHitObject drawableHitObject, JudgementResult result) => OnNewResult?.Invoke(drawableHitObject, result); private void onRevertResult(DrawableHitObject drawableHitObject, JudgementResult result) => OnRevertResult?.Invoke(drawableHitObject, result); @@ -311,7 +359,7 @@ namespace osu.Game.Rulesets.Objects.Drawables private void onDefaultsApplied(HitObject hitObject) { - Apply(hitObject); + Apply(hitObject, lifetimeEntry); DefaultsApplied?.Invoke(this); } @@ -558,15 +606,27 @@ namespace osu.Game.Rulesets.Objects.Drawables /// protected internal new ScheduledDelegate Schedule(Action action) => base.Schedule(action); - private double? lifetimeStart; - public override double LifetimeStart { - get => lifetimeStart ?? (HitObject.StartTime - InitialLifetimeOffset); - set + get => base.LifetimeStart; + set => setLifetime(value, LifetimeEnd); + } + + public override double LifetimeEnd + { + get => base.LifetimeEnd; + set => setLifetime(LifetimeStart, value); + } + + private void setLifetime(double lifetimeStart, double lifetimeEnd) + { + base.LifetimeStart = lifetimeStart; + base.LifetimeEnd = lifetimeEnd; + + if (lifetimeEntry != null) { - lifetimeStart = value; - base.LifetimeStart = value; + lifetimeEntry.LifetimeStart = lifetimeStart; + lifetimeEntry.LifetimeEnd = lifetimeEnd; } } diff --git a/osu.Game/Rulesets/Objects/HitObjectLifetimeEntry.cs b/osu.Game/Rulesets/Objects/HitObjectLifetimeEntry.cs new file mode 100644 index 0000000000..1954d7e6d2 --- /dev/null +++ b/osu.Game/Rulesets/Objects/HitObjectLifetimeEntry.cs @@ -0,0 +1,110 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Bindables; +using osu.Framework.Graphics.Performance; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects.Drawables; + +namespace osu.Game.Rulesets.Objects +{ + /// + /// A that stores the lifetime for a . + /// + public class HitObjectLifetimeEntry : LifetimeEntry + { + /// + /// The . + /// + public readonly HitObject HitObject; + + /// + /// The result that was judged with. + /// This is set by the accompanying , and reused when required for rewinding. + /// + internal JudgementResult Result; + + private readonly IBindable startTimeBindable = new BindableDouble(); + + /// + /// Creates a new . + /// + /// The to store the lifetime of. + public HitObjectLifetimeEntry(HitObject hitObject) + { + HitObject = hitObject; + + startTimeBindable.BindTo(HitObject.StartTimeBindable); + startTimeBindable.BindValueChanged(onStartTimeChanged, true); + } + + // The lifetime start, as set by the hitobject. + private double realLifetimeStart = double.MinValue; + + /// + /// The time at which the should become alive. + /// + public new double LifetimeStart + { + get => realLifetimeStart; + set => setLifetime(realLifetimeStart = value, LifetimeEnd); + } + + // The lifetime end, as set by the hitobject. + private double realLifetimeEnd = double.MaxValue; + + /// + /// The time at which the should become dead. + /// + public new double LifetimeEnd + { + get => realLifetimeEnd; + set => setLifetime(LifetimeStart, realLifetimeEnd = value); + } + + private void setLifetime(double start, double end) + { + if (keepAlive) + { + start = double.MinValue; + end = double.MaxValue; + } + + base.LifetimeStart = start; + base.LifetimeEnd = end; + } + + private bool keepAlive; + + /// + /// Whether the should be kept always alive. + /// + internal bool KeepAlive + { + set + { + if (keepAlive == value) + return; + + keepAlive = value; + setLifetime(realLifetimeStart, realLifetimeEnd); + } + } + + /// + /// A safe offset prior to the start time of at which it may begin displaying contents. + /// By default, s are assumed to display their contents within 10 seconds prior to their start time. + /// + /// + /// This is only used as an optimisation to delay the initial update of the and may be tuned more aggressively if required. + /// It is indirectly used to decide the automatic transform offset provided to . + /// A more accurate should be set for further optimisation (in , for example). + /// + protected virtual double InitialLifetimeOffset => 10000; + + /// + /// Resets according to the change in start time of the . + /// + private void onStartTimeChanged(ValueChangedEvent startTime) => LifetimeStart = HitObject.StartTime - InitialLifetimeOffset; + } +} diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index a36b66d62b..c912348604 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -15,7 +15,10 @@ using System.Linq; using System.Threading; using JetBrains.Annotations; using osu.Framework.Bindables; +using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Pooling; using osu.Framework.Input; using osu.Framework.Input.Events; using osu.Game.Configuration; @@ -38,9 +41,8 @@ namespace osu.Game.Rulesets.UI public abstract class DrawableRuleset : DrawableRuleset, IProvideCursor, ICanAttachKeyCounter where TObject : HitObject { - public override event Action OnNewResult; - - public override event Action OnRevertResult; + public override event Action NewResult; + public override event Action RevertResult; /// /// The selected variant. @@ -92,11 +94,8 @@ namespace osu.Game.Rulesets.UI protected IRulesetConfigManager Config { get; private set; } - /// - /// The mods which are to be applied. - /// [Cached(typeof(IReadOnlyList))] - protected readonly IReadOnlyList Mods; + protected override IReadOnlyList Mods { get; } private FrameStabilityContainer frameStabilityContainer; @@ -125,7 +124,11 @@ namespace osu.Game.Rulesets.UI RelativeSizeAxes = Axes.Both; KeyBindingInputManager = CreateInputManager(); - playfield = new Lazy(CreatePlayfield); + playfield = new Lazy(() => CreatePlayfield().With(p => + { + p.NewResult += (_, r) => NewResult?.Invoke(r); + p.RevertResult += (_, r) => RevertResult?.Invoke(r); + })); IsPaused.ValueChanged += paused => { @@ -183,7 +186,7 @@ namespace osu.Game.Rulesets.UI RegenerateAutoplay(); - loadObjects(cancellationToken); + loadObjects(cancellationToken ?? default); } public void RegenerateAutoplay() @@ -196,15 +199,15 @@ namespace osu.Game.Rulesets.UI /// /// Creates and adds drawable representations of hit objects to the play field. /// - private void loadObjects(CancellationToken? cancellationToken) + private void loadObjects(CancellationToken cancellationToken) { foreach (TObject h in Beatmap.HitObjects) { - cancellationToken?.ThrowIfCancellationRequested(); - addHitObject(h); + cancellationToken.ThrowIfCancellationRequested(); + AddHitObject(h); } - cancellationToken?.ThrowIfCancellationRequested(); + cancellationToken.ThrowIfCancellationRequested(); Playfield.PostProcess(); @@ -231,22 +234,58 @@ namespace osu.Game.Rulesets.UI } /// - /// Creates and adds the visual representation of a to this . + /// Adds a to this . /// - /// The to add the visual representation for. - private void addHitObject(TObject hitObject) + /// + /// This does not add the to the beatmap. + /// + /// The to add. + public void AddHitObject(TObject hitObject) { - var drawableObject = CreateDrawableRepresentation(hitObject); + var drawableRepresentation = CreateDrawableRepresentation(hitObject); - if (drawableObject == null) - return; - - drawableObject.OnNewResult += (_, r) => OnNewResult?.Invoke(r); - drawableObject.OnRevertResult += (_, r) => OnRevertResult?.Invoke(r); - - Playfield.Add(drawableObject); + // If a drawable representation exists, use it, otherwise assume the hitobject is being pooled. + if (drawableRepresentation != null) + Playfield.Add(drawableRepresentation); + else + Playfield.Add(GetLifetimeEntry(hitObject)); } + /// + /// Removes a from this . + /// + /// + /// This does not remove the from the beatmap. + /// + /// The to remove. + public bool RemoveHitObject(TObject hitObject) + { + var entry = GetLifetimeEntry(hitObject); + + // May have been newly-created by the above call - remove it anyway. + RemoveLifetimeEntry(hitObject); + + if (Playfield.Remove(entry)) + return true; + + // If the entry was not removed from the playfield, assume the hitobject is not being pooled and attempt a direct removal. + var drawableObject = Playfield.AllHitObjects.SingleOrDefault(d => d.HitObject == hitObject); + if (drawableObject != null) + return Playfield.Remove(drawableObject); + + return false; + } + + protected sealed override HitObjectLifetimeEntry CreateLifetimeEntry(HitObject hitObject) + { + if (!(hitObject is TObject tHitObject)) + throw new InvalidOperationException($"Unexpected hitobject type: {hitObject.GetType().ReadableName()}"); + + return CreateLifetimeEntry(tHitObject); + } + + protected virtual HitObjectLifetimeEntry CreateLifetimeEntry(TObject hitObject) => new HitObjectLifetimeEntry(hitObject); + public override void SetRecordTarget(Replay recordingReplay) { if (!(KeyBindingInputManager is IHasRecordingHandler recordingInputManager)) @@ -285,10 +324,15 @@ namespace osu.Game.Rulesets.UI } /// - /// Creates a DrawableHitObject from a HitObject. + /// Creates a to represent a . /// - /// The HitObject to make drawable. - /// The DrawableHitObject. + /// + /// If this method returns null, then this will assume the requested type is being pooled, + /// and will instead attempt to retrieve the s at the point they should become alive via pools registered through + /// or . + /// + /// The to represent. + /// The representing . public abstract DrawableHitObject CreateDrawableRepresentation(TObject h); public void Attach(KeyCounterDisplay keyCounter) => @@ -361,20 +405,20 @@ namespace osu.Game.Rulesets.UI /// Displays an interactive ruleset gameplay instance. /// /// This type is required only for adding non-generic type to the draw hierarchy. - /// Once IDrawable is a thing, this can also become an interface. /// /// + [Cached(typeof(DrawableRuleset))] public abstract class DrawableRuleset : CompositeDrawable { /// /// Invoked when a has been applied by a . /// - public abstract event Action OnNewResult; + public abstract event Action NewResult; /// /// Invoked when a is being reverted by a . /// - public abstract event Action OnRevertResult; + public abstract event Action RevertResult; /// /// Whether a replay is currently loaded. @@ -406,6 +450,11 @@ namespace osu.Game.Rulesets.UI /// public abstract IFrameStableClock FrameStableClock { get; } + /// + /// The mods which are to be applied. + /// + protected abstract IReadOnlyList Mods { get; } + /// ~ /// The associated ruleset. /// @@ -500,6 +549,99 @@ namespace osu.Game.Rulesets.UI /// Invoked when the user requests to pause while the resume overlay is active. /// public abstract void CancelResume(); + + private readonly Dictionary pools = new Dictionary(); + private readonly Dictionary lifetimeEntries = new Dictionary(); + + /// + /// Registers a default pool with this which is to be used whenever + /// representations are requested for the given type (via ). + /// + /// The number of s to be initially stored in the pool. + /// + /// The maximum number of s that can be stored in the pool. + /// If this limit is exceeded, every subsequent will be created anew instead of being retrieved from the pool, + /// until some of the existing s are returned to the pool. + /// + /// The type. + /// The receiver for s. + protected void RegisterPool(int initialSize, int? maximumSize = null) + where TObject : HitObject + where TDrawable : DrawableHitObject, new() + => RegisterPool(new DrawablePool(initialSize, maximumSize)); + + /// + /// Registers a custom pool with this which is to be used whenever + /// representations are requested for the given type (via ). + /// + /// The to register. + /// The type. + /// The receiver for s. + protected void RegisterPool([NotNull] DrawablePool pool) + where TObject : HitObject + where TDrawable : DrawableHitObject, new() + { + pools[typeof(TObject)] = pool; + AddInternal(pool); + } + + /// + /// Attempts to retrieve the poolable representation of a . + /// + /// The to retrieve the representation of. + /// The representing , or null if no poolable representation exists. + [CanBeNull] + public DrawableHitObject GetPooledDrawableRepresentation([NotNull] HitObject hitObject) + { + if (!pools.TryGetValue(hitObject.GetType(), out var pool)) + return null; + + return (DrawableHitObject)pool.Get(d => + { + var dho = (DrawableHitObject)d; + + // If this is the first time this DHO is being used (not loaded), then apply the DHO mods. + // This is done before Apply() so that the state is updated once when the hitobject is applied. + if (!dho.IsLoaded) + { + foreach (var m in Mods.OfType()) + m.ApplyToDrawableHitObjects(dho.Yield()); + } + + dho.Apply(hitObject, GetLifetimeEntry(hitObject)); + }); + } + + /// + /// Creates the for a given . + /// + /// + /// This may be overridden to provide custom lifetime control (e.g. via . + /// + /// The to create the entry for. + /// The . + [NotNull] + protected abstract HitObjectLifetimeEntry CreateLifetimeEntry([NotNull] HitObject hitObject); + + /// + /// Retrieves or creates the for a given . + /// + /// The to retrieve or create the for. + /// The for . + [NotNull] + protected HitObjectLifetimeEntry GetLifetimeEntry([NotNull] HitObject hitObject) + { + if (lifetimeEntries.TryGetValue(hitObject, out var entry)) + return entry; + + return lifetimeEntries[hitObject] = CreateLifetimeEntry(hitObject); + } + + /// + /// Removes the for a . + /// + /// The to remove the for. + internal void RemoveLifetimeEntry([NotNull] HitObject hitObject) => lifetimeEntries.Remove(hitObject); } public class BeatmapInvalidForRulesetException : ArgumentException diff --git a/osu.Game/Rulesets/UI/HitObjectContainer.cs b/osu.Game/Rulesets/UI/HitObjectContainer.cs index 4cadfa9ad4..25fb7ab9f3 100644 --- a/osu.Game/Rulesets/UI/HitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/HitObjectContainer.cs @@ -1,33 +1,150 @@ // 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.Diagnostics; using System.Linq; +using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Performance; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; namespace osu.Game.Rulesets.UI { public class HitObjectContainer : LifetimeManagementContainer { + /// + /// All currently in-use s. + /// public IEnumerable Objects => InternalChildren.Cast().OrderBy(h => h.HitObject.StartTime); + + /// + /// All currently in-use s that are alive. + /// + /// + /// If this uses pooled objects, this is equivalent to . + /// public IEnumerable AliveObjects => AliveInternalChildren.Cast().OrderBy(h => h.HitObject.StartTime); - private readonly Dictionary bindable, double timeAtAdd)> startTimeMap = new Dictionary, double)>(); + /// + /// Invoked when a is judged. + /// + public event Action NewResult; + + /// + /// Invoked when a judgement is reverted. + /// + public event Action RevertResult; + + /// + /// Invoked when a becomes used by a . + /// + /// + /// If this uses pooled objects, this represents the time when the s become alive. + /// + internal event Action HitObjectUsageBegan; + + /// + /// Invoked when a becomes unused by a . + /// + /// + /// If this uses pooled objects, this represents the time when the s become dead. + /// + internal event Action HitObjectUsageFinished; + + /// + /// The amount of time prior to the current time within which s should be considered alive. + /// + internal double PastLifetimeExtension { get; set; } + + /// + /// The amount of time after the current time within which s should be considered alive. + /// + internal double FutureLifetimeExtension { get; set; } + + private readonly Dictionary startTimeMap = new Dictionary(); + private readonly Dictionary drawableMap = new Dictionary(); + private readonly LifetimeEntryManager lifetimeManager = new LifetimeEntryManager(); + + [Resolved(CanBeNull = true)] + private DrawableRuleset drawableRuleset { get; set; } public HitObjectContainer() { RelativeSizeAxes = Axes.Both; + + lifetimeManager.EntryBecameAlive += entryBecameAlive; + lifetimeManager.EntryBecameDead += entryBecameDead; } + protected override void LoadAsyncComplete() + { + base.LoadAsyncComplete(); + + // Application of hitobjects during load() may have changed their start times, so ensure the correct sorting order. + SortInternal(); + } + + #region Pooling support + + public void Add(HitObjectLifetimeEntry entry) => lifetimeManager.AddEntry(entry); + + public bool Remove(HitObjectLifetimeEntry entry) => lifetimeManager.RemoveEntry(entry); + + private void entryBecameAlive(LifetimeEntry entry) => addDrawable((HitObjectLifetimeEntry)entry); + + private void entryBecameDead(LifetimeEntry entry) => removeDrawable((HitObjectLifetimeEntry)entry); + + private void addDrawable(HitObjectLifetimeEntry entry) + { + Debug.Assert(!drawableMap.ContainsKey(entry)); + + var drawable = drawableRuleset.GetPooledDrawableRepresentation(entry.HitObject); + if (drawable == null) + throw new InvalidOperationException($"A drawable representation could not be retrieved for hitobject type: {entry.HitObject.GetType().ReadableName()}."); + + drawable.OnNewResult += onNewResult; + drawable.OnRevertResult += onRevertResult; + + bindStartTime(drawable); + AddInternal(drawableMap[entry] = drawable, false); + + HitObjectUsageBegan?.Invoke(entry.HitObject); + } + + private void removeDrawable(HitObjectLifetimeEntry entry) + { + Debug.Assert(drawableMap.ContainsKey(entry)); + + var drawable = drawableMap[entry]; + drawable.OnNewResult -= onNewResult; + drawable.OnRevertResult -= onRevertResult; + drawable.OnKilled(); + + drawableMap.Remove(entry); + + unbindStartTime(drawable); + RemoveInternal(drawable); + + HitObjectUsageFinished?.Invoke(entry.HitObject); + } + + #endregion + + #region Non-pooling support + public virtual void Add(DrawableHitObject hitObject) { - // Added first for the comparer to remain ordered during AddInternal - startTimeMap[hitObject] = (hitObject.HitObject.StartTimeBindable.GetBoundCopy(), hitObject.HitObject.StartTime); - startTimeMap[hitObject].bindable.BindValueChanged(_ => onStartTimeChanged(hitObject)); + bindStartTime(hitObject); + + hitObject.OnNewResult += onNewResult; + hitObject.OnRevertResult += onRevertResult; AddInternal(hitObject); } @@ -37,54 +154,16 @@ namespace osu.Game.Rulesets.UI if (!RemoveInternal(hitObject)) return false; - // Removed last for the comparer to remain ordered during RemoveInternal - startTimeMap[hitObject].bindable.UnbindAll(); - startTimeMap.Remove(hitObject); + hitObject.OnNewResult -= onNewResult; + hitObject.OnRevertResult -= onRevertResult; + + unbindStartTime(hitObject); return true; } - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - unbindStartTimeMap(); - } - - public virtual void Clear(bool disposeChildren = true) - { - ClearInternal(disposeChildren); - unbindStartTimeMap(); - } - - private void unbindStartTimeMap() - { - foreach (var kvp in startTimeMap) - kvp.Value.bindable.UnbindAll(); - startTimeMap.Clear(); - } - public int IndexOf(DrawableHitObject hitObject) => IndexOfInternal(hitObject); - private void onStartTimeChanged(DrawableHitObject hitObject) - { - if (!RemoveInternal(hitObject)) - return; - - // Update the stored time, preserving the existing bindable - startTimeMap[hitObject] = (startTimeMap[hitObject].bindable, hitObject.HitObject.StartTime); - AddInternal(hitObject); - } - - protected override int Compare(Drawable x, Drawable y) - { - if (!(x is DrawableHitObject xObj) || !(y is DrawableHitObject yObj)) - return base.Compare(x, y); - - // Put earlier hitobjects towards the end of the list, so they handle input first - int i = startTimeMap[yObj].timeAtAdd.CompareTo(startTimeMap[xObj].timeAtAdd); - return i == 0 ? CompareReverseChildID(x, y) : i; - } - protected override void OnChildLifetimeBoundaryCrossed(LifetimeBoundaryCrossedEvent e) { if (!(e.Child is DrawableHitObject hitObject)) @@ -96,5 +175,71 @@ namespace osu.Game.Rulesets.UI hitObject.OnKilled(); } } + + #endregion + + public virtual void Clear(bool disposeChildren = true) + { + lifetimeManager.ClearEntries(); + + ClearInternal(disposeChildren); + unbindAllStartTimes(); + } + + protected override bool CheckChildrenLife() + { + bool aliveChanged = base.CheckChildrenLife(); + aliveChanged |= lifetimeManager.Update(Time.Current - PastLifetimeExtension, Time.Current + FutureLifetimeExtension); + return aliveChanged; + } + + private void onNewResult(DrawableHitObject d, JudgementResult r) => NewResult?.Invoke(d, r); + private void onRevertResult(DrawableHitObject d, JudgementResult r) => RevertResult?.Invoke(d, r); + + #region Comparator + StartTime tracking + + private void bindStartTime(DrawableHitObject hitObject) + { + var bindable = hitObject.StartTimeBindable.GetBoundCopy(); + + bindable.BindValueChanged(_ => + { + if (LoadState >= LoadState.Ready) + SortInternal(); + }); + + startTimeMap[hitObject] = bindable; + } + + private void unbindStartTime(DrawableHitObject hitObject) + { + startTimeMap[hitObject].UnbindAll(); + startTimeMap.Remove(hitObject); + } + + private void unbindAllStartTimes() + { + foreach (var kvp in startTimeMap) + kvp.Value.UnbindAll(); + startTimeMap.Clear(); + } + + protected override int Compare(Drawable x, Drawable y) + { + if (!(x is DrawableHitObject xObj) || !(y is DrawableHitObject yObj)) + return base.Compare(x, y); + + // Put earlier hitobjects towards the end of the list, so they handle input first + int i = yObj.HitObject.StartTime.CompareTo(xObj.HitObject.StartTime); + return i == 0 ? CompareReverseChildID(x, y) : i; + } + + #endregion + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + unbindAllStartTimes(); + } } } diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index d92ba210db..6747145d50 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -10,13 +10,25 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects; using osuTK; namespace osu.Game.Rulesets.UI { public abstract class Playfield : CompositeDrawable { + /// + /// Invoked when a is judged. + /// + public event Action NewResult; + + /// + /// Invoked when a judgement is reverted. + /// + public event Action RevertResult; + /// /// The contained in this Playfield. /// @@ -72,7 +84,13 @@ namespace osu.Game.Rulesets.UI { RelativeSizeAxes = Axes.Both; - hitObjectContainerLazy = new Lazy(CreateHitObjectContainer); + hitObjectContainerLazy = new Lazy(() => CreateHitObjectContainer().With(h => + { + h.NewResult += (d, r) => NewResult?.Invoke(d, r); + h.RevertResult += (d, r) => RevertResult?.Invoke(d, r); + h.HitObjectUsageBegan += o => HitObjectUsageBegan?.Invoke(o); + h.HitObjectUsageFinished += o => HitObjectUsageFinished?.Invoke(o); + })); } [Resolved(CanBeNull = true)] @@ -101,13 +119,73 @@ namespace osu.Game.Rulesets.UI /// Adds a DrawableHitObject to this Playfield. /// /// The DrawableHitObject to add. - public virtual void Add(DrawableHitObject h) => HitObjectContainer.Add(h); + public virtual void Add(DrawableHitObject h) + { + HitObjectContainer.Add(h); + OnHitObjectAdded(h.HitObject); + } /// /// Remove a DrawableHitObject from this Playfield. /// /// The DrawableHitObject to remove. - public virtual bool Remove(DrawableHitObject h) => HitObjectContainer.Remove(h); + public virtual bool Remove(DrawableHitObject h) + { + if (!HitObjectContainer.Remove(h)) + return false; + + OnHitObjectRemoved(h.HitObject); + return false; + } + + /// + /// Adds a for a pooled to this . + /// + /// The controlling the lifetime of the . + public virtual void Add(HitObjectLifetimeEntry entry) + { + HitObjectContainer.Add(entry); + lifetimeEntryMap[entry.HitObject] = entry; + OnHitObjectAdded(entry.HitObject); + } + + /// + /// Removes a for a pooled from this . + /// + /// The controlling the lifetime of the . + /// Whether the was successfully removed. + public virtual bool Remove(HitObjectLifetimeEntry entry) + { + if (HitObjectContainer.Remove(entry)) + { + lifetimeEntryMap.Remove(entry.HitObject); + OnHitObjectRemoved(entry.HitObject); + return true; + } + + bool removedFromNested = false; + + if (nestedPlayfields.IsValueCreated) + removedFromNested = nestedPlayfields.Value.Any(p => p.Remove(entry)); + + return removedFromNested; + } + + /// + /// Invoked when a is added to this . + /// + /// The added . + protected virtual void OnHitObjectAdded(HitObject hitObject) + { + } + + /// + /// Invoked when a is removed from this . + /// + /// The removed . + protected virtual void OnHitObjectRemoved(HitObject hitObject) + { + } /// /// The cursor currently being used by this . May be null if no cursor is provided. @@ -131,6 +209,12 @@ namespace osu.Game.Rulesets.UI protected void AddNested(Playfield otherPlayfield) { otherPlayfield.DisplayJudgements.BindTo(DisplayJudgements); + + otherPlayfield.NewResult += (d, r) => NewResult?.Invoke(d, r); + otherPlayfield.RevertResult += (d, r) => RevertResult?.Invoke(d, r); + otherPlayfield.HitObjectUsageBegan += h => HitObjectUsageBegan?.Invoke(h); + otherPlayfield.HitObjectUsageFinished += h => HitObjectUsageFinished?.Invoke(h); + nestedPlayfields.Value.Add(otherPlayfield); } @@ -162,6 +246,99 @@ namespace osu.Game.Rulesets.UI /// protected virtual HitObjectContainer CreateHitObjectContainer() => new HitObjectContainer(); + #region Editor logic + + /// + /// Invoked when a becomes used by a . + /// + /// + /// If this uses pooled objects, this represents the time when the s become alive. + /// + internal event Action HitObjectUsageBegan; + + /// + /// Invoked when a becomes unused by a . + /// + /// + /// If this uses pooled objects, this represents the time when the s become dead. + /// + internal event Action HitObjectUsageFinished; + + private readonly Dictionary lifetimeEntryMap = new Dictionary(); + + /// + /// Sets whether to keep a given always alive within this or any nested . + /// + /// The to set. + /// Whether to keep always alive. + internal void SetKeepAlive(HitObject hitObject, bool keepAlive) + { + if (lifetimeEntryMap.TryGetValue(hitObject, out var entry)) + { + entry.KeepAlive = keepAlive; + return; + } + + if (!nestedPlayfields.IsValueCreated) + return; + + foreach (var p in nestedPlayfields.Value) + p.SetKeepAlive(hitObject, keepAlive); + } + + /// + /// Keeps all s alive within this and all nested s. + /// + internal void KeepAllAlive() + { + foreach (var (_, entry) in lifetimeEntryMap) + entry.KeepAlive = true; + + if (!nestedPlayfields.IsValueCreated) + return; + + foreach (var p in nestedPlayfields.Value) + p.KeepAllAlive(); + } + + /// + /// The amount of time prior to the current time within which s should be considered alive. + /// + internal double PastLifetimeExtension + { + get => HitObjectContainer.PastLifetimeExtension; + set + { + HitObjectContainer.PastLifetimeExtension = value; + + if (!nestedPlayfields.IsValueCreated) + return; + + foreach (var nested in nestedPlayfields.Value) + nested.PastLifetimeExtension = value; + } + } + + /// + /// The amount of time after the current time within which s should be considered alive. + /// + internal double FutureLifetimeExtension + { + get => HitObjectContainer.FutureLifetimeExtension; + set + { + HitObjectContainer.FutureLifetimeExtension = value; + + if (!nestedPlayfields.IsValueCreated) + return; + + foreach (var nested in nestedPlayfields.Value) + nested.FutureLifetimeExtension = value; + } + } + + #endregion + public class InvisibleCursorContainer : GameplayCursorContainer { protected override Drawable CreateCursor() => new InvisibleCursor(); diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index b67f6a6ba6..3229719d5a 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using System.Collections.Specialized; using System.Diagnostics; using System.Linq; @@ -34,6 +35,8 @@ namespace osu.Game.Screens.Edit.Compose.Components protected SelectionHandler SelectionHandler { get; private set; } + protected readonly HitObjectComposer Composer; + [Resolved(CanBeNull = true)] private IEditorChangeHandler changeHandler { get; set; } @@ -44,12 +47,15 @@ namespace osu.Game.Screens.Edit.Compose.Components protected EditorBeatmap Beatmap { get; private set; } private readonly BindableList selectedHitObjects = new BindableList(); + private readonly Dictionary blueprintMap = new Dictionary(); [Resolved(canBeNull: true)] private IPositionSnapProvider snapProvider { get; set; } - protected BlueprintContainer() + protected BlueprintContainer(HitObjectComposer composer) { + Composer = composer; + RelativeSizeAxes = Axes.Both; } @@ -68,8 +74,12 @@ namespace osu.Game.Screens.Edit.Compose.Components DragBox.CreateProxy().With(p => p.Depth = float.MinValue) }); - foreach (var obj in Beatmap.HitObjects) - AddBlueprintFor(obj); + // 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) => @@ -94,8 +104,18 @@ namespace osu.Game.Screens.Edit.Compose.Components { base.LoadComplete(); - Beatmap.HitObjectAdded += AddBlueprintFor; + 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 CreateSelectionBlueprintContainer() => @@ -247,29 +267,17 @@ namespace osu.Game.Screens.Edit.Compose.Components #region Blueprint Addition/Removal - private void removeBlueprintFor(HitObject hitObject) + private void addBlueprintFor(HitObject hitObject) { - var blueprint = SelectionBlueprints.SingleOrDefault(m => m.HitObject == hitObject); - if (blueprint == null) + if (blueprintMap.ContainsKey(hitObject)) return; - blueprint.Deselect(); - - blueprint.Selected -= onBlueprintSelected; - blueprint.Deselected -= onBlueprintDeselected; - - SelectionBlueprints.Remove(blueprint); - - if (movementBlueprint == blueprint) - finishSelectionMovement(); - } - - protected virtual void AddBlueprintFor(HitObject hitObject) - { var blueprint = CreateBlueprintFor(hitObject); if (blueprint == null) return; + blueprintMap[hitObject] = blueprint; + blueprint.Selected += onBlueprintSelected; blueprint.Deselected += onBlueprintDeselected; @@ -277,6 +285,41 @@ namespace osu.Game.Screens.Edit.Compose.Components blueprint.Select(); SelectionBlueprints.Add(blueprint); + + OnBlueprintAdded(hitObject); + } + + private void removeBlueprintFor(HitObject hitObject) + { + if (!blueprintMap.Remove(hitObject, out var blueprint)) + return; + + blueprint.Deselect(); + blueprint.Selected -= onBlueprintSelected; + blueprint.Deselected -= onBlueprintDeselected; + + SelectionBlueprints.Remove(blueprint); + + if (movementBlueprint == blueprint) + finishSelectionMovement(); + + OnBlueprintRemoved(hitObject); + } + + /// + /// Called after a blueprint has been added. + /// + /// The for which the blueprint has been added. + protected virtual void OnBlueprintAdded(HitObject hitObject) + { + } + + /// + /// Called after a blueprint has been removed. + /// + /// The for which the blueprint has been removed. + protected virtual void OnBlueprintRemoved(HitObject hitObject) + { } #endregion @@ -349,7 +392,13 @@ namespace osu.Game.Screens.Edit.Compose.Components /// /// Selects all s. /// - private void selectAll() => SelectionBlueprints.ToList().ForEach(m => m.Select()); + private void selectAll() + { + Composer.Playfield.KeepAllAlive(); + + // Scheduled to allow the change in lifetime to take place. + Schedule(() => SelectionBlueprints.ToList().ForEach(m => m.Select())); + } /// /// Deselects all selected s. @@ -360,12 +409,16 @@ namespace osu.Game.Screens.Edit.Compose.Components { SelectionHandler.HandleSelected(blueprint); SelectionBlueprints.ChangeChildDepth(blueprint, 1); + + Composer.Playfield.SetKeepAlive(blueprint.HitObject, true); } private void onBlueprintDeselected(SelectionBlueprint blueprint) { SelectionHandler.HandleDeselected(blueprint); SelectionBlueprints.ChangeChildDepth(blueprint, 0); + + Composer.Playfield.SetKeepAlive(blueprint.HitObject, false); } #endregion @@ -456,9 +509,15 @@ namespace osu.Game.Screens.Edit.Compose.Components if (Beatmap != null) { - Beatmap.HitObjectAdded -= AddBlueprintFor; + Beatmap.HitObjectAdded -= addBlueprintFor; Beatmap.HitObjectRemoved -= removeBlueprintFor; } + + if (Composer != null) + { + Composer.Playfield.HitObjectUsageBegan -= addBlueprintFor; + Composer.Playfield.HitObjectUsageFinished -= removeBlueprintFor; + } } } } diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index 1527d20f54..0d2e2360b1 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -27,23 +27,16 @@ namespace osu.Game.Screens.Edit.Compose.Components /// public class ComposeBlueprintContainer : BlueprintContainer { - [Resolved] - private HitObjectComposer composer { get; set; } - - private PlacementBlueprint currentPlacement; + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; private readonly Container placementBlueprintContainer; - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; - + private PlacementBlueprint currentPlacement; private InputManager inputManager; - private readonly IEnumerable drawableHitObjects; - - public ComposeBlueprintContainer(IEnumerable drawableHitObjects) + public ComposeBlueprintContainer(HitObjectComposer composer) + : base(composer) { - this.drawableHitObjects = drawableHitObjects; - placementBlueprintContainer = new Container { RelativeSizeAxes = Axes.Both @@ -162,7 +155,7 @@ namespace osu.Game.Screens.Edit.Compose.Components private void updatePlacementPosition() { - var snapResult = composer.SnapScreenSpacePositionToValidTime(inputManager.CurrentState.Mouse.Position); + var snapResult = Composer.SnapScreenSpacePositionToValidTime(inputManager.CurrentState.Mouse.Position); currentPlacement.UpdatePosition(snapResult); } @@ -173,7 +166,7 @@ namespace osu.Game.Screens.Edit.Compose.Components { base.Update(); - if (composer.CursorInPlacementArea) + if (Composer.CursorInPlacementArea) createPlacement(); else if (currentPlacement?.PlacementActive == false) removePlacement(); @@ -186,7 +179,7 @@ namespace osu.Game.Screens.Edit.Compose.Components protected sealed override SelectionBlueprint CreateBlueprintFor(HitObject hitObject) { - var drawable = drawableHitObjects.FirstOrDefault(d => d.HitObject == hitObject); + var drawable = Composer.HitObjects.FirstOrDefault(d => d.HitObject == hitObject); if (drawable == null) return null; @@ -196,11 +189,11 @@ namespace osu.Game.Screens.Edit.Compose.Components public virtual OverlaySelectionBlueprint CreateBlueprintFor(DrawableHitObject hitObject) => null; - protected override void AddBlueprintFor(HitObject hitObject) + protected override void OnBlueprintAdded(HitObject hitObject) { - refreshTool(); + base.OnBlueprintAdded(hitObject); - base.AddBlueprintFor(hitObject); + refreshTool(); // on successful placement, the new combo button should be reset as this is the most common user interaction. if (Beatmap.SelectedHitObjects.Count == 0) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs index 7233faa955..f6675902fc 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs @@ -219,6 +219,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline [Resolved] private IBeatSnapProvider beatSnapProvider { get; set; } + /// + /// The total amount of time visible on the timeline. + /// + public double VisibleRange => track.Length / Zoom; + public SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) => new SnapResult(screenSpacePosition, beatSnapProvider.SnapTime(getTimeFromPosition(Content.ToLocalSpace(screenSpacePosition)))); diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs index 10913a8bb9..0271b2def9 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs @@ -26,12 +26,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private EditorBeatmap beatmap { get; set; } private DragEvent lastDragEvent; - private Bindable placement; - private SelectionBlueprint placementBlueprint; - public TimelineBlueprintContainer() + public TimelineBlueprintContainer(HitObjectComposer composer) + : base(composer) { RelativeSizeAxes = Axes.Both; Anchor = Anchor.Centre; @@ -97,6 +96,12 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline if (lastDragEvent != null) OnDrag(lastDragEvent); + if (Composer != null) + { + Composer.Playfield.PastLifetimeExtension = timeline.VisibleRange / 2; + Composer.Playfield.FutureLifetimeExtension = timeline.VisibleRange / 2; + } + base.Update(); } diff --git a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs index 5282b4d998..d9948aa23c 100644 --- a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs +++ b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs @@ -53,6 +53,6 @@ namespace osu.Game.Screens.Edit.Compose return beatmapSkinProvider.WithChild(rulesetSkinProvider.WithChild(composer)); } - protected override Drawable CreateTimelineContent() => composer == null ? base.CreateTimelineContent() : new TimelineBlueprintContainer(); + protected override Drawable CreateTimelineContent() => composer == null ? base.CreateTimelineContent() : new TimelineBlueprintContainer(composer); } } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index f9af1818d0..ee4f835c6f 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -261,14 +261,14 @@ namespace osu.Game.Screens.Play // bind clock into components that require it DrawableRuleset.IsPaused.BindTo(GameplayClockContainer.IsPaused); - DrawableRuleset.OnNewResult += r => + DrawableRuleset.NewResult += r => { HealthProcessor.ApplyResult(r); ScoreProcessor.ApplyResult(r); gameplayBeatmap.ApplyResult(r); }; - DrawableRuleset.OnRevertResult += r => + DrawableRuleset.RevertResult += r => { HealthProcessor.RevertResult(r); ScoreProcessor.RevertResult(r); diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 405fb1a6ca..1850ee3488 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -26,7 +26,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 099ecd8319..2ac23f1503 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -88,7 +88,7 @@ - +