1
0
mirror of https://github.com/ppy/osu.git synced 2024-09-21 21:27:24 +08:00

Merge branch 'master' into fix-perform-from-menu-overlay-hiding

This commit is contained in:
Bartłomiej Dach 2020-11-13 19:11:53 +01:00 committed by GitHub
commit 403415147d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 1140 additions and 210 deletions

View File

@ -52,6 +52,6 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.1030.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2020.1030.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.1111.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2020.1113.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Edit.Blueprints; using osu.Game.Rulesets.Mania.Edit.Blueprints;
using osu.Game.Rulesets.Mania.Objects.Drawables; using osu.Game.Rulesets.Mania.Objects.Drawables;
@ -12,8 +11,8 @@ namespace osu.Game.Rulesets.Mania.Edit
{ {
public class ManiaBlueprintContainer : ComposeBlueprintContainer public class ManiaBlueprintContainer : ComposeBlueprintContainer
{ {
public ManiaBlueprintContainer(IEnumerable<DrawableHitObject> drawableHitObjects) public ManiaBlueprintContainer(HitObjectComposer composer)
: base(drawableHitObjects) : base(composer)
{ {
} }

View File

@ -13,7 +13,6 @@ using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces;
using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Screens.Edit.Compose.Components; using osu.Game.Screens.Edit.Compose.Components;
@ -89,8 +88,8 @@ namespace osu.Game.Rulesets.Mania.Edit
return drawableRuleset; return drawableRuleset;
} }
protected override ComposeBlueprintContainer CreateBlueprintContainer(IEnumerable<DrawableHitObject> hitObjects) protected override ComposeBlueprintContainer CreateBlueprintContainer()
=> new ManiaBlueprintContainer(hitObjects); => new ManiaBlueprintContainer(this);
protected override IReadOnlyList<HitObjectCompositionTool> CompositionTools => new HitObjectCompositionTool[] protected override IReadOnlyList<HitObjectCompositionTool> CompositionTools => new HitObjectCompositionTool[]
{ {

View File

@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
Position = new Vector2(128, 128), Position = new Vector2(128, 128),
ComboIndex = 1, ComboIndex = 1,
}))); }), null));
} }
private HitCircle prepareObject(HitCircle circle) private HitCircle prepareObject(HitCircle circle)

View File

@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Osu.Tests
new Vector2(300, 0), new Vector2(300, 0),
}), }),
RepeatCount = 1 RepeatCount = 1
}))); }), null));
} }
private Slider prepareObject(Slider slider) private Slider prepareObject(Slider slider)

View File

@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Osu.Tests
Position = new Vector2(256, 192), Position = new Vector2(256, 192),
ComboIndex = 1, ComboIndex = 1,
Duration = 1000, Duration = 1000,
}))); }), null));
} }
private Spinner prepareObject(Spinner circle) private Spinner prepareObject(Spinner circle)

View File

@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles; using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles;
@ -14,8 +13,8 @@ namespace osu.Game.Rulesets.Osu.Edit
{ {
public class OsuBlueprintContainer : ComposeBlueprintContainer public class OsuBlueprintContainer : ComposeBlueprintContainer
{ {
public OsuBlueprintContainer(IEnumerable<DrawableHitObject> drawableHitObjects) public OsuBlueprintContainer(HitObjectComposer composer)
: base(drawableHitObjects) : base(composer)
{ {
} }

View File

@ -16,7 +16,6 @@ using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Screens.Edit.Components.TernaryButtons; using osu.Game.Screens.Edit.Components.TernaryButtons;
@ -80,8 +79,8 @@ namespace osu.Game.Rulesets.Osu.Edit
updateDistanceSnapGrid(); updateDistanceSnapGrid();
} }
protected override ComposeBlueprintContainer CreateBlueprintContainer(IEnumerable<DrawableHitObject> hitObjects) protected override ComposeBlueprintContainer CreateBlueprintContainer()
=> new OsuBlueprintContainer(hitObjects); => new OsuBlueprintContainer(this);
private DistanceSnapGrid distanceSnapGrid; private DistanceSnapGrid distanceSnapGrid;
private Container distanceSnapGridContainer; private Container distanceSnapGridContainer;

View File

@ -51,9 +51,15 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
var drawableOsuObject = (DrawableOsuHitObject)drawableObject; var drawableOsuObject = (DrawableOsuHitObject)drawableObject;
state.BindTo(drawableObject.State); state.BindTo(drawableObject.State);
state.BindValueChanged(updateState, true);
accentColour.BindTo(drawableObject.AccentColour); accentColour.BindTo(drawableObject.AccentColour);
indexInCurrentCombo.BindTo(drawableOsuObject.IndexInCurrentComboBindable);
}
protected override void LoadComplete()
{
base.LoadComplete();
state.BindValueChanged(updateState, true);
accentColour.BindValueChanged(colour => accentColour.BindValueChanged(colour =>
{ {
explode.Colour = colour.NewValue; explode.Colour = colour.NewValue;
@ -61,7 +67,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
circle.Colour = colour.NewValue; circle.Colour = colour.NewValue;
}, true); }, true);
indexInCurrentCombo.BindTo(drawableOsuObject.IndexInCurrentComboBindable);
indexInCurrentCombo.BindValueChanged(index => number.Text = (index.NewValue + 1).ToString(), true); indexInCurrentCombo.BindValueChanged(index => number.Text = (index.NewValue + 1).ToString(), true);
} }

View File

@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Taiko.Edit.Blueprints; using osu.Game.Rulesets.Taiko.Edit.Blueprints;
@ -11,8 +10,8 @@ namespace osu.Game.Rulesets.Taiko.Edit
{ {
public class TaikoBlueprintContainer : ComposeBlueprintContainer public class TaikoBlueprintContainer : ComposeBlueprintContainer
{ {
public TaikoBlueprintContainer(IEnumerable<DrawableHitObject> hitObjects) public TaikoBlueprintContainer(HitObjectComposer composer)
: base(hitObjects) : base(composer)
{ {
} }

View File

@ -4,7 +4,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Screens.Edit.Compose.Components; using osu.Game.Screens.Edit.Compose.Components;
@ -24,7 +23,7 @@ namespace osu.Game.Rulesets.Taiko.Edit
new SwellCompositionTool() new SwellCompositionTool()
}; };
protected override ComposeBlueprintContainer CreateBlueprintContainer(IEnumerable<DrawableHitObject> hitObjects) protected override ComposeBlueprintContainer CreateBlueprintContainer()
=> new TaikoBlueprintContainer(hitObjects); => new TaikoBlueprintContainer(this);
} }
} }

View File

