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 @@
-
+