mirror of
https://github.com/ppy/osu.git
synced 2024-12-14 20:13:21 +08:00
Merge pull request #24118 from bdach/nested-entries
Add links to nested objects' lifetime entries to `HitObjectLifetimeEntry`
This commit is contained in:
commit
674ade0c24
@ -170,7 +170,16 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
ManualClock clock = null;
|
||||
|
||||
var beatmap = new Beatmap();
|
||||
beatmap.HitObjects.Add(new TestHitObjectWithNested { Duration = 40 });
|
||||
beatmap.HitObjects.Add(new TestHitObjectWithNested
|
||||
{
|
||||
Duration = 40,
|
||||
NestedObjects = new HitObject[]
|
||||
{
|
||||
new PooledNestedHitObject { StartTime = 10 },
|
||||
new PooledNestedHitObject { StartTime = 20 },
|
||||
new PooledNestedHitObject { StartTime = 30 }
|
||||
}
|
||||
});
|
||||
|
||||
createTest(beatmap, 10, () => new FramedClock(clock = new ManualClock()));
|
||||
|
||||
@ -209,6 +218,49 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
AddAssert("object judged", () => playfield.JudgedObjects.Count == 1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPooledObjectWithNonPooledNesteds()
|
||||
{
|
||||
ManualClock clock = null;
|
||||
TestHitObjectWithNested hitObjectWithNested;
|
||||
|
||||
var beatmap = new Beatmap();
|
||||
beatmap.HitObjects.Add(hitObjectWithNested = new TestHitObjectWithNested
|
||||
{
|
||||
Duration = 40,
|
||||
NestedObjects = new HitObject[]
|
||||
{
|
||||
new PooledNestedHitObject { StartTime = 10 },
|
||||
new NonPooledNestedHitObject { StartTime = 20 },
|
||||
new NonPooledNestedHitObject { StartTime = 30 }
|
||||
}
|
||||
});
|
||||
|
||||
createTest(beatmap, 10, () => new FramedClock(clock = new ManualClock()));
|
||||
|
||||
AddAssert("hitobject entry has all nesteds", () => playfield.HitObjectContainer.Entries.Single().NestedEntries, () => Has.Count.EqualTo(3));
|
||||
|
||||
AddStep("skip to middle of object", () => clock.CurrentTime = (hitObjectWithNested.StartTime + hitObjectWithNested.GetEndTime()) / 2);
|
||||
AddAssert("2 objects judged", () => playfield.JudgedObjects.Count, () => Is.EqualTo(2));
|
||||
AddAssert("entry not all judged", () => playfield.HitObjectContainer.Entries.Single().AllJudged, () => Is.False);
|
||||
|
||||
AddStep("skip to before end of object", () => clock.CurrentTime = hitObjectWithNested.GetEndTime() - 1);
|
||||
AddAssert("3 objects judged", () => playfield.JudgedObjects.Count, () => Is.EqualTo(3));
|
||||
AddAssert("entry not all judged", () => playfield.HitObjectContainer.Entries.Single().AllJudged, () => Is.False);
|
||||
|
||||
AddStep("removing object doesn't crash", () => playfield.Remove(hitObjectWithNested));
|
||||
AddStep("clear judged", () => playfield.JudgedObjects.Clear());
|
||||
|
||||
AddStep("add object back", () => playfield.Add(hitObjectWithNested));
|
||||
AddAssert("entry not all judged", () => playfield.HitObjectContainer.Entries.Single().AllJudged, () => Is.False);
|
||||
|
||||
AddStep("skip to long past object", () => clock.CurrentTime = 100_000);
|
||||
// the parent entry should still be linked to nested entries of pooled objects that are managed externally
|
||||
// but not contain synthetic entries that were created for the non-pooled objects.
|
||||
AddAssert("entry still has non-synthetic nested entries", () => playfield.HitObjectContainer.Entries.Single().NestedEntries, () => Has.Count.EqualTo(1));
|
||||
AddAssert("entry all judged", () => playfield.HitObjectContainer.Entries.Single().AllJudged, () => Is.True);
|
||||
}
|
||||
|
||||
private void createTest(IBeatmap beatmap, int poolSize, Func<IFrameBasedClock> createClock = null)
|
||||
{
|
||||
AddStep("create test", () =>
|
||||
@ -289,7 +341,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
RegisterPool<TestHitObject, DrawableTestHitObject>(poolSize);
|
||||
RegisterPool<TestKilledHitObject, DrawableTestKilledHitObject>(poolSize);
|
||||
RegisterPool<TestHitObjectWithNested, DrawableTestHitObjectWithNested>(poolSize);
|
||||
RegisterPool<NestedHitObject, DrawableNestedHitObject>(poolSize);
|
||||
RegisterPool<PooledNestedHitObject, DrawableNestedHitObject>(poolSize);
|
||||
}
|
||||
|
||||
protected override HitObjectLifetimeEntry CreateLifetimeEntry(HitObject hitObject) => new TestHitObjectLifetimeEntry(hitObject);
|
||||
@ -422,16 +474,22 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
private class TestHitObjectWithNested : TestHitObject
|
||||
{
|
||||
public IEnumerable<HitObject> NestedObjects { get; init; } = Array.Empty<HitObject>();
|
||||
|
||||
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
|
||||
{
|
||||
base.CreateNestedHitObjects(cancellationToken);
|
||||
|
||||
for (int i = 0; i < 3; ++i)
|
||||
AddNested(new NestedHitObject { StartTime = (float)Duration * (i + 1) / 4 });
|
||||
foreach (var ho in NestedObjects)
|
||||
AddNested(ho);
|
||||
}
|
||||
}
|
||||
|
||||
private class NestedHitObject : ConvertHitObject
|
||||
private class PooledNestedHitObject : ConvertHitObject
|
||||
{
|
||||
}
|
||||
|
||||
private class NonPooledNestedHitObject : ConvertHitObject
|
||||
{
|
||||
}
|
||||
|
||||
@ -482,6 +540,9 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
nestedContainer.Clear(false);
|
||||
}
|
||||
|
||||
protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject)
|
||||
=> hitObject is NonPooledNestedHitObject nonPooled ? new DrawableNestedHitObject(nonPooled) : null;
|
||||
|
||||
protected override void CheckForResult(bool userTriggered, double timeOffset)
|
||||
{
|
||||
base.CheckForResult(userTriggered, timeOffset);
|
||||
@ -490,25 +551,30 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
}
|
||||
}
|
||||
|
||||
private partial class DrawableNestedHitObject : DrawableHitObject<NestedHitObject>
|
||||
private partial class DrawableNestedHitObject : DrawableHitObject
|
||||
{
|
||||
public DrawableNestedHitObject()
|
||||
: this(null)
|
||||
{
|
||||
}
|
||||
|
||||
public DrawableNestedHitObject(NestedHitObject hitObject)
|
||||
public DrawableNestedHitObject(PooledNestedHitObject hitObject)
|
||||
: base(hitObject)
|
||||
{
|
||||
}
|
||||
|
||||
public DrawableNestedHitObject(NonPooledNestedHitObject hitObject)
|
||||
: base(hitObject)
|
||||
{
|
||||
Size = new Vector2(15);
|
||||
Colour = Colour4.White;
|
||||
RelativePositionAxes = Axes.Both;
|
||||
Origin = Anchor.Centre;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Size = new Vector2(15);
|
||||
Colour = Colour4.White;
|
||||
RelativePositionAxes = Axes.Both;
|
||||
Origin = Anchor.Centre;
|
||||
|
||||
AddInternal(new Circle
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
|
@ -98,9 +98,9 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
public virtual bool DisplayResult => true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether this <see cref="DrawableHitObject"/> and all of its nested <see cref="DrawableHitObject"/>s have been judged.
|
||||
/// The scoring result of this <see cref="DrawableHitObject"/>.
|
||||
/// </summary>
|
||||
public bool AllJudged => Judged && NestedHitObjects.All(h => h.AllJudged);
|
||||
public JudgementResult Result => Entry?.Result;
|
||||
|
||||
/// <summary>
|
||||
/// Whether this <see cref="DrawableHitObject"/> has been hit. This occurs if <see cref="Result"/> is hit.
|
||||
@ -112,12 +112,12 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
/// Whether this <see cref="DrawableHitObject"/> has been judged.
|
||||
/// Note: This does NOT include nested hitobjects.
|
||||
/// </summary>
|
||||
public bool Judged => Result?.HasResult ?? true;
|
||||
public bool Judged => Entry?.Judged ?? true;
|
||||
|
||||
/// <summary>
|
||||
/// The scoring result of this <see cref="DrawableHitObject"/>.
|
||||
/// Whether this <see cref="DrawableHitObject"/> and all of its nested <see cref="DrawableHitObject"/>s have been judged.
|
||||
/// </summary>
|
||||
public JudgementResult Result => Entry?.Result;
|
||||
public bool AllJudged => Entry?.AllJudged ?? true;
|
||||
|
||||
/// <summary>
|
||||
/// The relative X position of this hit object for sample playback balance adjustment.
|
||||
@ -218,6 +218,8 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
|
||||
protected sealed override void OnApply(HitObjectLifetimeEntry entry)
|
||||
{
|
||||
Debug.Assert(Entry != null);
|
||||
|
||||
// LifetimeStart is already computed using HitObjectLifetimeEntry's InitialLifetimeOffset.
|
||||
// We override this with DHO's InitialLifetimeOffset for a non-pooled DHO.
|
||||
if (entry is SyntheticHitObjectEntry)
|
||||
@ -247,6 +249,12 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
drawableNested.ParentHitObject = this;
|
||||
|
||||
nestedHitObjects.Add(drawableNested);
|
||||
|
||||
// assume that synthetic entries are not pooled and therefore need to be managed from within the DHO.
|
||||
// this is important for the correctness of value of flags such as `AllJudged`.
|
||||
if (drawableNested.Entry is SyntheticHitObjectEntry syntheticNestedEntry)
|
||||
Entry.NestedEntries.Add(syntheticNestedEntry);
|
||||
|
||||
AddNestedHitObject(drawableNested);
|
||||
}
|
||||
|
||||
@ -290,6 +298,8 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
|
||||
protected sealed override void OnFree(HitObjectLifetimeEntry entry)
|
||||
{
|
||||
Debug.Assert(Entry != null);
|
||||
|
||||
StartTimeBindable.UnbindFrom(HitObject.StartTimeBindable);
|
||||
|
||||
if (HitObject is IHasComboInformation combo)
|
||||
@ -318,6 +328,8 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
}
|
||||
|
||||
nestedHitObjects.Clear();
|
||||
// clean up synthetic entries manually added in `Apply()`.
|
||||
Entry.NestedEntries.RemoveAll(nestedEntry => nestedEntry is SyntheticHitObjectEntry);
|
||||
ClearNestedHitObjects();
|
||||
|
||||
HitObject.DefaultsApplied -= onDefaultsApplied;
|
||||
|
@ -2,6 +2,8 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics.Performance;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
@ -19,12 +21,28 @@ namespace osu.Game.Rulesets.Objects
|
||||
/// </summary>
|
||||
public readonly HitObject HitObject;
|
||||
|
||||
/// <summary>
|
||||
/// The list of <see cref="HitObjectLifetimeEntry"/> for the <see cref="HitObject"/>'s nested objects (if any).
|
||||
/// </summary>
|
||||
public List<HitObjectLifetimeEntry> NestedEntries { get; internal set; } = new List<HitObjectLifetimeEntry>();
|
||||
|
||||
/// <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;
|
||||
|
||||
/// <summary>
|
||||
/// Whether <see cref="HitObject"/> has been judged.
|
||||
/// Note: This does NOT include nested hitobjects.
|
||||
/// </summary>
|
||||
public bool Judged => Result?.HasResult ?? true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether <see cref="HitObject"/> and all of its nested objects have been judged.
|
||||
/// </summary>
|
||||
public bool AllJudged => Judged && NestedEntries.All(h => h.AllJudged);
|
||||
|
||||
private readonly IBindable<double> startTimeBindable = new BindableDouble();
|
||||
|
||||
internal event Action? RevertResult;
|
||||
|
@ -43,11 +43,6 @@ namespace osu.Game.Rulesets.Objects.Pooling
|
||||
/// </remarks>
|
||||
private readonly Dictionary<HitObjectLifetimeEntry, HitObject> parentMap = new Dictionary<HitObjectLifetimeEntry, HitObject>();
|
||||
|
||||
/// <summary>
|
||||
/// Stores the list of child entries for each hit object managed by this <see cref="HitObjectEntryManager"/>.
|
||||
/// </summary>
|
||||
private readonly Dictionary<HitObject, List<HitObjectLifetimeEntry>> childrenMap = new Dictionary<HitObject, List<HitObjectLifetimeEntry>>();
|
||||
|
||||
public void Add(HitObjectLifetimeEntry entry, HitObject? parent)
|
||||
{
|
||||
HitObject hitObject = entry.HitObject;
|
||||
@ -57,22 +52,24 @@ namespace osu.Game.Rulesets.Objects.Pooling
|
||||
|
||||
// Add the entry.
|
||||
entryMap[hitObject] = entry;
|
||||
childrenMap[hitObject] = new List<HitObjectLifetimeEntry>();
|
||||
|
||||
// If the entry has a parent, set it and add the entry to the parent's children.
|
||||
if (parent != null)
|
||||
{
|
||||
parentMap[entry] = parent;
|
||||
if (childrenMap.TryGetValue(parent, out var parentChildEntries))
|
||||
parentChildEntries.Add(entry);
|
||||
if (entryMap.TryGetValue(parent, out var parentEntry))
|
||||
parentEntry.NestedEntries.Add(entry);
|
||||
}
|
||||
|
||||
hitObject.DefaultsApplied += onDefaultsApplied;
|
||||
OnEntryAdded?.Invoke(entry, parent);
|
||||
}
|
||||
|
||||
public void Remove(HitObjectLifetimeEntry entry)
|
||||
public bool Remove(HitObjectLifetimeEntry entry)
|
||||
{
|
||||
if (entry is SyntheticHitObjectEntry)
|
||||
return false;
|
||||
|
||||
HitObject hitObject = entry.HitObject;
|
||||
|
||||
if (!entryMap.ContainsKey(hitObject))
|
||||
@ -81,18 +78,16 @@ namespace osu.Game.Rulesets.Objects.Pooling
|
||||
entryMap.Remove(hitObject);
|
||||
|
||||
// If the entry has a parent, unset it and remove the entry from the parents' children.
|
||||
if (parentMap.Remove(entry, out var parent) && childrenMap.TryGetValue(parent, out var parentChildEntries))
|
||||
parentChildEntries.Remove(entry);
|
||||
if (parentMap.Remove(entry, out var parent) && entryMap.TryGetValue(parent, out var parentEntry))
|
||||
parentEntry.NestedEntries.Remove(entry);
|
||||
|
||||
// Remove all the entries' children.
|
||||
if (childrenMap.Remove(hitObject, out var childEntries))
|
||||
{
|
||||
foreach (var childEntry in childEntries)
|
||||
Remove(childEntry);
|
||||
}
|
||||
foreach (var childEntry in entry.NestedEntries)
|
||||
Remove(childEntry);
|
||||
|
||||
hitObject.DefaultsApplied -= onDefaultsApplied;
|
||||
OnEntryRemoved?.Invoke(entry, parent);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryGet(HitObject hitObject, [MaybeNullWhen(false)] out HitObjectLifetimeEntry entry)
|
||||
@ -105,16 +100,16 @@ namespace osu.Game.Rulesets.Objects.Pooling
|
||||
/// </summary>
|
||||
private void onDefaultsApplied(HitObject hitObject)
|
||||
{
|
||||
if (!childrenMap.Remove(hitObject, out var childEntries))
|
||||
if (!entryMap.TryGetValue(hitObject, out var entry))
|
||||
return;
|
||||
|
||||
// Remove all the entries' children. At this point the parents' (this entries') children list has been removed from the map, so this does not cause upwards traversal.
|
||||
foreach (var entry in childEntries)
|
||||
Remove(entry);
|
||||
// Replace the entire list rather than clearing to prevent circular traversal later.
|
||||
var previousEntries = entry.NestedEntries;
|
||||
entry.NestedEntries = new List<HitObjectLifetimeEntry>();
|
||||
|
||||
// The removed children list needs to be added back to the map for the entry to potentially receive children.
|
||||
childEntries.Clear();
|
||||
childrenMap[hitObject] = childEntries;
|
||||
// Remove all the entries' children. At this point the parents' (this entries') children list has been reconstructed, so this does not cause upwards traversal.
|
||||
foreach (var nested in previousEntries)
|
||||
Remove(nested);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user