@ -10,7 +10,7 @@ namespace osu.Game.Tests.Visual.Editing
[TestFixture] [TestFixture]
public class TestSceneTimelineBlueprintContainer : TimelineTestScene public class TestSceneTimelineBlueprintContainer : TimelineTestScene
{ {
public override Drawable CreateTestComponent() => new TimelineBlueprintContainer(); public override Drawable CreateTestComponent() => new TimelineBlueprintContainer(Composer);
protected override void LoadComplete() protected override void LoadComplete()
{ {

View File

@ -21,21 +21,25 @@ namespace osu.Game.Tests.Visual.Editing
{ {
protected TimelineArea TimelineArea { get; private set; } protected TimelineArea TimelineArea { get; private set; }
protected HitObjectComposer Composer { get; private set; }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(AudioManager audio) private void load(AudioManager audio)
{ {
Beatmap.Value = new WaveformTestBeatmap(audio); Beatmap.Value = new WaveformTestBeatmap(audio);
var playable = Beatmap.Value.GetPlayableBeatmap(Beatmap.Value.BeatmapInfo.Ruleset); var playable = Beatmap.Value.GetPlayableBeatmap(Beatmap.Value.BeatmapInfo.Ruleset);
var editorBeatmap = new EditorBeatmap(playable); var editorBeatmap = new EditorBeatmap(playable);
Dependencies.Cache(editorBeatmap); Dependencies.Cache(editorBeatmap);
Dependencies.CacheAs<IBeatSnapProvider>(editorBeatmap); Dependencies.CacheAs<IBeatSnapProvider>(editorBeatmap);
Composer = playable.BeatmapInfo.Ruleset.CreateInstance().CreateHitObjectComposer().With(d => d.Alpha = 0);
AddRange(new Drawable[] AddRange(new Drawable[]
{ {
editorBeatmap, editorBeatmap,
Composer,
new FillFlowContainer new FillFlowContainer
{ {
AutoSizeAxes = Axes.Both, AutoSizeAxes = Axes.Both,

View File

@ -6,6 +6,7 @@ using System.Linq;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
namespace osu.Game.Tests.Visual.Gameplay namespace osu.Game.Tests.Visual.Gameplay
@ -21,8 +22,14 @@ namespace osu.Game.Tests.Visual.Gameplay
protected override void AddCheckSteps() protected override void AddCheckSteps()
{ {
AddUntilStep("wait for fail", () => Player.HasFailed); AddUntilStep("wait for fail", () => Player.HasFailed);
AddUntilStep("wait for multiple judged objects", () => ((FailPlayer)Player).DrawableRuleset.Playfield.AllHitObjects.Count(h => h.AllJudged) > 1); AddUntilStep("wait for multiple judgements", () => ((FailPlayer)Player).ScoreProcessor.JudgedHits > 1);
AddAssert("total judgements == 1", () => ((FailPlayer)Player).HealthProcessor.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 private class FailPlayer : TestPlayer

View File

@ -0,0 +1,247 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using 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<DrawableTestHitObject>().SingleOrDefault()?.HitObject == drawableRuleset.Beatmap.HitObjects[0]);
AddStep("get DHO", () => firstObject = this.ChildrenOfType<DrawableTestHitObject>().Single());
AddStep("fast forward to second object", () => clock.CurrentTime = drawableRuleset.Beatmap.HitObjects[1].StartTime);
AddUntilStep("second object shown", () => this.ChildrenOfType<DrawableTestHitObject>().SingleOrDefault()?.HitObject == drawableRuleset.Beatmap.HitObjects[1]);
AddAssert("DHO reused", () => this.ChildrenOfType<DrawableTestHitObject>().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<DrawableTestHitObject>().Count() == 2);
AddAssert("DHOs have different hitobjects",
() => this.ChildrenOfType<DrawableTestHitObject>().ElementAt(0).HitObject != this.ChildrenOfType<DrawableTestHitObject>().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<DrawableTestHitObject>().Any());
AddUntilStep("no DHOs shown", () => !this.ChildrenOfType<DrawableTestHitObject>().Any());
}
private void createTest(IBeatmap beatmap, int poolSize, Func<IFrameBasedClock> 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<Mod> GetModsFor(ModType type) => throw new NotImplementedException();
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod> 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<TestHitObject>
{
public int PoolSize;
public TestDrawablePoolingRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods = null)
: base(ruleset, beatmap, mods)
{
}
[BackgroundDependencyLoader]
private void load()
{
RegisterPool<TestHitObject, DrawableTestHitObject>(PoolSize);
}
protected override HitObjectLifetimeEntry CreateLifetimeEntry(TestHitObject hitObject) => new TestHitObjectLifetimeEntry(hitObject);
public override DrawableHitObject<TestHitObject> 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<TestHitObject>
{
public TestBeatmapConverter(IBeatmap beatmap, Ruleset ruleset)
: base(beatmap, ruleset)
{
}
public override bool CanConvert() => true;
protected override IEnumerable<TestHitObject> 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<TestHitObject>
{
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
}
}

View File

@ -8,10 +8,9 @@ using osu.Framework.Audio;
using osu.Framework.Audio.Track; using osu.Framework.Audio.Track;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Formats;
using osu.Game.IO;
using osu.Game.IO.Archives; 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.Beatmaps;
using osu.Game.Tests.Resources; using osu.Game.Tests.Resources;
@ -25,8 +24,8 @@ namespace osu.Game.Tests
private readonly Beatmap beatmap; private readonly Beatmap beatmap;
private readonly ITrackStore trackStore; private readonly ITrackStore trackStore;
public WaveformTestBeatmap(AudioManager audioManager) public WaveformTestBeatmap(AudioManager audioManager, RulesetInfo rulesetInfo = null)
: this(audioManager, new WaveformBeatmap()) : 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)); 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<Beatmap>(beatmapReader).Decode(beatmapReader);
}
}
} }
} }

View File

@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
@ -65,17 +64,13 @@ namespace osu.Game.Rulesets.Edit
private void addHitObject(HitObject hitObject) private void addHitObject(HitObject hitObject)
{ {
var drawableObject = drawableRuleset.CreateDrawableRepresentation((TObject)hitObject); drawableRuleset.AddHitObject((TObject)hitObject);
drawableRuleset.Playfield.Add(drawableObject);
drawableRuleset.Playfield.PostProcess(); drawableRuleset.Playfield.PostProcess();
} }
private void removeHitObject(HitObject hitObject) private void removeHitObject(HitObject hitObject)
{ {
var drawableObject = Playfield.AllHitObjects.Single(d => d.HitObject == hitObject); drawableRuleset.RemoveHitObject((TObject)hitObject);
drawableRuleset.Playfield.Remove(drawableObject);
drawableRuleset.Playfield.PostProcess(); drawableRuleset.Playfield.PostProcess();
} }

View File

@ -104,7 +104,7 @@ namespace osu.Game.Rulesets.Edit
drawableRulesetWrapper, drawableRulesetWrapper,
// layers above playfield // layers above playfield
drawableRulesetWrapper.CreatePlayfieldAdjustmentContainer() drawableRulesetWrapper.CreatePlayfieldAdjustmentContainer()
.WithChild(BlueprintContainer = CreateBlueprintContainer(HitObjects)) .WithChild(BlueprintContainer = CreateBlueprintContainer())
} }
}, },
new FillFlowContainer new FillFlowContainer
@ -182,9 +182,8 @@ namespace osu.Game.Rulesets.Edit
/// <summary> /// <summary>
/// Construct a relevant blueprint container. This will manage hitobject selection/placement input handling and display logic. /// Construct a relevant blueprint container. This will manage hitobject selection/placement input handling and display logic.
/// </summary> /// </summary>
/// <param name="hitObjects">A live collection of all <see cref="DrawableHitObject"/>s in the editor beatmap.</param> protected virtual ComposeBlueprintContainer CreateBlueprintContainer()
protected virtual ComposeBlueprintContainer CreateBlueprintContainer(IEnumerable<DrawableHitObject> hitObjects) => new ComposeBlueprintContainer(this);
=> new ComposeBlueprintContainer(hitObjects);
/// <summary> /// <summary>
/// Construct a drawable ruleset for the provided ruleset. /// Construct a drawable ruleset for the provided ruleset.

View File

@ -10,6 +10,7 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Primitives;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Framework.Threading; using osu.Framework.Threading;
@ -19,6 +20,7 @@ using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Skinning; using osu.Game.Skinning;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Rulesets.UI;
using osuTK.Graphics; using osuTK.Graphics;
namespace osu.Game.Rulesets.Objects.Drawables namespace osu.Game.Rulesets.Objects.Drawables
@ -108,7 +110,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
/// </remarks> /// </remarks>
protected virtual float SamplePlaybackPosition => 0.5f; protected virtual float SamplePlaybackPosition => 0.5f;
private readonly Bindable<double> startTimeBindable = new Bindable<double>(); public readonly Bindable<double> StartTimeBindable = new Bindable<double>();
private readonly BindableList<HitSampleInfo> samplesBindable = new BindableList<HitSampleInfo>(); private readonly BindableList<HitSampleInfo> samplesBindable = new BindableList<HitSampleInfo>();
private readonly Bindable<bool> userPositionalHitSounds = new Bindable<bool>(); private readonly Bindable<bool> userPositionalHitSounds = new Bindable<bool>();
private readonly Bindable<int> comboIndexBindable = new Bindable<int>(); private readonly Bindable<int> comboIndexBindable = new Bindable<int>();
@ -128,6 +130,17 @@ namespace osu.Game.Rulesets.Objects.Drawables
/// </summary> /// </summary>
private bool hasHitObjectApplied; private bool hasHitObjectApplied;
/// <summary>
/// The <see cref="HitObjectLifetimeEntry"/> controlling the lifetime of the currently-attached <see cref="HitObject"/>.
/// </summary>
[CanBeNull]
private HitObjectLifetimeEntry lifetimeEntry;
[Resolved(CanBeNull = true)]
private DrawableRuleset drawableRuleset { get; set; }
private Container<PausableSkinnableSound> samplesContainer;
/// <summary> /// <summary>
/// Creates a new <see cref="DrawableHitObject"/>. /// Creates a new <see cref="DrawableHitObject"/>.
/// </summary> /// </summary>
@ -144,6 +157,9 @@ namespace osu.Game.Rulesets.Objects.Drawables
private void load(OsuConfigManager config) private void load(OsuConfigManager config)
{ {
config.BindWith(OsuSetting.PositionalHitSounds, userPositionalHitSounds); config.BindWith(OsuSetting.PositionalHitSounds, userPositionalHitSounds);
// Explicit non-virtual function call.
base.AddInternal(samplesContainer = new Container<PausableSkinnableSound> { RelativeSizeAxes = Axes.Both });
} }
protected override void LoadAsyncComplete() protected override void LoadAsyncComplete()
@ -151,14 +167,13 @@ namespace osu.Game.Rulesets.Objects.Drawables
base.LoadAsyncComplete(); base.LoadAsyncComplete();
if (HitObject != null) if (HitObject != null)
Apply(HitObject); Apply(HitObject, lifetimeEntry);
} }
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.LoadComplete(); base.LoadComplete();
startTimeBindable.BindValueChanged(_ => updateState(State.Value, true));
comboIndexBindable.BindValueChanged(_ => updateComboColour(), true); comboIndexBindable.BindValueChanged(_ => updateComboColour(), true);
updateState(ArmedState.Idle, true); updateState(ArmedState.Idle, true);
@ -168,19 +183,38 @@ namespace osu.Game.Rulesets.Objects.Drawables
/// Applies a new <see cref="HitObject"/> to be represented by this <see cref="DrawableHitObject"/>. /// Applies a new <see cref="HitObject"/> to be represented by this <see cref="DrawableHitObject"/>.
/// </summary> /// </summary>
/// <param name="hitObject">The <see cref="HitObject"/> to apply.</param> /// <param name="hitObject">The <see cref="HitObject"/> to apply.</param>
public void Apply(HitObject hitObject) /// <param name="lifetimeEntry">The <see cref="HitObjectLifetimeEntry"/> controlling the lifetime of <paramref name="hitObject"/>.</param>
public void Apply([NotNull] HitObject hitObject, [CanBeNull] HitObjectLifetimeEntry lifetimeEntry)
{ {
free(); free();
HitObject = hitObject ?? throw new InvalidOperationException($"Cannot apply a null {nameof(HitObject)}."); 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. // Ensure this DHO has a result.
Result ??= CreateResult(HitObject.CreateJudgement()) Result ??= CreateResult(HitObject.CreateJudgement())
?? throw new InvalidOperationException($"{GetType().ReadableName()} must provide a {nameof(JudgementResult)} through {nameof(CreateResult)}."); ?? 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) 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.OnNewResult += onNewResult;
drawableNested.OnRevertResult += onRevertResult; drawableNested.OnRevertResult += onRevertResult;
@ -188,9 +222,13 @@ namespace osu.Game.Rulesets.Objects.Drawables
nestedHitObjects.Value.Add(drawableNested); nestedHitObjects.Value.Add(drawableNested);
AddNestedHitObject(drawableNested); AddNestedHitObject(drawableNested);
drawableNested.OnParentReceived(this);
} }
startTimeBindable.BindTo(HitObject.StartTimeBindable); StartTimeBindable.BindTo(HitObject.StartTimeBindable);
StartTimeBindable.BindValueChanged(onStartTimeChanged);
if (HitObject is IHasComboInformation combo) if (HitObject is IHasComboInformation combo)
comboIndexBindable.BindTo(combo.ComboIndexBindable); comboIndexBindable.BindTo(combo.ComboIndexBindable);
@ -217,12 +255,14 @@ namespace osu.Game.Rulesets.Objects.Drawables
if (!hasHitObjectApplied) if (!hasHitObjectApplied)
return; return;
startTimeBindable.UnbindFrom(HitObject.StartTimeBindable); StartTimeBindable.UnbindFrom(HitObject.StartTimeBindable);
if (HitObject is IHasComboInformation combo) if (HitObject is IHasComboInformation combo)
comboIndexBindable.UnbindFrom(combo.ComboIndexBindable); comboIndexBindable.UnbindFrom(combo.ComboIndexBindable);
samplesBindable.UnbindFrom(HitObject.SamplesBindable); 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. // 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(). // In order to stop this needless update, the event is unbound and re-bound as late as possible in Apply().
samplesBindable.CollectionChanged -= onSamplesChanged; samplesBindable.CollectionChanged -= onSamplesChanged;
@ -245,6 +285,8 @@ namespace osu.Game.Rulesets.Objects.Drawables
OnFree(HitObject); OnFree(HitObject);
HitObject = null; HitObject = null;
lifetimeEntry = null;
hasHitObjectApplied = false; hasHitObjectApplied = false;
} }
@ -275,16 +317,21 @@ namespace osu.Game.Rulesets.Objects.Drawables
{ {
} }
/// <summary>
/// Invoked when this <see cref="DrawableHitObject"/> receives a new parenting <see cref="DrawableHitObject"/>.
/// </summary>
/// <param name="parent">The parenting <see cref="DrawableHitObject"/>.</param>
protected virtual void OnParentReceived(DrawableHitObject parent)
{
}
/// <summary> /// <summary>
/// Invoked by the base <see cref="DrawableHitObject"/> to populate samples, once on initial load and potentially again on any change to the samples collection. /// Invoked by the base <see cref="DrawableHitObject"/> to populate samples, once on initial load and potentially again on any change to the samples collection.
/// </summary> /// </summary>
protected virtual void LoadSamples() protected virtual void LoadSamples()
{ {
if (Samples != null) samplesContainer.Clear();
{ Samples = null;
RemoveInternal(Samples);
Samples = null;
}
var samples = GetSamples().ToArray(); 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}."); + $" This is an indication that {nameof(HitObject.ApplyDefaults)} has not been invoked on {this}.");
} }
Samples = new PausableSkinnableSound(samples.Select(s => HitObject.SampleControlPoint.ApplyTo(s))); samplesContainer.Add(Samples = new PausableSkinnableSound(samples.Select(s => HitObject.SampleControlPoint.ApplyTo(s))));
AddInternal(Samples);
} }
private void onSamplesChanged(object sender, NotifyCollectionChangedEventArgs e) => LoadSamples(); private void onSamplesChanged(object sender, NotifyCollectionChangedEventArgs e) => LoadSamples();
private void onStartTimeChanged(ValueChangedEvent<double> startTime) => updateState(State.Value, true);
private void onNewResult(DrawableHitObject drawableHitObject, JudgementResult result) => OnNewResult?.Invoke(drawableHitObject, result); private void onNewResult(DrawableHitObject drawableHitObject, JudgementResult result) => OnNewResult?.Invoke(drawableHitObject, result);
private void onRevertResult(DrawableHitObject drawableHitObject, JudgementResult result) => OnRevertResult?.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) private void onDefaultsApplied(HitObject hitObject)
{ {
Apply(hitObject); Apply(hitObject, lifetimeEntry);
DefaultsApplied?.Invoke(this); DefaultsApplied?.Invoke(this);
} }
@ -558,15 +606,27 @@ namespace osu.Game.Rulesets.Objects.Drawables
/// </remarks> /// </remarks>
protected internal new ScheduledDelegate Schedule(Action action) => base.Schedule(action); protected internal new ScheduledDelegate Schedule(Action action) => base.Schedule(action);
private double? lifetimeStart;
public override double LifetimeStart public override double LifetimeStart
{ {
get => lifetimeStart ?? (HitObject.StartTime - InitialLifetimeOffset); get => base.LifetimeStart;
set 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; lifetimeEntry.LifetimeStart = lifetimeStart;
base.LifetimeStart = value; lifetimeEntry.LifetimeEnd = lifetimeEnd;
} }
} }

View File

@ -0,0 +1,110 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables;
using osu.Framework.Graphics.Performance;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Drawables;
namespace osu.Game.Rulesets.Objects
{
/// <summary>
/// A <see cref="LifetimeEntry"/> that stores the lifetime for a <see cref="HitObject"/>.
/// </summary>
public class HitObjectLifetimeEntry : LifetimeEntry
{
/// <summary>
/// The <see cref="HitObject"/>.
/// </summary>
public readonly HitObject HitObject;
/// <summary>
/// The result that <see cref="HitObject"/> was judged with.
/// This is set by the accompanying <see cref="DrawableHitObject"/>, and reused when required for rewinding.
/// </summary>
internal JudgementResult Result;
private readonly IBindable<double> startTimeBindable = new BindableDouble();
/// <summary>
/// Creates a new <see cref="HitObjectLifetimeEntry"/>.
/// </summary>
/// <param name="hitObject">The <see cref="HitObject"/> to store the lifetime of.</param>
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;
/// <summary>
/// The time at which the <see cref="HitObject"/> should become alive.
/// </summary>
public new double LifetimeStart
{
get => realLifetimeStart;
set => setLifetime(realLifetimeStart = value, LifetimeEnd);
}
// The lifetime end, as set by the hitobject.
private double realLifetimeEnd = double.MaxValue;
/// <summary>
/// The time at which the <see cref="HitObject"/> should become dead.
/// </summary>
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;
/// <summary>
/// Whether the <see cref="HitObject"/> should be kept always alive.
/// </summary>
internal bool KeepAlive
{
set
{
if (keepAlive == value)
return;
keepAlive = value;
setLifetime(realLifetimeStart, realLifetimeEnd);
}
}
/// <summary>
/// A safe offset prior to the start time of <see cref="HitObject"/> at which it may begin displaying contents.
/// By default, <see cref="HitObject"/>s are assumed to display their contents within 10 seconds prior to their start time.
/// </summary>
/// <remarks>
/// This is only used as an optimisation to delay the initial update of the <see cref="HitObject"/> and may be tuned more aggressively if required.
/// It is indirectly used to decide the automatic transform offset provided to <see cref="DrawableHitObject.UpdateInitialTransforms"/>.
/// A more accurate <see cref="LifetimeStart"/> should be set for further optimisation (in <see cref="DrawableHitObject.LoadComplete"/>, for example).
/// </remarks>
protected virtual double InitialLifetimeOffset => 10000;
/// <summary>
/// Resets <see cref="LifetimeStart"/> according to the change in start time of the <see cref="HitObject"/>.
/// </summary>
private void onStartTimeChanged(ValueChangedEvent<double> startTime) => LifetimeStart = HitObject.StartTime - InitialLifetimeOffset;
}
}

View File

@ -15,7 +15,10 @@ using System.Linq;
using System.Threading; using System.Threading;
using JetBrains.Annotations; using JetBrains.Annotations;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Pooling;
using osu.Framework.Input; using osu.Framework.Input;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Configuration; using osu.Game.Configuration;
@ -38,9 +41,8 @@ namespace osu.Game.Rulesets.UI
public abstract class DrawableRuleset<TObject> : DrawableRuleset, IProvideCursor, ICanAttachKeyCounter public abstract class DrawableRuleset<TObject> : DrawableRuleset, IProvideCursor, ICanAttachKeyCounter
where TObject : HitObject where TObject : HitObject
{ {
public override event Action<JudgementResult> OnNewResult; public override event Action<JudgementResult> NewResult;
public override event Action<JudgementResult> RevertResult;
public override event Action<JudgementResult> OnRevertResult;
/// <summary> /// <summary>
/// The selected variant. /// The selected variant.
@ -92,11 +94,8 @@ namespace osu.Game.Rulesets.UI
protected IRulesetConfigManager Config { get; private set; } protected IRulesetConfigManager Config { get; private set; }
/// <summary>
/// The mods which are to be applied.
/// </summary>
[Cached(typeof(IReadOnlyList<Mod>))] [Cached(typeof(IReadOnlyList<Mod>))]
protected readonly IReadOnlyList<Mod> Mods; protected override IReadOnlyList<Mod> Mods { get; }
private FrameStabilityContainer frameStabilityContainer; private FrameStabilityContainer frameStabilityContainer;
@ -125,7 +124,11 @@ namespace osu.Game.Rulesets.UI
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
KeyBindingInputManager = CreateInputManager(); KeyBindingInputManager = CreateInputManager();
playfield = new Lazy<Playfield>(CreatePlayfield); playfield = new Lazy<Playfield>(() => CreatePlayfield().With(p =>
{
p.NewResult += (_, r) => NewResult?.Invoke(r);
p.RevertResult += (_, r) => RevertResult?.Invoke(r);
}));
IsPaused.ValueChanged += paused => IsPaused.ValueChanged += paused =>
{ {
@ -183,7 +186,7 @@ namespace osu.Game.Rulesets.UI
RegenerateAutoplay(); RegenerateAutoplay();
loadObjects(cancellationToken); loadObjects(cancellationToken ?? default);
} }
public void RegenerateAutoplay() public void RegenerateAutoplay()
@ -196,15 +199,15 @@ namespace osu.Game.Rulesets.UI
/// <summary> /// <summary>
/// Creates and adds drawable representations of hit objects to the play field. /// Creates and adds drawable representations of hit objects to the play field.
/// </summary> /// </summary>
private void loadObjects(CancellationToken? cancellationToken) private void loadObjects(CancellationToken cancellationToken)
{ {
foreach (TObject h in Beatmap.HitObjects) foreach (TObject h in Beatmap.HitObjects)
{ {
cancellationToken?.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
addHitObject(h); AddHitObject(h);
} }
cancellationToken?.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
Playfield.PostProcess(); Playfield.PostProcess();
@ -231,22 +234,58 @@ namespace osu.Game.Rulesets.UI
} }
/// <summary> /// <summary>
/// Creates and adds the visual representation of a <typeparamref name="TObject"/> to this <see cref="DrawableRuleset{TObject}"/>. /// Adds a <see cref="HitObject"/> to this <see cref="DrawableRuleset"/>.
/// </summary> /// </summary>
/// <param name="hitObject">The <typeparamref name="TObject"/> to add the visual representation for.</param> /// <remarks>
private void addHitObject(TObject hitObject) /// This does not add the <see cref="HitObject"/> to the beatmap.
/// </remarks>
/// <param name="hitObject">The <see cref="HitObject"/> to add.</param>
public void AddHitObject(TObject hitObject)
{ {
var drawableObject = CreateDrawableRepresentation(hitObject); var drawableRepresentation = CreateDrawableRepresentation(hitObject);
if (drawableObject == null) // If a drawable representation exists, use it, otherwise assume the hitobject is being pooled.
return; if (drawableRepresentation != null)
Playfield.Add(drawableRepresentation);
drawableObject.OnNewResult += (_, r) => OnNewResult?.Invoke(r); else
drawableObject.OnRevertResult += (_, r) => OnRevertResult?.Invoke(r); Playfield.Add(GetLifetimeEntry(hitObject));
Playfield.Add(drawableObject);
} }
/// <summary>
/// Removes a <see cref="HitObject"/> from this <see cref="DrawableRuleset"/>.
/// </summary>
/// <remarks>
/// This does not remove the <see cref="HitObject"/> from the beatmap.
/// </remarks>
/// <param name="hitObject">The <see cref="HitObject"/> to remove.</param>
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) public override void SetRecordTarget(Replay recordingReplay)
{ {
if (!(KeyBindingInputManager is IHasRecordingHandler recordingInputManager)) if (!(KeyBindingInputManager is IHasRecordingHandler recordingInputManager))
@ -285,10 +324,15 @@ namespace osu.Game.Rulesets.UI
} }
/// <summary> /// <summary>
/// Creates a DrawableHitObject from a HitObject. /// Creates a <see cref="DrawableHitObject{TObject}"/> to represent a <see cref="HitObject"/>.
/// </summary> /// </summary>
/// <param name="h">The HitObject to make drawable.</param> /// <remarks>
/// <returns>The DrawableHitObject.</returns> /// If this method returns <c>null</c>, then this <see cref="DrawableRuleset"/> will assume the requested <see cref="HitObject"/> type is being pooled,
/// and will instead attempt to retrieve the <see cref="DrawableHitObject"/>s at the point they should become alive via pools registered through
/// <see cref="DrawableRuleset.RegisterPool{TObject, TDrawable}(int, int?)"/> or <see cref="DrawableRuleset.RegisterPool{TObject, TDrawable}(DrawablePool{TDrawable})"/>.
/// </remarks>
/// <param name="h">The <see cref="HitObject"/> to represent.</param>
/// <returns>The representing <see cref="DrawableHitObject{TObject}"/>.</returns>
public abstract DrawableHitObject<TObject> CreateDrawableRepresentation(TObject h); public abstract DrawableHitObject<TObject> CreateDrawableRepresentation(TObject h);
public void Attach(KeyCounterDisplay keyCounter) => public void Attach(KeyCounterDisplay keyCounter) =>
@ -361,20 +405,20 @@ namespace osu.Game.Rulesets.UI
/// Displays an interactive ruleset gameplay instance. /// Displays an interactive ruleset gameplay instance.
/// <remarks> /// <remarks>
/// This type is required only for adding non-generic type to the draw hierarchy. /// 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.
/// </remarks> /// </remarks>
/// </summary> /// </summary>
[Cached(typeof(DrawableRuleset))]
public abstract class DrawableRuleset : CompositeDrawable public abstract class DrawableRuleset : CompositeDrawable
{ {
/// <summary> /// <summary>
/// Invoked when a <see cref="JudgementResult"/> has been applied by a <see cref="DrawableHitObject"/>. /// Invoked when a <see cref="JudgementResult"/> has been applied by a <see cref="DrawableHitObject"/>.
/// </summary> /// </summary>
public abstract event Action<JudgementResult> OnNewResult; public abstract event Action<JudgementResult> NewResult;
/// <summary> /// <summary>
/// Invoked when a <see cref="JudgementResult"/> is being reverted by a <see cref="DrawableHitObject"/>. /// Invoked when a <see cref="JudgementResult"/> is being reverted by a <see cref="DrawableHitObject"/>.
/// </summary> /// </summary>
public abstract event Action<JudgementResult> OnRevertResult; public abstract event Action<JudgementResult> RevertResult;
/// <summary> /// <summary>
/// Whether a replay is currently loaded. /// Whether a replay is currently loaded.
@ -406,6 +450,11 @@ namespace osu.Game.Rulesets.UI
/// </summary> /// </summary>
public abstract IFrameStableClock FrameStableClock { get; } public abstract IFrameStableClock FrameStableClock { get; }
/// <summary>
/// The mods which are to be applied.
/// </summary>
protected abstract IReadOnlyList<Mod> Mods { get; }
/// <summary>~ /// <summary>~
/// The associated ruleset. /// The associated ruleset.
/// </summary> /// </summary>
@ -500,6 +549,99 @@ namespace osu.Game.Rulesets.UI
/// Invoked when the user requests to pause while the resume overlay is active. /// Invoked when the user requests to pause while the resume overlay is active.
/// </summary> /// </summary>
public abstract void CancelResume(); public abstract void CancelResume();
private readonly Dictionary<Type, IDrawablePool> pools = new Dictionary<Type, IDrawablePool>();
private readonly Dictionary<HitObject, HitObjectLifetimeEntry> lifetimeEntries = new Dictionary<HitObject, HitObjectLifetimeEntry>();
/// <summary>
/// Registers a default <see cref="DrawableHitObject"/> pool with this <see cref="DrawableRuleset"/> which is to be used whenever
/// <see cref="DrawableHitObject"/> representations are requested for the given <typeparamref name="TObject"/> type (via <see cref="GetPooledDrawableRepresentation"/>).
/// </summary>
/// <param name="initialSize">The number of <see cref="DrawableHitObject"/>s to be initially stored in the pool.</param>
/// <param name="maximumSize">
/// The maximum number of <see cref="DrawableHitObject"/>s that can be stored in the pool.
/// If this limit is exceeded, every subsequent <see cref="DrawableHitObject"/> will be created anew instead of being retrieved from the pool,
/// until some of the existing <see cref="DrawableHitObject"/>s are returned to the pool.
/// </param>
/// <typeparam name="TObject">The <see cref="HitObject"/> type.</typeparam>
/// <typeparam name="TDrawable">The <see cref="DrawableHitObject"/> receiver for <typeparamref name="TObject"/>s.</typeparam>
protected void RegisterPool<TObject, TDrawable>(int initialSize, int? maximumSize = null)
where TObject : HitObject
where TDrawable : DrawableHitObject, new()
=> RegisterPool<TObject, TDrawable>(new DrawablePool<TDrawable>(initialSize, maximumSize));
/// <summary>
/// Registers a custom <see cref="DrawableHitObject"/> pool with this <see cref="DrawableRuleset"/> which is to be used whenever
/// <see cref="DrawableHitObject"/> representations are requested for the given <typeparamref name="TObject"/> type (via <see cref="GetPooledDrawableRepresentation"/>).
/// </summary>
/// <param name="pool">The <see cref="DrawablePool{T}"/> to register.</param>
/// <typeparam name="TObject">The <see cref="HitObject"/> type.</typeparam>
/// <typeparam name="TDrawable">The <see cref="DrawableHitObject"/> receiver for <typeparamref name="TObject"/>s.</typeparam>
protected void RegisterPool<TObject, TDrawable>([NotNull] DrawablePool<TDrawable> pool)
where TObject : HitObject
where TDrawable : DrawableHitObject, new()
{
pools[typeof(TObject)] = pool;
AddInternal(pool);
}
/// <summary>
/// Attempts to retrieve the poolable <see cref="DrawableHitObject"/> representation of a <see cref="HitObject"/>.
/// </summary>
/// <param name="hitObject">The <see cref="HitObject"/> to retrieve the <see cref="DrawableHitObject"/> representation of.</param>
/// <returns>The <see cref="DrawableHitObject"/> representing <see cref="HitObject"/>, or <c>null</c> if no poolable representation exists.</returns>
[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<IApplicableToDrawableHitObjects>())
m.ApplyToDrawableHitObjects(dho.Yield());
}
dho.Apply(hitObject, GetLifetimeEntry(hitObject));
});
}
/// <summary>
/// Creates the <see cref="HitObjectLifetimeEntry"/> for a given <see cref="HitObject"/>.
/// </summary>
/// <remarks>
/// This may be overridden to provide custom lifetime control (e.g. via <see cref="HitObjectLifetimeEntry.InitialLifetimeOffset"/>.
/// </remarks>
/// <param name="hitObject">The <see cref="HitObject"/> to create the entry for.</param>
/// <returns>The <see cref="HitObjectLifetimeEntry"/>.</returns>
[NotNull]
protected abstract HitObjectLifetimeEntry CreateLifetimeEntry([NotNull] HitObject hitObject);
/// <summary>
/// Retrieves or creates the <see cref="HitObjectLifetimeEntry"/> for a given <see cref="HitObject"/>.
/// </summary>
/// <param name="hitObject">The <see cref="HitObject"/> to retrieve or create the <see cref="HitObjectLifetimeEntry"/> for.</param>
/// <returns>The <see cref="HitObjectLifetimeEntry"/> for <paramref name="hitObject"/>.</returns>
[NotNull]
protected HitObjectLifetimeEntry GetLifetimeEntry([NotNull] HitObject hitObject)
{
if (lifetimeEntries.TryGetValue(hitObject, out var entry))
return entry;
return lifetimeEntries[hitObject] = CreateLifetimeEntry(hitObject);
}
/// <summary>
/// Removes the <see cref="HitObjectLifetimeEntry"/> for a <see cref="HitObject"/>.
/// </summary>
/// <param name="hitObject">The <see cref="HitObject"/> to remove the <see cref="HitObjectLifetimeEntry"/> for.</param>
internal void RemoveLifetimeEntry([NotNull] HitObject hitObject) => lifetimeEntries.Remove(hitObject);
} }
public class BeatmapInvalidForRulesetException : ArgumentException public class BeatmapInvalidForRulesetException : ArgumentException

View File

@ -1,33 +1,150 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Linq; using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Performance; using osu.Framework.Graphics.Performance;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
namespace osu.Game.Rulesets.UI namespace osu.Game.Rulesets.UI
{ {
public class HitObjectContainer : LifetimeManagementContainer public class HitObjectContainer : LifetimeManagementContainer
{ {
/// <summary>
/// All currently in-use <see cref="DrawableHitObject"/>s.
/// </summary>
public IEnumerable<DrawableHitObject> Objects => InternalChildren.Cast<DrawableHitObject>().OrderBy(h => h.HitObject.StartTime); public IEnumerable<DrawableHitObject> Objects => InternalChildren.Cast<DrawableHitObject>().OrderBy(h => h.HitObject.StartTime);
/// <summary>
/// All currently in-use <see cref="DrawableHitObject"/>s that are alive.
/// </summary>
/// <remarks>
/// If this <see cref="HitObjectContainer"/> uses pooled objects, this is equivalent to <see cref="Objects"/>.
/// </remarks>
public IEnumerable<DrawableHitObject> AliveObjects => AliveInternalChildren.Cast<DrawableHitObject>().OrderBy(h => h.HitObject.StartTime); public IEnumerable<DrawableHitObject> AliveObjects => AliveInternalChildren.Cast<DrawableHitObject>().OrderBy(h => h.HitObject.StartTime);
private readonly Dictionary<DrawableHitObject, (IBindable<double> bindable, double timeAtAdd)> startTimeMap = new Dictionary<DrawableHitObject, (IBindable<double>, double)>(); /// <summary>
/// Invoked when a <see cref="DrawableHitObject"/> is judged.
/// </summary>
public event Action<DrawableHitObject, JudgementResult> NewResult;
/// <summary>
/// Invoked when a <see cref="DrawableHitObject"/> judgement is reverted.
/// </summary>
public event Action<DrawableHitObject, JudgementResult> RevertResult;
/// <summary>
/// Invoked when a <see cref="HitObject"/> becomes used by a <see cref="DrawableHitObject"/>.
/// </summary>
/// <remarks>
/// If this <see cref="HitObjectContainer"/> uses pooled objects, this represents the time when the <see cref="HitObject"/>s become alive.
/// </remarks>
internal event Action<HitObject> HitObjectUsageBegan;
/// <summary>
/// Invoked when a <see cref="HitObject"/> becomes unused by a <see cref="DrawableHitObject"/>.
/// </summary>
/// <remarks>
/// If this <see cref="HitObjectContainer"/> uses pooled objects, this represents the time when the <see cref="HitObject"/>s become dead.
/// </remarks>
internal event Action<HitObject> HitObjectUsageFinished;
/// <summary>
/// The amount of time prior to the current time within which <see cref="HitObject"/>s should be considered alive.
/// </summary>
internal double PastLifetimeExtension { get; set; }
/// <summary>
/// The amount of time after the current time within which <see cref="HitObject"/>s should be considered alive.
/// </summary>
internal double FutureLifetimeExtension { get; set; }
private readonly Dictionary<DrawableHitObject, IBindable> startTimeMap = new Dictionary<DrawableHitObject, IBindable>();
private readonly Dictionary<HitObjectLifetimeEntry, DrawableHitObject> drawableMap = new Dictionary<HitObjectLifetimeEntry, DrawableHitObject>();
private readonly LifetimeEntryManager lifetimeManager = new LifetimeEntryManager();
[Resolved(CanBeNull = true)]
private DrawableRuleset drawableRuleset { get; set; }
public HitObjectContainer() public HitObjectContainer()
{ {
RelativeSizeAxes = Axes.Both; 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) public virtual void Add(DrawableHitObject hitObject)
{ {
// Added first for the comparer to remain ordered during AddInternal bindStartTime(hitObject);
startTimeMap[hitObject] = (hitObject.HitObject.StartTimeBindable.GetBoundCopy(), hitObject.HitObject.StartTime);
startTimeMap[hitObject].bindable.BindValueChanged(_ => onStartTimeChanged(hitObject)); hitObject.OnNewResult += onNewResult;
hitObject.OnRevertResult += onRevertResult;
AddInternal(hitObject); AddInternal(hitObject);
} }
@ -37,54 +154,16 @@ namespace osu.Game.Rulesets.UI
if (!RemoveInternal(hitObject)) if (!RemoveInternal(hitObject))
return false; return false;
// Removed last for the comparer to remain ordered during RemoveInternal hitObject.OnNewResult -= onNewResult;
startTimeMap[hitObject].bindable.UnbindAll(); hitObject.OnRevertResult -= onRevertResult;
startTimeMap.Remove(hitObject);
unbindStartTime(hitObject);
return true; 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); 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) protected override void OnChildLifetimeBoundaryCrossed(LifetimeBoundaryCrossedEvent e)
{ {
if (!(e.Child is DrawableHitObject hitObject)) if (!(e.Child is DrawableHitObject hitObject))
@ -96,5 +175,71 @@ namespace osu.Game.Rulesets.UI
hitObject.OnKilled(); 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();
}
} }
} }

View File

@ -10,13 +10,25 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osuTK; using osuTK;
namespace osu.Game.Rulesets.UI namespace osu.Game.Rulesets.UI
{ {
public abstract class Playfield : CompositeDrawable public abstract class Playfield : CompositeDrawable
{ {
/// <summary>
/// Invoked when a <see cref="DrawableHitObject"/> is judged.
/// </summary>
public event Action<DrawableHitObject, JudgementResult> NewResult;
/// <summary>
/// Invoked when a <see cref="DrawableHitObject"/> judgement is reverted.
/// </summary>
public event Action<DrawableHitObject, JudgementResult> RevertResult;
/// <summary> /// <summary>
/// The <see cref="DrawableHitObject"/> contained in this Playfield. /// The <see cref="DrawableHitObject"/> contained in this Playfield.
/// </summary> /// </summary>
@ -72,7 +84,13 @@ namespace osu.Game.Rulesets.UI
{ {
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
hitObjectContainerLazy = new Lazy<HitObjectContainer>(CreateHitObjectContainer); hitObjectContainerLazy = new Lazy<HitObjectContainer>(() => 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)] [Resolved(CanBeNull = true)]
@ -101,13 +119,73 @@ namespace osu.Game.Rulesets.UI
/// Adds a DrawableHitObject to this Playfield. /// Adds a DrawableHitObject to this Playfield.
/// </summary> /// </summary>
/// <param name="h">The DrawableHitObject to add.</param> /// <param name="h">The DrawableHitObject to add.</param>
public virtual void Add(DrawableHitObject h) => HitObjectContainer.Add(h); public virtual void Add(DrawableHitObject h)
{
HitObjectContainer.Add(h);
OnHitObjectAdded(h.HitObject);
}
/// <summary> /// <summary>
/// Remove a DrawableHitObject from this Playfield. /// Remove a DrawableHitObject from this Playfield.
/// </summary> /// </summary>
/// <param name="h">The DrawableHitObject to remove.</param> /// <param name="h">The DrawableHitObject to remove.</param>
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;
}
/// <summary>
/// Adds a <see cref="HitObjectLifetimeEntry"/> for a pooled <see cref="HitObject"/> to this <see cref="Playfield"/>.
/// </summary>
/// <param name="entry">The <see cref="HitObjectLifetimeEntry"/> controlling the lifetime of the <see cref="HitObject"/>.</param>
public virtual void Add(HitObjectLifetimeEntry entry)
{
HitObjectContainer.Add(entry);
lifetimeEntryMap[entry.HitObject] = entry;
OnHitObjectAdded(entry.HitObject);
}
/// <summary>
/// Removes a <see cref="HitObjectLifetimeEntry"/> for a pooled <see cref="HitObject"/> from this <see cref="Playfield"/>.
/// </summary>
/// <param name="entry">The <see cref="HitObjectLifetimeEntry"/> controlling the lifetime of the <see cref="HitObject"/>.</param>
/// <returns>Whether the <see cref="HitObject"/> was successfully removed.</returns>
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;
}
/// <summary>
/// Invoked when a <see cref="HitObject"/> is added to this <see cref="Playfield"/>.
/// </summary>
/// <param name="hitObject">The added <see cref="HitObject"/>.</param>
protected virtual void OnHitObjectAdded(HitObject hitObject)
{
}
/// <summary>
/// Invoked when a <see cref="HitObject"/> is removed from this <see cref="Playfield"/>.
/// </summary>
/// <param name="hitObject">The removed <see cref="HitObject"/>.</param>
protected virtual void OnHitObjectRemoved(HitObject hitObject)
{
}
/// <summary> /// <summary>
/// The cursor currently being used by this <see cref="Playfield"/>. May be null if no cursor is provided. /// The cursor currently being used by this <see cref="Playfield"/>. May be null if no cursor is provided.
@ -131,6 +209,12 @@ namespace osu.Game.Rulesets.UI
protected void AddNested(Playfield otherPlayfield) protected void AddNested(Playfield otherPlayfield)
{ {
otherPlayfield.DisplayJudgements.BindTo(DisplayJudgements); 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); nestedPlayfields.Value.Add(otherPlayfield);
} }
@ -162,6 +246,99 @@ namespace osu.Game.Rulesets.UI
/// </summary> /// </summary>
protected virtual HitObjectContainer CreateHitObjectContainer() => new HitObjectContainer(); protected virtual HitObjectContainer CreateHitObjectContainer() => new HitObjectContainer();
#region Editor logic
/// <summary>
/// Invoked when a <see cref="HitObject"/> becomes used by a <see cref="DrawableHitObject"/>.
/// </summary>
/// <remarks>
/// If this <see cref="HitObjectContainer"/> uses pooled objects, this represents the time when the <see cref="HitObject"/>s become alive.
/// </remarks>
internal event Action<HitObject> HitObjectUsageBegan;
/// <summary>
/// Invoked when a <see cref="HitObject"/> becomes unused by a <see cref="DrawableHitObject"/>.
/// </summary>
/// <remarks>
/// If this <see cref="HitObjectContainer"/> uses pooled objects, this represents the time when the <see cref="HitObject"/>s become dead.
/// </remarks>
internal event Action<HitObject> HitObjectUsageFinished;
private readonly Dictionary<HitObject, HitObjectLifetimeEntry> lifetimeEntryMap = new Dictionary<HitObject, HitObjectLifetimeEntry>();
/// <summary>
/// Sets whether to keep a given <see cref="HitObject"/> always alive within this or any nested <see cref="Playfield"/>.
/// </summary>
/// <param name="hitObject">The <see cref="HitObject"/> to set.</param>
/// <param name="keepAlive">Whether to keep <paramref name="hitObject"/> always alive.</param>
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);
}
/// <summary>
/// Keeps all <see cref="HitObject"/>s alive within this and all nested <see cref="Playfield"/>s.
/// </summary>
internal void KeepAllAlive()
{
foreach (var (_, entry) in lifetimeEntryMap)
entry.KeepAlive = true;
if (!nestedPlayfields.IsValueCreated)
return;
foreach (var p in nestedPlayfields.Value)
p.KeepAllAlive();
}
/// <summary>
/// The amount of time prior to the current time within which <see cref="HitObject"/>s should be considered alive.
/// </summary>
internal double PastLifetimeExtension
{
get => HitObjectContainer.PastLifetimeExtension;
set
{
HitObjectContainer.PastLifetimeExtension = value;
if (!nestedPlayfields.IsValueCreated)
return;
foreach (var nested in nestedPlayfields.Value)
nested.PastLifetimeExtension = value;
}
}
/// <summary>
/// The amount of time after the current time within which <see cref="HitObject"/>s should be considered alive.
/// </summary>
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 public class InvisibleCursorContainer : GameplayCursorContainer
{ {
protected override Drawable CreateCursor() => new InvisibleCursor(); protected override Drawable CreateCursor() => new InvisibleCursor();

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Collections.Generic;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
@ -34,6 +35,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
protected SelectionHandler SelectionHandler { get; private set; } protected SelectionHandler SelectionHandler { get; private set; }
protected readonly HitObjectComposer Composer;
[Resolved(CanBeNull = true)] [Resolved(CanBeNull = true)]
private IEditorChangeHandler changeHandler { get; set; } private IEditorChangeHandler changeHandler { get; set; }
@ -44,12 +47,15 @@ namespace osu.Game.Screens.Edit.Compose.Components
protected EditorBeatmap Beatmap { get; private set; } protected EditorBeatmap Beatmap { get; private set; }
private readonly BindableList<HitObject> selectedHitObjects = new BindableList<HitObject>(); private readonly BindableList<HitObject> selectedHitObjects = new BindableList<HitObject>();
private readonly Dictionary<HitObject, SelectionBlueprint> blueprintMap = new Dictionary<HitObject, SelectionBlueprint>();
[Resolved(canBeNull: true)] [Resolved(canBeNull: true)]
private IPositionSnapProvider snapProvider { get; set; } private IPositionSnapProvider snapProvider { get; set; }
protected BlueprintContainer() protected BlueprintContainer(HitObjectComposer composer)
{ {
Composer = composer;
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
} }
@ -68,8 +74,12 @@ namespace osu.Game.Screens.Edit.Compose.Components
DragBox.CreateProxy().With(p => p.Depth = float.MinValue) DragBox.CreateProxy().With(p => p.Depth = float.MinValue)
}); });
foreach (var obj in Beatmap.HitObjects) // For non-pooled rulesets, hitobjects are already present in the playfield which allows the blueprints to be loaded in the async context.
AddBlueprintFor(obj); if (Composer != null)
{
foreach (var obj in Composer.HitObjects)
addBlueprintFor(obj.HitObject);
}
selectedHitObjects.BindTo(Beatmap.SelectedHitObjects); selectedHitObjects.BindTo(Beatmap.SelectedHitObjects);
selectedHitObjects.CollectionChanged += (selectedObjects, args) => selectedHitObjects.CollectionChanged += (selectedObjects, args) =>
@ -94,8 +104,18 @@ namespace osu.Game.Screens.Edit.Compose.Components
{ {
base.LoadComplete(); base.LoadComplete();
Beatmap.HitObjectAdded += AddBlueprintFor; Beatmap.HitObjectAdded += addBlueprintFor;
Beatmap.HitObjectRemoved += removeBlueprintFor; Beatmap.HitObjectRemoved += removeBlueprintFor;
if (Composer != null)
{
// For pooled rulesets, blueprints must be added for hitobjects already "current" as they would've not been "current" during the async load addition process above.
foreach (var obj in Composer.HitObjects)
addBlueprintFor(obj.HitObject);
Composer.Playfield.HitObjectUsageBegan += addBlueprintFor;
Composer.Playfield.HitObjectUsageFinished += removeBlueprintFor;
}
} }
protected virtual Container<SelectionBlueprint> CreateSelectionBlueprintContainer() => protected virtual Container<SelectionBlueprint> CreateSelectionBlueprintContainer() =>
@ -247,29 +267,17 @@ namespace osu.Game.Screens.Edit.Compose.Components
#region Blueprint Addition/Removal #region Blueprint Addition/Removal
private void removeBlueprintFor(HitObject hitObject) private void addBlueprintFor(HitObject hitObject)
{ {
var blueprint = SelectionBlueprints.SingleOrDefault(m => m.HitObject == hitObject); if (blueprintMap.ContainsKey(hitObject))
if (blueprint == null)
return; 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); var blueprint = CreateBlueprintFor(hitObject);
if (blueprint == null) if (blueprint == null)
return; return;
blueprintMap[hitObject] = blueprint;
blueprint.Selected += onBlueprintSelected; blueprint.Selected += onBlueprintSelected;
blueprint.Deselected += onBlueprintDeselected; blueprint.Deselected += onBlueprintDeselected;
@ -277,6 +285,41 @@ namespace osu.Game.Screens.Edit.Compose.Components
blueprint.Select(); blueprint.Select();
SelectionBlueprints.Add(blueprint); 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);
}
/// <summary>
/// Called after a <see cref="HitObject"/> blueprint has been added.
/// </summary>
/// <param name="hitObject">The <see cref="HitObject"/> for which the blueprint has been added.</param>
protected virtual void OnBlueprintAdded(HitObject hitObject)
{
}
/// <summary>
/// Called after a <see cref="HitObject"/> blueprint has been removed.
/// </summary>
/// <param name="hitObject">The <see cref="HitObject"/> for which the blueprint has been removed.</param>
protected virtual void OnBlueprintRemoved(HitObject hitObject)
{
} }
#endregion #endregion
@ -349,7 +392,13 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// <summary> /// <summary>
/// Selects all <see cref="SelectionBlueprint"/>s. /// Selects all <see cref="SelectionBlueprint"/>s.
/// </summary> /// </summary>
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()));
}
/// <summary> /// <summary>
/// Deselects all selected <see cref="SelectionBlueprint"/>s. /// Deselects all selected <see cref="SelectionBlueprint"/>s.
@ -360,12 +409,16 @@ namespace osu.Game.Screens.Edit.Compose.Components
{ {
SelectionHandler.HandleSelected(blueprint); SelectionHandler.HandleSelected(blueprint);
SelectionBlueprints.ChangeChildDepth(blueprint, 1); SelectionBlueprints.ChangeChildDepth(blueprint, 1);
Composer.Playfield.SetKeepAlive(blueprint.HitObject, true);
} }
private void onBlueprintDeselected(SelectionBlueprint blueprint) private void onBlueprintDeselected(SelectionBlueprint blueprint)
{ {
SelectionHandler.HandleDeselected(blueprint); SelectionHandler.HandleDeselected(blueprint);
SelectionBlueprints.ChangeChildDepth(blueprint, 0); SelectionBlueprints.ChangeChildDepth(blueprint, 0);
Composer.Playfield.SetKeepAlive(blueprint.HitObject, false);
} }
#endregion #endregion
@ -456,9 +509,15 @@ namespace osu.Game.Screens.Edit.Compose.Components
if (Beatmap != null) if (Beatmap != null)
{ {
Beatmap.HitObjectAdded -= AddBlueprintFor; Beatmap.HitObjectAdded -= addBlueprintFor;
Beatmap.HitObjectRemoved -= removeBlueprintFor; Beatmap.HitObjectRemoved -= removeBlueprintFor;
} }
if (Composer != null)
{
Composer.Playfield.HitObjectUsageBegan -= addBlueprintFor;
Composer.Playfield.HitObjectUsageFinished -= removeBlueprintFor;
}
} }
} }
} }

View File

@ -27,23 +27,16 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// </summary> /// </summary>
public class ComposeBlueprintContainer : BlueprintContainer public class ComposeBlueprintContainer : BlueprintContainer
{ {
[Resolved] public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
private HitObjectComposer composer { get; set; }
private PlacementBlueprint currentPlacement;
private readonly Container<PlacementBlueprint> placementBlueprintContainer; private readonly Container<PlacementBlueprint> placementBlueprintContainer;
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; private PlacementBlueprint currentPlacement;
private InputManager inputManager; private InputManager inputManager;
private readonly IEnumerable<DrawableHitObject> drawableHitObjects; public ComposeBlueprintContainer(HitObjectComposer composer)
: base(composer)
public ComposeBlueprintContainer(IEnumerable<DrawableHitObject> drawableHitObjects)
{ {
this.drawableHitObjects = drawableHitObjects;
placementBlueprintContainer = new Container<PlacementBlueprint> placementBlueprintContainer = new Container<PlacementBlueprint>
{ {
RelativeSizeAxes = Axes.Both RelativeSizeAxes = Axes.Both
@ -162,7 +155,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
private void updatePlacementPosition() private void updatePlacementPosition()
{ {
var snapResult = composer.SnapScreenSpacePositionToValidTime(inputManager.CurrentState.Mouse.Position); var snapResult = Composer.SnapScreenSpacePositionToValidTime(inputManager.CurrentState.Mouse.Position);
currentPlacement.UpdatePosition(snapResult); currentPlacement.UpdatePosition(snapResult);
} }
@ -173,7 +166,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
{ {
base.Update(); base.Update();
if (composer.CursorInPlacementArea) if (Composer.CursorInPlacementArea)
createPlacement(); createPlacement();
else if (currentPlacement?.PlacementActive == false) else if (currentPlacement?.PlacementActive == false)
removePlacement(); removePlacement();
@ -186,7 +179,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
protected sealed override SelectionBlueprint CreateBlueprintFor(HitObject hitObject) 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) if (drawable == null)
return null; return null;
@ -196,11 +189,11 @@ namespace osu.Game.Screens.Edit.Compose.Components
public virtual OverlaySelectionBlueprint CreateBlueprintFor(DrawableHitObject hitObject) => null; 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. // on successful placement, the new combo button should be reset as this is the most common user interaction.
if (Beatmap.SelectedHitObjects.Count == 0) if (Beatmap.SelectedHitObjects.Count == 0)

View File

@ -219,6 +219,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
[Resolved] [Resolved]
private IBeatSnapProvider beatSnapProvider { get; set; } private IBeatSnapProvider beatSnapProvider { get; set; }
/// <summary>
/// The total amount of time visible on the timeline.
/// </summary>
public double VisibleRange => track.Length / Zoom;
public SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) => public SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) =>
new SnapResult(screenSpacePosition, beatSnapProvider.SnapTime(getTimeFromPosition(Content.ToLocalSpace(screenSpacePosition)))); new SnapResult(screenSpacePosition, beatSnapProvider.SnapTime(getTimeFromPosition(Content.ToLocalSpace(screenSpacePosition))));

View File

@ -26,12 +26,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
private EditorBeatmap beatmap { get; set; } private EditorBeatmap beatmap { get; set; }
private DragEvent lastDragEvent; private DragEvent lastDragEvent;
private Bindable<HitObject> placement; private Bindable<HitObject> placement;
private SelectionBlueprint placementBlueprint; private SelectionBlueprint placementBlueprint;
public TimelineBlueprintContainer() public TimelineBlueprintContainer(HitObjectComposer composer)
: base(composer)
{ {
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
Anchor = Anchor.Centre; Anchor = Anchor.Centre;
@ -97,6 +96,12 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
if (lastDragEvent != null) if (lastDragEvent != null)
OnDrag(lastDragEvent); OnDrag(lastDragEvent);
if (Composer != null)
{
Composer.Playfield.PastLifetimeExtension = timeline.VisibleRange / 2;
Composer.Playfield.FutureLifetimeExtension = timeline.VisibleRange / 2;
}
base.Update(); base.Update();
} }

View File

@ -53,6 +53,6 @@ namespace osu.Game.Screens.Edit.Compose
return beatmapSkinProvider.WithChild(rulesetSkinProvider.WithChild(composer)); 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);
} }
} }

View File

@ -261,14 +261,14 @@ namespace osu.Game.Screens.Play
// bind clock into components that require it // bind clock into components that require it
DrawableRuleset.IsPaused.BindTo(GameplayClockContainer.IsPaused); DrawableRuleset.IsPaused.BindTo(GameplayClockContainer.IsPaused);
DrawableRuleset.OnNewResult += r => DrawableRuleset.NewResult += r =>
{ {
HealthProcessor.ApplyResult(r); HealthProcessor.ApplyResult(r);
ScoreProcessor.ApplyResult(r); ScoreProcessor.ApplyResult(r);
gameplayBeatmap.ApplyResult(r); gameplayBeatmap.ApplyResult(r);
}; };
DrawableRuleset.OnRevertResult += r => DrawableRuleset.RevertResult += r =>
{ {
HealthProcessor.RevertResult(r); HealthProcessor.RevertResult(r);
ScoreProcessor.RevertResult(r); ScoreProcessor.RevertResult(r);

View File

@ -26,7 +26,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="ppy.osu.Framework" Version="2020.1111.0" /> <PackageReference Include="ppy.osu.Framework" Version="2020.1113.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.1030.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2020.1030.0" />
<PackageReference Include="Sentry" Version="2.1.6" /> <PackageReference Include="Sentry" Version="2.1.6" />
<PackageReference Include="SharpCompress" Version="0.26.0" /> <PackageReference Include="SharpCompress" Version="0.26.0" />

View File

@ -70,7 +70,7 @@
<Reference Include="System.Net.Http" /> <Reference Include="System.Net.Http" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="ppy.osu.Framework.iOS" Version="2020.1111.0" /> <PackageReference Include="ppy.osu.Framework.iOS" Version="2020.1113.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.1030.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2020.1030.0" />
</ItemGroup> </ItemGroup>
<!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) --> <!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) -->
@ -88,7 +88,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="ppy.osu.Framework" Version="2020.1111.0" /> <PackageReference Include="ppy.osu.Framework" Version="2020.1113.0" />
<PackageReference Include="SharpCompress" Version="0.26.0" /> <PackageReference Include="SharpCompress" Version="0.26.0" />
<PackageReference Include="NUnit" Version="3.12.0" /> <PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="SharpRaven" Version="2.4.0" /> <PackageReference Include="SharpRaven" Version="2.4.0" />