From 757a4b5c319cf0b382e97db6ae5775c5ff5f1105 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 5 Nov 2020 19:47:23 +0900 Subject: [PATCH 01/55] Add hitobject lifetime model --- .../Objects/HitObjectLifetimeEntry.cs | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 osu.Game/Rulesets/Objects/HitObjectLifetimeEntry.cs diff --git a/osu.Game/Rulesets/Objects/HitObjectLifetimeEntry.cs b/osu.Game/Rulesets/Objects/HitObjectLifetimeEntry.cs new file mode 100644 index 0000000000..f134c66274 --- /dev/null +++ b/osu.Game/Rulesets/Objects/HitObjectLifetimeEntry.cs @@ -0,0 +1,98 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics.Performance; +using osu.Game.Rulesets.Objects.Drawables; + +namespace osu.Game.Rulesets.Objects +{ + /// + /// A that stores the lifetime for a . + /// + public class HitObjectLifetimeEntry : LifetimeEntry + { + /// + /// The . + /// + public readonly HitObject HitObject; + + /// + /// Creates a new . + /// + /// The to store the lifetime of. + public HitObjectLifetimeEntry(HitObject hitObject) + { + HitObject = hitObject; + ResetLifetimeStart(); + } + + // The lifetime start, as set by the hitobject. + private double realLifetimeStart = double.MinValue; + + /// + /// The time at which the should become alive. + /// + public new double LifetimeStart + { + get => realLifetimeStart; + set => setLifetime(realLifetimeStart = value, LifetimeEnd); + } + + // The lifetime end, as set by the hitobject. + private double realLifetimeEnd = double.MaxValue; + + /// + /// The time at which the should become dead. + /// + public new double LifetimeEnd + { + get => realLifetimeEnd; + set => setLifetime(LifetimeStart, realLifetimeEnd = value); + } + + private void setLifetime(double start, double end) + { + if (keepAlive) + { + start = double.MinValue; + end = double.MaxValue; + } + + base.LifetimeStart = start; + base.LifetimeEnd = end; + } + + private bool keepAlive; + + /// + /// Whether the should be kept always alive. + /// + internal bool KeepAlive + { + set + { + if (keepAlive == value) + return; + + keepAlive = value; + setLifetime(realLifetimeStart, realLifetimeEnd); + } + } + + /// + /// A safe offset prior to the start time of at which it may begin displaying contents. + /// By default, s are assumed to display their contents within 10 seconds prior to their start time. + /// + /// + /// This is only used as an optimisation to delay the initial update of the and may be tuned more aggressively if required. + /// It is indirectly used to decide the automatic transform offset provided to . + /// A more accurate should be set for further optimisation (in , for example). + /// + protected virtual double InitialLifetimeOffset => 10000; + + /// + /// Resets according to the start time of the . + /// + internal void ResetLifetimeStart() => LifetimeStart = HitObject.StartTime - InitialLifetimeOffset; + } +} From 45e9f16f6b3a3300ce5779c032dbb1e51873a29a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 6 Nov 2020 22:09:54 +0900 Subject: [PATCH 02/55] Add initial DrawableRuleset interface --- osu.Game/Rulesets/UI/DrawableRuleset.cs | 77 +++++++++++++++++++++++-- 1 file changed, 72 insertions(+), 5 deletions(-) diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index f6cf836fe7..8c6db661b5 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -15,7 +15,9 @@ using System.Linq; using System.Threading; using JetBrains.Annotations; using osu.Framework.Bindables; +using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Pooling; using osu.Framework.Input; using osu.Framework.Input.Events; using osu.Game.Configuration; @@ -92,11 +94,8 @@ namespace osu.Game.Rulesets.UI protected IRulesetConfigManager Config { get; private set; } - /// - /// The mods which are to be applied. - /// [Cached(typeof(IReadOnlyList))] - protected readonly IReadOnlyList Mods; + protected override IReadOnlyList Mods { get; } private FrameStabilityContainer frameStabilityContainer; @@ -284,12 +283,15 @@ namespace osu.Game.Rulesets.UI } } + public sealed override DrawableHitObject GetDrawableRepresentation(HitObject hitObject) + => base.GetDrawableRepresentation(hitObject) ?? CreateDrawableRepresentation((TObject)hitObject); + /// /// Creates a DrawableHitObject from a HitObject. /// /// The HitObject to make drawable. /// The DrawableHitObject. - public abstract DrawableHitObject CreateDrawableRepresentation(TObject h); + public virtual DrawableHitObject CreateDrawableRepresentation(TObject h) => null; public void Attach(KeyCounterDisplay keyCounter) => (KeyBindingInputManager as ICanAttachKeyCounter)?.Attach(keyCounter); @@ -406,6 +408,11 @@ namespace osu.Game.Rulesets.UI /// public abstract IFrameStableClock FrameStableClock { get; } + /// + /// The mods which are to be applied. + /// + protected abstract IReadOnlyList Mods { get; } + /// ~ /// The associated ruleset. /// @@ -500,6 +507,66 @@ namespace osu.Game.Rulesets.UI /// Invoked when the user requests to pause while the resume overlay is active. /// public abstract void CancelResume(); + + /// + /// Whether this should retrieve pooled s. + /// + /// + /// Pools must be registered with this via in order for s to be retrieved. + /// + /// If true, hitobjects will be added to the via . + /// If false, will be used instead. + /// + /// + protected virtual bool PoolHitObjects => false; + + private readonly Dictionary pools = new Dictionary(); + + protected void RegisterPool(int initialSize, int? maximumSize = null) + where TObject : HitObject + where TDrawable : DrawableHitObject, new() + { + var pool = CreatePool(initialSize, maximumSize); + pools[typeof(TObject)] = pool; + AddInternal(pool); + } + + /// + /// Creates the to retrieve s of the given type from. + /// + /// The number of hitobject to be prepared for initial consumption. + /// An optional maximum size after which the pool will no longer be expanded. + /// The type of retrievable from this pool. + /// The . + protected virtual DrawablePool CreatePool(int initialSize, int? maximumSize = null) + where TDrawable : DrawableHitObject, new() + => new DrawablePool(initialSize, maximumSize); + + /// + /// Retrieves the drawable representation of a . + /// + /// The to retrieve the drawable representation of. + /// The representing . + public virtual DrawableHitObject GetDrawableRepresentation(HitObject hitObject) + { + if (!pools.TryGetValue(hitObject.GetType(), out var pool)) + return null; + + return (DrawableHitObject)pool.Get(d => + { + var dho = (DrawableHitObject)d; + + // If this is the first time this DHO is being used (not loaded), then apply the DHO mods. + // This is done before Apply() so that the state is updated once when the hitobject is applied. + if (!dho.IsLoaded) + { + foreach (var m in Mods.OfType()) + m.ApplyToDrawableHitObjects(dho.Yield()); + } + + dho.Apply(hitObject); + }); + } } public class BeatmapInvalidForRulesetException : ArgumentException From 6f3f6dc28b88c2ef5455c80cc6e81ac31423024c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 10 Nov 2020 20:16:52 +0900 Subject: [PATCH 03/55] Add hitobject lifetime support --- .../TestSceneHitCircleApplication.cs | 2 +- .../TestSceneSliderApplication.cs | 2 +- .../TestSceneSpinnerApplication.cs | 2 +- .../Objects/Drawables/DrawableHitObject.cs | 53 +++++++++++++++---- .../Objects/HitObjectLifetimeEntry.cs | 18 +++++-- osu.Game/Rulesets/UI/DrawableRuleset.cs | 25 ++++++++- 6 files changed, 86 insertions(+), 16 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleApplication.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleApplication.cs index 8b3fead366..5fc1082743 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleApplication.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleApplication.cs @@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Osu.Tests { Position = new Vector2(128, 128), ComboIndex = 1, - }))); + }), null)); } private HitCircle prepareObject(HitCircle circle) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs index f76c7e2a3e..fb1ebbb0d0 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs @@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Osu.Tests new Vector2(300, 0), }), RepeatCount = 1 - }))); + }), null)); } private Slider prepareObject(Slider slider) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerApplication.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerApplication.cs index 5951574079..0558dad30d 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerApplication.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerApplication.cs @@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Osu.Tests Position = new Vector2(256, 192), ComboIndex = 1, Duration = 1000, - }))); + }), null)); } private Spinner prepareObject(Spinner circle) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 7a4e136553..2ac478f640 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -120,6 +120,12 @@ namespace osu.Game.Rulesets.Objects.Drawables /// private bool hasHitObjectApplied; + /// + /// The controlling the lifetime of the currently-attached . + /// + [CanBeNull] + private HitObjectLifetimeEntry lifetimeEntry; + /// /// Creates a new . /// @@ -143,7 +149,7 @@ namespace osu.Game.Rulesets.Objects.Drawables base.LoadAsyncComplete(); if (HitObject != null) - Apply(HitObject); + Apply(HitObject, lifetimeEntry); } protected override void LoadComplete() @@ -160,16 +166,33 @@ namespace osu.Game.Rulesets.Objects.Drawables /// Applies a new to be represented by this . /// /// The to apply. - public void Apply(HitObject hitObject) + /// The controlling the lifetime of . + public void Apply([NotNull] HitObject hitObject, [CanBeNull] HitObjectLifetimeEntry lifetimeEntry) { free(); HitObject = hitObject ?? throw new InvalidOperationException($"Cannot apply a null {nameof(HitObject)}."); + this.lifetimeEntry = lifetimeEntry; + + if (lifetimeEntry != null) + { + // Transfer lifetime from the entry. + LifetimeStart = lifetimeEntry.LifetimeStart; + LifetimeEnd = lifetimeEntry.LifetimeEnd; + + // Copy any existing result from the entry (required for rewind / judgement revert). + Result = lifetimeEntry.Result; + } + // Ensure this DHO has a result. Result ??= CreateResult(HitObject.CreateJudgement()) ?? throw new InvalidOperationException($"{GetType().ReadableName()} must provide a {nameof(JudgementResult)} through {nameof(CreateResult)}."); + // Copy back the result to the entry for potential future retrieval. + if (lifetimeEntry != null) + lifetimeEntry.Result = Result; + foreach (var h in HitObject.NestedHitObjects) { var drawableNested = CreateNestedHitObject(h) ?? throw new InvalidOperationException($"{nameof(CreateNestedHitObject)} returned null for {h.GetType().ReadableName()}."); @@ -302,7 +325,7 @@ namespace osu.Game.Rulesets.Objects.Drawables private void onDefaultsApplied(HitObject hitObject) { - Apply(hitObject); + Apply(hitObject, lifetimeEntry); DefaultsApplied?.Invoke(this); } @@ -549,15 +572,27 @@ namespace osu.Game.Rulesets.Objects.Drawables /// protected internal new ScheduledDelegate Schedule(Action action) => base.Schedule(action); - private double? lifetimeStart; - public override double LifetimeStart { - get => lifetimeStart ?? (HitObject.StartTime - InitialLifetimeOffset); - set + get => base.LifetimeStart; + set => setLifetime(value, LifetimeEnd); + } + + public override double LifetimeEnd + { + get => base.LifetimeEnd; + set => setLifetime(LifetimeStart, value); + } + + private void setLifetime(double lifetimeStart, double lifetimeEnd) + { + base.LifetimeStart = lifetimeStart; + base.LifetimeEnd = lifetimeEnd; + + if (lifetimeEntry != null) { - lifetimeStart = value; - base.LifetimeStart = value; + lifetimeEntry.LifetimeStart = lifetimeStart; + lifetimeEntry.LifetimeEnd = lifetimeEnd; } } diff --git a/osu.Game/Rulesets/Objects/HitObjectLifetimeEntry.cs b/osu.Game/Rulesets/Objects/HitObjectLifetimeEntry.cs index f134c66274..1954d7e6d2 100644 --- a/osu.Game/Rulesets/Objects/HitObjectLifetimeEntry.cs +++ b/osu.Game/Rulesets/Objects/HitObjectLifetimeEntry.cs @@ -1,7 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Bindables; using osu.Framework.Graphics.Performance; +using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects.Drawables; namespace osu.Game.Rulesets.Objects @@ -16,6 +18,14 @@ namespace osu.Game.Rulesets.Objects /// public readonly HitObject HitObject; + /// + /// The result that was judged with. + /// This is set by the accompanying , and reused when required for rewinding. + /// + internal JudgementResult Result; + + private readonly IBindable startTimeBindable = new BindableDouble(); + /// /// Creates a new . /// @@ -23,7 +33,9 @@ namespace osu.Game.Rulesets.Objects public HitObjectLifetimeEntry(HitObject hitObject) { HitObject = hitObject; - ResetLifetimeStart(); + + startTimeBindable.BindTo(HitObject.StartTimeBindable); + startTimeBindable.BindValueChanged(onStartTimeChanged, true); } // The lifetime start, as set by the hitobject. @@ -91,8 +103,8 @@ namespace osu.Game.Rulesets.Objects protected virtual double InitialLifetimeOffset => 10000; /// - /// Resets according to the start time of the . + /// Resets according to the change in start time of the . /// - internal void ResetLifetimeStart() => LifetimeStart = HitObject.StartTime - InitialLifetimeOffset; + private void onStartTimeChanged(ValueChangedEvent startTime) => LifetimeStart = HitObject.StartTime - InitialLifetimeOffset; } } diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index 8c6db661b5..33c422adb8 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -16,6 +16,7 @@ using System.Threading; using JetBrains.Annotations; using osu.Framework.Bindables; using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Pooling; using osu.Framework.Input; @@ -246,6 +247,16 @@ namespace osu.Game.Rulesets.UI Playfield.Add(drawableObject); } + protected sealed override HitObjectLifetimeEntry CreateLifetimeEntry(HitObject hitObject) + { + if (!(hitObject is TObject tHitObject)) + throw new InvalidOperationException($"Unexpected hitobject type: {hitObject.GetType().ReadableName()}"); + + return CreateLifetimeEntry(tHitObject); + } + + protected virtual HitObjectLifetimeEntry CreateLifetimeEntry(TObject hitObject) => new HitObjectLifetimeEntry(hitObject); + public override void SetRecordTarget(Replay recordingReplay) { if (!(KeyBindingInputManager is IHasRecordingHandler recordingInputManager)) @@ -564,9 +575,21 @@ namespace osu.Game.Rulesets.UI m.ApplyToDrawableHitObjects(dho.Yield()); } - dho.Apply(hitObject); + dho.Apply(hitObject, GetLifetimeEntry(hitObject)); }); } + + protected abstract HitObjectLifetimeEntry CreateLifetimeEntry(HitObject hitObject); + + private readonly Dictionary lifetimeEntries = new Dictionary(); + + protected HitObjectLifetimeEntry GetLifetimeEntry(HitObject hitObject) + { + if (lifetimeEntries.TryGetValue(hitObject, out var entry)) + return entry; + + return lifetimeEntries[hitObject] = CreateLifetimeEntry(hitObject); + } } public class BeatmapInvalidForRulesetException : ArgumentException From 31e4d71852e34f6daa15fbf9d180462ab232f8c3 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 10 Nov 2020 22:49:02 +0900 Subject: [PATCH 04/55] Rewrite HitObjectContainer with pooling support --- .../Objects/Drawables/DrawableHitObject.cs | 8 +- osu.Game/Rulesets/UI/HitObjectContainer.cs | 213 ++++++++++++++---- 2 files changed, 168 insertions(+), 53 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 2ac478f640..5299e53d5c 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -100,7 +100,7 @@ namespace osu.Game.Rulesets.Objects.Drawables /// protected virtual float SamplePlaybackPosition => 0.5f; - private readonly Bindable startTimeBindable = new Bindable(); + public readonly Bindable StartTimeBindable = new Bindable(); private readonly BindableList samplesBindable = new BindableList(); private readonly Bindable userPositionalHitSounds = new Bindable(); private readonly Bindable comboIndexBindable = new Bindable(); @@ -156,7 +156,7 @@ namespace osu.Game.Rulesets.Objects.Drawables { base.LoadComplete(); - startTimeBindable.BindValueChanged(_ => updateState(State.Value, true)); + StartTimeBindable.BindValueChanged(_ => updateState(State.Value, true)); comboIndexBindable.BindValueChanged(_ => updateComboColour(), true); updateState(ArmedState.Idle, true); @@ -205,7 +205,7 @@ namespace osu.Game.Rulesets.Objects.Drawables AddNestedHitObject(drawableNested); } - startTimeBindable.BindTo(HitObject.StartTimeBindable); + StartTimeBindable.BindTo(HitObject.StartTimeBindable); if (HitObject is IHasComboInformation combo) comboIndexBindable.BindTo(combo.ComboIndexBindable); @@ -231,7 +231,7 @@ namespace osu.Game.Rulesets.Objects.Drawables if (!hasHitObjectApplied) return; - startTimeBindable.UnbindFrom(HitObject.StartTimeBindable); + StartTimeBindable.UnbindFrom(HitObject.StartTimeBindable); if (HitObject is IHasComboInformation combo) comboIndexBindable.UnbindFrom(combo.ComboIndexBindable); diff --git a/osu.Game/Rulesets/UI/HitObjectContainer.cs b/osu.Game/Rulesets/UI/HitObjectContainer.cs index 4cadfa9ad4..97604f62c8 100644 --- a/osu.Game/Rulesets/UI/HitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/HitObjectContainer.cs @@ -1,35 +1,132 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; +using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Performance; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; namespace osu.Game.Rulesets.UI { public class HitObjectContainer : LifetimeManagementContainer { - public IEnumerable Objects => InternalChildren.Cast().OrderBy(h => h.HitObject.StartTime); - public IEnumerable AliveObjects => AliveInternalChildren.Cast().OrderBy(h => h.HitObject.StartTime); + /// + /// All currently in-use s. + /// + public IEnumerable Objects => InternalChildren.OfType().OrderBy(h => h.HitObject.StartTime); - private readonly Dictionary bindable, double timeAtAdd)> startTimeMap = new Dictionary, double)>(); + /// + /// All currently in-use s that are alive. + /// + /// + /// If this uses pooled objects, this is equivalent to . + /// + public IEnumerable AliveObjects => AliveInternalChildren.OfType().OrderBy(h => h.HitObject.StartTime); + + public event Action NewResult; + public event Action RevertResult; + + /// + /// Invoked when a becomes used by a . + /// + /// + /// If this uses pooled objects, this represents the time when the s become alive. + /// + public event Action HitObjectUsageBegan; + + /// + /// Invoked when a becomes unused by a . + /// + /// + /// If this uses pooled objects, this represents the time when the s become dead. + /// + public event Action HitObjectUsageFinished; + + /// + /// The amount of time prior to the current time within which s should be considered alive. + /// + public double PastLifetimeExtension { get; set; } + + /// + /// The amount of time after the current time within which s should be considered alive. + /// + public double FutureLifetimeExtension { get; set; } + + private readonly Dictionary startTimeMap = new Dictionary(); + private readonly Dictionary drawableMap = new Dictionary(); + private readonly LifetimeEntryManager lifetimeManager = new LifetimeEntryManager(); + + [Resolved(CanBeNull = true)] + private DrawableRuleset drawableRuleset { get; set; } public HitObjectContainer() { RelativeSizeAxes = Axes.Both; + + lifetimeManager.EntryBecameAlive += entryBecameAlive; + lifetimeManager.EntryBecameDead += entryBecameDead; } + #region Pooling support + + public void Add(HitObjectLifetimeEntry entry) => lifetimeManager.AddEntry(entry); + + public void 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.GetDrawableRepresentation(entry.HitObject); + drawable.OnNewResult += onNewResult; + drawable.OnRevertResult += onRevertResult; + + bindStartTime(drawable); + AddInternal(drawableMap[entry] = drawable, false); + + HitObjectUsageBegan?.Invoke(entry.HitObject); + } + + private void removeDrawable(HitObjectLifetimeEntry entry) + { + Debug.Assert(drawableMap.ContainsKey(entry)); + + var drawable = drawableMap[entry]; + drawable.OnNewResult -= onNewResult; + drawable.OnRevertResult -= onRevertResult; + drawable.OnKilled(); + + drawableMap.Remove(entry); + + unbindStartTime(drawable); + RemoveInternal(drawable); + + HitObjectUsageFinished?.Invoke(entry.HitObject); + } + + #endregion + + #region Non-pooling support + public virtual void Add(DrawableHitObject hitObject) { - // Added first for the comparer to remain ordered during AddInternal - startTimeMap[hitObject] = (hitObject.HitObject.StartTimeBindable.GetBoundCopy(), hitObject.HitObject.StartTime); - startTimeMap[hitObject].bindable.BindValueChanged(_ => onStartTimeChanged(hitObject)); - + bindStartTime(hitObject); AddInternal(hitObject); + + hitObject.OnNewResult += onNewResult; + hitObject.OnRevertResult += onRevertResult; } public virtual bool Remove(DrawableHitObject hitObject) @@ -37,54 +134,16 @@ namespace osu.Game.Rulesets.UI if (!RemoveInternal(hitObject)) return false; - // Removed last for the comparer to remain ordered during RemoveInternal - startTimeMap[hitObject].bindable.UnbindAll(); - startTimeMap.Remove(hitObject); + hitObject.OnNewResult -= onNewResult; + hitObject.OnRevertResult -= onRevertResult; + + unbindStartTime(hitObject); return true; } - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - unbindStartTimeMap(); - } - - public virtual void Clear(bool disposeChildren = true) - { - ClearInternal(disposeChildren); - unbindStartTimeMap(); - } - - private void unbindStartTimeMap() - { - foreach (var kvp in startTimeMap) - kvp.Value.bindable.UnbindAll(); - startTimeMap.Clear(); - } - public int IndexOf(DrawableHitObject hitObject) => IndexOfInternal(hitObject); - private void onStartTimeChanged(DrawableHitObject hitObject) - { - if (!RemoveInternal(hitObject)) - return; - - // Update the stored time, preserving the existing bindable - startTimeMap[hitObject] = (startTimeMap[hitObject].bindable, hitObject.HitObject.StartTime); - AddInternal(hitObject); - } - - protected override int Compare(Drawable x, Drawable y) - { - if (!(x is DrawableHitObject xObj) || !(y is DrawableHitObject yObj)) - return base.Compare(x, y); - - // Put earlier hitobjects towards the end of the list, so they handle input first - int i = startTimeMap[yObj].timeAtAdd.CompareTo(startTimeMap[xObj].timeAtAdd); - return i == 0 ? CompareReverseChildID(x, y) : i; - } - protected override void OnChildLifetimeBoundaryCrossed(LifetimeBoundaryCrossedEvent e) { if (!(e.Child is DrawableHitObject hitObject)) @@ -96,5 +155,61 @@ namespace osu.Game.Rulesets.UI hitObject.OnKilled(); } } + + #endregion + + public virtual void Clear(bool disposeChildren = true) + { + lifetimeManager.ClearEntries(); + + ClearInternal(disposeChildren); + unbindAllStartTimes(); + } + + protected override bool CheckChildrenLife() => base.CheckChildrenLife() | lifetimeManager.Update(Time.Current - PastLifetimeExtension, Time.Current + FutureLifetimeExtension); + + 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(_ => 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(); + } } } From 66213f2ed0903dd21809943a10e04aca3f2bf069 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 10 Nov 2020 23:32:30 +0900 Subject: [PATCH 05/55] Add pooling support to DrawableRuleset + Playfield --- osu.Game/Rulesets/UI/DrawableRuleset.cs | 58 ++++----- osu.Game/Rulesets/UI/HitObjectContainer.cs | 9 +- osu.Game/Rulesets/UI/Playfield.cs | 136 ++++++++++++++++++++- osu.Game/Screens/Play/Player.cs | 4 +- 4 files changed, 173 insertions(+), 34 deletions(-) diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index 33c422adb8..87a04312ab 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -41,9 +41,8 @@ namespace osu.Game.Rulesets.UI public abstract class DrawableRuleset : DrawableRuleset, IProvideCursor, ICanAttachKeyCounter where TObject : HitObject { - public override event Action OnNewResult; - - public override event Action OnRevertResult; + public override event Action NewResult; + public override event Action RevertResult; /// /// The selected variant. @@ -125,7 +124,11 @@ namespace osu.Game.Rulesets.UI RelativeSizeAxes = Axes.Both; KeyBindingInputManager = CreateInputManager(); - playfield = new Lazy(CreatePlayfield); + playfield = new Lazy(() => CreatePlayfield().With(p => + { + p.NewResult += (_, r) => NewResult?.Invoke(r); + p.RevertResult += (_, r) => RevertResult?.Invoke(r); + })); IsPaused.ValueChanged += paused => { @@ -183,7 +186,7 @@ namespace osu.Game.Rulesets.UI RegenerateAutoplay(); - loadObjects(cancellationToken); + loadObjects(cancellationToken ?? default); } public void RegenerateAutoplay() @@ -196,15 +199,15 @@ namespace osu.Game.Rulesets.UI /// /// Creates and adds drawable representations of hit objects to the play field. /// - private void loadObjects(CancellationToken? cancellationToken) + private void loadObjects(CancellationToken cancellationToken) { foreach (TObject h in Beatmap.HitObjects) { - cancellationToken?.ThrowIfCancellationRequested(); - addHitObject(h); + cancellationToken.ThrowIfCancellationRequested(); + AddHitObject(h); } - cancellationToken?.ThrowIfCancellationRequested(); + cancellationToken.ThrowIfCancellationRequested(); Playfield.PostProcess(); @@ -230,21 +233,24 @@ namespace osu.Game.Rulesets.UI ResumeOverlay?.Hide(); } - /// - /// Creates and adds the visual representation of a to this . - /// - /// The to add the visual representation for. - private void addHitObject(TObject hitObject) + public void AddHitObject(TObject hitObject) { - var drawableObject = CreateDrawableRepresentation(hitObject); + if (PoolHitObjects) + Playfield.Add(GetLifetimeEntry(hitObject)); + else + Playfield.Add(CreateDrawableRepresentation(hitObject)); + } - if (drawableObject == null) - return; - - drawableObject.OnNewResult += (_, r) => OnNewResult?.Invoke(r); - drawableObject.OnRevertResult += (_, r) => OnRevertResult?.Invoke(r); - - Playfield.Add(drawableObject); + public void RemoveHitObject(TObject hitObject) + { + if (PoolHitObjects) + Playfield.Remove(GetLifetimeEntry(hitObject)); + else + { + var drawableObject = Playfield.AllHitObjects.SingleOrDefault(d => d.HitObject == hitObject); + if (drawableObject != null) + Playfield.Remove(drawableObject); + } } protected sealed override HitObjectLifetimeEntry CreateLifetimeEntry(HitObject hitObject) @@ -382,12 +388,12 @@ namespace osu.Game.Rulesets.UI /// /// Invoked when a has been applied by a . /// - public abstract event Action OnNewResult; + public abstract event Action NewResult; /// /// Invoked when a is being reverted by a . /// - public abstract event Action OnRevertResult; + public abstract event Action RevertResult; /// /// Whether a replay is currently loaded. @@ -524,10 +530,6 @@ namespace osu.Game.Rulesets.UI /// /// /// Pools must be registered with this via in order for s to be retrieved. - /// - /// If true, hitobjects will be added to the via . - /// If false, will be used instead. - /// /// protected virtual bool PoolHitObjects => false; diff --git a/osu.Game/Rulesets/UI/HitObjectContainer.cs b/osu.Game/Rulesets/UI/HitObjectContainer.cs index 97604f62c8..3a5e0b64ed 100644 --- a/osu.Game/Rulesets/UI/HitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/HitObjectContainer.cs @@ -31,7 +31,14 @@ namespace osu.Game.Rulesets.UI /// public IEnumerable AliveObjects => AliveInternalChildren.OfType().OrderBy(h => h.HitObject.StartTime); + /// + /// Invoked when a is judged. + /// public event Action NewResult; + + /// + /// Invoked when a judgement is reverted. + /// public event Action RevertResult; /// @@ -79,7 +86,7 @@ namespace osu.Game.Rulesets.UI public void Add(HitObjectLifetimeEntry entry) => lifetimeManager.AddEntry(entry); - public void Remove(HitObjectLifetimeEntry entry) => lifetimeManager.RemoveEntry(entry); + public bool Remove(HitObjectLifetimeEntry entry) => lifetimeManager.RemoveEntry(entry); private void entryBecameAlive(LifetimeEntry entry) => addDrawable((HitObjectLifetimeEntry)entry); diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index d92ba210db..2bd2bb9e06 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -10,13 +10,41 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects; using osuTK; namespace osu.Game.Rulesets.UI { public abstract class Playfield : CompositeDrawable { + /// + /// Invoked when a is judged. + /// + public event Action NewResult; + + /// + /// Invoked when a judgement is reverted. + /// + public event Action RevertResult; + + /// + /// Invoked when a becomes used by a . + /// + /// + /// If this uses pooled objects, this represents the time when the s become alive. + /// + public event Action HitObjectUsageBegan; + + /// + /// Invoked when a becomes unused by a . + /// + /// + /// If this uses pooled objects, this represents the time when the s become dead. + /// + public event Action HitObjectUsageFinished; + /// /// The contained in this Playfield. /// @@ -72,7 +100,13 @@ namespace osu.Game.Rulesets.UI { RelativeSizeAxes = Axes.Both; - hitObjectContainerLazy = new Lazy(CreateHitObjectContainer); + hitObjectContainerLazy = new Lazy(() => CreateHitObjectContainer().With(h => + { + h.NewResult += (d, r) => NewResult?.Invoke(d, r); + h.RevertResult += (d, r) => RevertResult?.Invoke(d, r); + h.HitObjectUsageBegan += o => HitObjectUsageBegan?.Invoke(o); + h.HitObjectUsageFinished += o => HitObjectUsageFinished?.Invoke(o); + })); } [Resolved(CanBeNull = true)] @@ -101,13 +135,103 @@ namespace osu.Game.Rulesets.UI /// Adds a DrawableHitObject to this Playfield. /// /// The DrawableHitObject to add. - public virtual void Add(DrawableHitObject h) => HitObjectContainer.Add(h); + public virtual void Add(DrawableHitObject h) + { + HitObjectContainer.Add(h); + + h.OnNewResult += (d, r) => NewResult?.Invoke(d, r); + h.OnRevertResult += (d, r) => RevertResult?.Invoke(d, r); + + OnHitObjectAdded(h.HitObject); + } /// /// Remove a DrawableHitObject from this Playfield. /// /// The DrawableHitObject to remove. - public virtual bool Remove(DrawableHitObject h) => HitObjectContainer.Remove(h); + public virtual bool Remove(DrawableHitObject h) + { + if (!HitObjectContainer.Remove(h)) + return false; + + OnHitObjectRemoved(h.HitObject); + return false; + } + + private readonly Dictionary lifetimeEntryMap = new Dictionary(); + + /// + /// Adds a to this . + /// + /// The controlling the lifetime of the . + public void Add(HitObjectLifetimeEntry entry) + { + HitObjectContainer.Add(entry); + lifetimeEntryMap[entry.HitObject] = entry; + OnHitObjectAdded(entry.HitObject); + } + + /// + /// Removes a to this . + /// + /// The controlling the lifetime of the . + public void Remove(HitObjectLifetimeEntry entry) + { + if (HitObjectContainer.Remove(entry)) + OnHitObjectRemoved(entry.HitObject); + lifetimeEntryMap.Remove(entry.HitObject); + } + + /// + /// Invoked when a is added to this . + /// + /// The added . + protected virtual void OnHitObjectAdded(HitObject hitObject) + { + } + + /// + /// Invoked when a is removed from this . + /// + /// The removed . + protected virtual void OnHitObjectRemoved(HitObject hitObject) + { + } + + /// + /// Sets whether to keep a given always alive within this or any nested . + /// + /// The to set. + /// Whether to keep always alive. + public void SetKeepAlive(HitObject hitObject, bool keepAlive) + { + if (lifetimeEntryMap.TryGetValue(hitObject, out var entry)) + { + entry.KeepAlive = keepAlive; + return; + } + + if (!nestedPlayfields.IsValueCreated) + return; + + foreach (var p in nestedPlayfields.Value) + p.SetKeepAlive(hitObject, keepAlive); + } + + /// + /// Keeps all s alive within this and all nested s. + /// + public void KeepAllAlive() + { + foreach (var (_, entry) in lifetimeEntryMap) + entry.KeepAlive = true; + + if (!nestedPlayfields.IsValueCreated) + return; + + foreach (var p in nestedPlayfields.Value) + p.KeepAllAlive(); + } /// /// The cursor currently being used by this . May be null if no cursor is provided. @@ -131,6 +255,12 @@ namespace osu.Game.Rulesets.UI protected void AddNested(Playfield otherPlayfield) { otherPlayfield.DisplayJudgements.BindTo(DisplayJudgements); + + otherPlayfield.NewResult += (d, r) => NewResult?.Invoke(d, r); + otherPlayfield.RevertResult += (d, r) => RevertResult?.Invoke(d, r); + otherPlayfield.HitObjectUsageBegan += h => HitObjectUsageBegan?.Invoke(h); + otherPlayfield.HitObjectUsageFinished += h => HitObjectUsageFinished?.Invoke(h); + nestedPlayfields.Value.Add(otherPlayfield); } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index f9af1818d0..ee4f835c6f 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -261,14 +261,14 @@ namespace osu.Game.Screens.Play // bind clock into components that require it DrawableRuleset.IsPaused.BindTo(GameplayClockContainer.IsPaused); - DrawableRuleset.OnNewResult += r => + DrawableRuleset.NewResult += r => { HealthProcessor.ApplyResult(r); ScoreProcessor.ApplyResult(r); gameplayBeatmap.ApplyResult(r); }; - DrawableRuleset.OnRevertResult += r => + DrawableRuleset.RevertResult += r => { HealthProcessor.RevertResult(r); ScoreProcessor.RevertResult(r); From 99e5450af3930eba7f9431179c8afafe45686a0d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 11 Nov 2020 00:22:36 +0900 Subject: [PATCH 06/55] Cache DrawableRuleset --- osu.Game/Rulesets/UI/DrawableRuleset.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index 87a04312ab..b78cfe9086 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -383,6 +383,7 @@ namespace osu.Game.Rulesets.UI /// Once IDrawable is a thing, this can also become an interface. /// /// + [Cached(typeof(DrawableRuleset))] public abstract class DrawableRuleset : CompositeDrawable { /// From e525784cb2deb8287e3727d3ebc6ed2e639a635a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 11 Nov 2020 00:24:14 +0900 Subject: [PATCH 07/55] Clear lifetimeEntry after use --- osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 5299e53d5c..244cf831c3 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -259,6 +259,8 @@ namespace osu.Game.Rulesets.Objects.Drawables OnFree(HitObject); HitObject = null; + lifetimeEntry = null; + hasHitObjectApplied = false; } From b725c9cce9e938063a64ee2a5709ce5859c8e59e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 11 Nov 2020 00:24:28 +0900 Subject: [PATCH 08/55] Fix possible nullrefs --- osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 244cf831c3..bcf1103f39 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -613,13 +613,13 @@ namespace osu.Game.Rulesets.Objects.Drawables /// The time at which state transforms should be applied that line up to 's StartTime. /// This is used to offset calls to . /// - public double StateUpdateTime => HitObject.StartTime; + public double StateUpdateTime => HitObject?.StartTime ?? 0; /// /// The time at which judgement dependent state transforms should be applied. This is equivalent of the (end) time of the object, in addition to any judgement offset. /// This is used to offset calls to . /// - public double HitStateUpdateTime => Result?.TimeAbsolute ?? HitObject.GetEndTime(); + public double HitStateUpdateTime => Result?.TimeAbsolute ?? HitObject?.GetEndTime() ?? 0; /// /// Will be called at least once after this has become not alive. From 3401b099d4dadbb92ee3471be3ac9f5774684f1e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 11 Nov 2020 18:50:08 +0900 Subject: [PATCH 09/55] Add some tests --- .../Gameplay/TestScenePoolingRuleset.cs | 247 ++++++++++++++++++ 1 file changed, 247 insertions(+) create mode 100644 osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs new file mode 100644 index 0000000000..bb09aec416 --- /dev/null +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs @@ -0,0 +1,247 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input; +using osu.Framework.Testing; +using osu.Framework.Timing; +using osu.Framework.Utils; +using osu.Game.Beatmaps; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Difficulty; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Objects.Legacy; +using osu.Game.Rulesets.UI; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Tests.Visual.Gameplay +{ + public class TestScenePoolingRuleset : OsuTestScene + { + private const double time_between_objects = 1000; + + private TestDrawablePoolingRuleset drawableRuleset; + + [Test] + public void TestReusedWithHitObjectsSpacedFarApart() + { + ManualClock clock = null; + + createTest(new Beatmap + { + HitObjects = + { + new HitObject(), + new HitObject { StartTime = time_between_objects } + } + }, 1, () => new FramedClock(clock = new ManualClock())); + + DrawableTestHitObject firstObject = null; + AddUntilStep("first object shown", () => this.ChildrenOfType().SingleOrDefault()?.HitObject == drawableRuleset.Beatmap.HitObjects[0]); + AddStep("get DHO", () => firstObject = this.ChildrenOfType().Single()); + + AddStep("fast forward to second object", () => clock.CurrentTime = drawableRuleset.Beatmap.HitObjects[1].StartTime); + + AddUntilStep("second object shown", () => this.ChildrenOfType().SingleOrDefault()?.HitObject == drawableRuleset.Beatmap.HitObjects[1]); + AddAssert("DHO reused", () => this.ChildrenOfType().Single() == firstObject); + } + + [Test] + public void TestNotReusedWithHitObjectsSpacedClose() + { + ManualClock clock = null; + + createTest(new Beatmap + { + HitObjects = + { + new HitObject(), + new HitObject { StartTime = 250 } + } + }, 2, () => new FramedClock(clock = new ManualClock())); + + AddStep("fast forward to second object", () => clock.CurrentTime = drawableRuleset.Beatmap.HitObjects[1].StartTime); + + AddUntilStep("two DHOs shown", () => this.ChildrenOfType().Count() == 2); + AddAssert("DHOs have different hitobjects", + () => this.ChildrenOfType().ElementAt(0).HitObject != this.ChildrenOfType().ElementAt(1).HitObject); + } + + [Test] + public void TestManyHitObjects() + { + var beatmap = new Beatmap(); + + for (int i = 0; i < 500; i++) + beatmap.HitObjects.Add(new HitObject { StartTime = i * 10 }); + + createTest(beatmap, 100); + + AddUntilStep("any DHOs shown", () => this.ChildrenOfType().Any()); + AddUntilStep("no DHOs shown", () => !this.ChildrenOfType().Any()); + } + + private void createTest(IBeatmap beatmap, int poolSize, Func createClock = null) => AddStep("create test", () => + { + var ruleset = new TestPoolingRuleset(); + + drawableRuleset = (TestDrawablePoolingRuleset)ruleset.CreateDrawableRulesetWith(CreateWorkingBeatmap(beatmap).GetPlayableBeatmap(ruleset.RulesetInfo)); + drawableRuleset.FrameStablePlayback = true; + drawableRuleset.PoolSize = poolSize; + + Child = new Container + { + RelativeSizeAxes = Axes.Both, + Clock = createClock?.Invoke() ?? new FramedOffsetClock(Clock, false) { Offset = -Clock.CurrentTime }, + Child = drawableRuleset + }; + }); + + #region Ruleset + + private class TestPoolingRuleset : Ruleset + { + public override IEnumerable GetModsFor(ModType type) => throw new NotImplementedException(); + + public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => new TestDrawablePoolingRuleset(this, beatmap, mods); + + public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new TestBeatmapConverter(beatmap, this); + + public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => throw new NotImplementedException(); + + public override string Description { get; } = string.Empty; + + public override string ShortName { get; } = string.Empty; + } + + private class TestDrawablePoolingRuleset : DrawableRuleset + { + public int PoolSize; + + public TestDrawablePoolingRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null) + : base(ruleset, beatmap, mods) + { + } + + [BackgroundDependencyLoader] + private void load() + { + RegisterPool(PoolSize); + } + + protected override bool PoolHitObjects => true; + + protected override HitObjectLifetimeEntry CreateLifetimeEntry(TestHitObject hitObject) => new TestHitObjectLifetimeEntry(hitObject); + + protected override PassThroughInputManager CreateInputManager() => new PassThroughInputManager(); + + protected override Playfield CreatePlayfield() => new TestPlayfield(); + + private class TestHitObjectLifetimeEntry : HitObjectLifetimeEntry + { + public TestHitObjectLifetimeEntry(HitObject hitObject) + : base(hitObject) + { + } + + protected override double InitialLifetimeOffset => 0; + } + } + + private class TestPlayfield : Playfield + { + public TestPlayfield() + { + AddInternal(HitObjectContainer); + } + + protected override GameplayCursorContainer CreateCursor() => null; + } + + private class TestBeatmapConverter : BeatmapConverter + { + public TestBeatmapConverter(IBeatmap beatmap, Ruleset ruleset) + : base(beatmap, ruleset) + { + } + + public override bool CanConvert() => true; + + protected override IEnumerable ConvertHitObject(HitObject original, IBeatmap beatmap, CancellationToken cancellationToken) + { + yield return new TestHitObject + { + StartTime = original.StartTime, + Duration = 250 + }; + } + } + + #endregion + + #region HitObject + + private class TestHitObject : ConvertHitObject + { + public double EndTime => StartTime + Duration; + + public double Duration { get; set; } + } + + private class DrawableTestHitObject : DrawableHitObject + { + public DrawableTestHitObject() + : base(null) + { + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + + Position = new Vector2(RNG.Next(-200, 200), RNG.Next(-200, 200)); + Size = new Vector2(50, 50); + + Colour = new Color4(RNG.NextSingle(), RNG.NextSingle(), RNG.NextSingle(), 1f); + } + + [BackgroundDependencyLoader] + private void load() + { + AddInternal(new Circle + { + RelativeSizeAxes = Axes.Both, + }); + } + + protected override void CheckForResult(bool userTriggered, double timeOffset) + { + if (timeOffset > HitObject.Duration) + ApplyResult(r => r.Type = r.Judgement.MaxResult); + } + + protected override void UpdateHitStateTransforms(ArmedState state) + { + base.UpdateHitStateTransforms(state); + + switch (state) + { + case ArmedState.Hit: + case ArmedState.Miss: + this.FadeOut(250); + break; + } + } + } + + #endregion + } +} From 7d020181343f942159ae21df57b3ca8d07aad9f6 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 11 Nov 2020 18:54:40 +0900 Subject: [PATCH 10/55] Remove some unnecessary implementations for now --- osu.Game/Rulesets/UI/HitObjectContainer.cs | 32 +------------ osu.Game/Rulesets/UI/Playfield.cs | 55 ---------------------- 2 files changed, 1 insertion(+), 86 deletions(-) diff --git a/osu.Game/Rulesets/UI/HitObjectContainer.cs b/osu.Game/Rulesets/UI/HitObjectContainer.cs index 3a5e0b64ed..a0c95898be 100644 --- a/osu.Game/Rulesets/UI/HitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/HitObjectContainer.cs @@ -41,32 +41,6 @@ namespace osu.Game.Rulesets.UI /// public event Action RevertResult; - /// - /// Invoked when a becomes used by a . - /// - /// - /// If this uses pooled objects, this represents the time when the s become alive. - /// - public event Action HitObjectUsageBegan; - - /// - /// Invoked when a becomes unused by a . - /// - /// - /// If this uses pooled objects, this represents the time when the s become dead. - /// - public event Action HitObjectUsageFinished; - - /// - /// The amount of time prior to the current time within which s should be considered alive. - /// - public double PastLifetimeExtension { get; set; } - - /// - /// The amount of time after the current time within which s should be considered alive. - /// - public double FutureLifetimeExtension { get; set; } - private readonly Dictionary startTimeMap = new Dictionary(); private readonly Dictionary drawableMap = new Dictionary(); private readonly LifetimeEntryManager lifetimeManager = new LifetimeEntryManager(); @@ -102,8 +76,6 @@ namespace osu.Game.Rulesets.UI bindStartTime(drawable); AddInternal(drawableMap[entry] = drawable, false); - - HitObjectUsageBegan?.Invoke(entry.HitObject); } private void removeDrawable(HitObjectLifetimeEntry entry) @@ -119,8 +91,6 @@ namespace osu.Game.Rulesets.UI unbindStartTime(drawable); RemoveInternal(drawable); - - HitObjectUsageFinished?.Invoke(entry.HitObject); } #endregion @@ -173,7 +143,7 @@ namespace osu.Game.Rulesets.UI unbindAllStartTimes(); } - protected override bool CheckChildrenLife() => base.CheckChildrenLife() | lifetimeManager.Update(Time.Current - PastLifetimeExtension, Time.Current + FutureLifetimeExtension); + protected override bool CheckChildrenLife() => base.CheckChildrenLife() | lifetimeManager.Update(Time.Current, Time.Current); private void onNewResult(DrawableHitObject d, JudgementResult r) => NewResult?.Invoke(d, r); private void onRevertResult(DrawableHitObject d, JudgementResult r) => RevertResult?.Invoke(d, r); diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index 2bd2bb9e06..cdaf9364af 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -29,22 +29,6 @@ namespace osu.Game.Rulesets.UI /// public event Action RevertResult; - /// - /// Invoked when a becomes used by a . - /// - /// - /// If this uses pooled objects, this represents the time when the s become alive. - /// - public event Action HitObjectUsageBegan; - - /// - /// Invoked when a becomes unused by a . - /// - /// - /// If this uses pooled objects, this represents the time when the s become dead. - /// - public event Action HitObjectUsageFinished; - /// /// The contained in this Playfield. /// @@ -104,8 +88,6 @@ namespace osu.Game.Rulesets.UI { 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); })); } @@ -198,41 +180,6 @@ namespace osu.Game.Rulesets.UI { } - /// - /// Sets whether to keep a given always alive within this or any nested . - /// - /// The to set. - /// Whether to keep always alive. - public void SetKeepAlive(HitObject hitObject, bool keepAlive) - { - if (lifetimeEntryMap.TryGetValue(hitObject, out var entry)) - { - entry.KeepAlive = keepAlive; - return; - } - - if (!nestedPlayfields.IsValueCreated) - return; - - foreach (var p in nestedPlayfields.Value) - p.SetKeepAlive(hitObject, keepAlive); - } - - /// - /// Keeps all s alive within this and all nested s. - /// - public void KeepAllAlive() - { - foreach (var (_, entry) in lifetimeEntryMap) - entry.KeepAlive = true; - - if (!nestedPlayfields.IsValueCreated) - return; - - foreach (var p in nestedPlayfields.Value) - p.KeepAllAlive(); - } - /// /// The cursor currently being used by this . May be null if no cursor is provided. /// @@ -258,8 +205,6 @@ namespace osu.Game.Rulesets.UI 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); } From 606cfacedf959e08727c8e642a35ef83bf8cad96 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 11 Nov 2020 19:01:12 +0900 Subject: [PATCH 11/55] Fix state update exception in a better way --- .../Objects/Drawables/Pieces/MainCirclePiece.cs | 11 ++++++++--- .../Rulesets/Objects/Drawables/DrawableHitObject.cs | 4 ++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/MainCirclePiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/MainCirclePiece.cs index 98432eb4fe..bf2236c945 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/MainCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/MainCirclePiece.cs @@ -51,9 +51,15 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces var drawableOsuObject = (DrawableOsuHitObject)drawableObject; state.BindTo(drawableObject.State); - state.BindValueChanged(updateState, true); - accentColour.BindTo(drawableObject.AccentColour); + indexInCurrentCombo.BindTo(drawableOsuObject.IndexInCurrentComboBindable); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + state.BindValueChanged(updateState, true); accentColour.BindValueChanged(colour => { explode.Colour = colour.NewValue; @@ -61,7 +67,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces circle.Colour = colour.NewValue; }, true); - indexInCurrentCombo.BindTo(drawableOsuObject.IndexInCurrentComboBindable); indexInCurrentCombo.BindValueChanged(index => number.Text = (index.NewValue + 1).ToString(), true); } diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index bcf1103f39..244cf831c3 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -613,13 +613,13 @@ namespace osu.Game.Rulesets.Objects.Drawables /// The time at which state transforms should be applied that line up to 's StartTime. /// This is used to offset calls to . /// - public double StateUpdateTime => HitObject?.StartTime ?? 0; + public double StateUpdateTime => HitObject.StartTime; /// /// The time at which judgement dependent state transforms should be applied. This is equivalent of the (end) time of the object, in addition to any judgement offset. /// This is used to offset calls to . /// - public double HitStateUpdateTime => Result?.TimeAbsolute ?? HitObject?.GetEndTime() ?? 0; + public double HitStateUpdateTime => Result?.TimeAbsolute ?? HitObject.GetEndTime(); /// /// Will be called at least once after this has become not alive. From 7fdaf69903cc8158850c70523054f09551c26a4f Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 11 Nov 2020 19:12:12 +0900 Subject: [PATCH 12/55] Add some more xmldocs --- osu.Game/Rulesets/UI/DrawableRuleset.cs | 28 +++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index 1120f11df2..2c5fce3e86 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -526,6 +526,9 @@ namespace osu.Game.Rulesets.UI /// public abstract void CancelResume(); + private readonly Dictionary pools = new Dictionary(); + private readonly Dictionary lifetimeEntries = new Dictionary(); + /// /// Whether this should retrieve pooled s. /// @@ -534,8 +537,14 @@ namespace osu.Game.Rulesets.UI /// protected virtual bool PoolHitObjects => false; - private readonly Dictionary pools = new Dictionary(); - + /// + /// Registers a pool with this which is to be used whenever + /// representations are requested for the given type (via ). + /// + /// The number of drawables to be prepared for initial consumption. + /// An optional maximum size after which the pool will no longer be expanded. + /// The type. + /// The receiver for s. protected void RegisterPool(int initialSize, int? maximumSize = null) where TObject : HitObject where TDrawable : DrawableHitObject, new() @@ -582,10 +591,21 @@ namespace osu.Game.Rulesets.UI }); } + /// + /// Creates the for a given . + /// + /// + /// This may be overridden to provide custom lifetime control (e.g. via . + /// + /// The to create the entry for. + /// The . protected abstract HitObjectLifetimeEntry CreateLifetimeEntry(HitObject hitObject); - private readonly Dictionary lifetimeEntries = new Dictionary(); - + /// + /// Retrieves or creates the for a given . + /// + /// The to retrieve or create the for. + /// The for . protected HitObjectLifetimeEntry GetLifetimeEntry(HitObject hitObject) { if (lifetimeEntries.TryGetValue(hitObject, out var entry)) From a8929b07644ab0c5d07a23c988c8497fb9c17477 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 11 Nov 2020 19:27:07 +0900 Subject: [PATCH 13/55] Revert unnecessary change of casting --- osu.Game/Rulesets/UI/HitObjectContainer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/UI/HitObjectContainer.cs b/osu.Game/Rulesets/UI/HitObjectContainer.cs index a0c95898be..12d78dac2b 100644 --- a/osu.Game/Rulesets/UI/HitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/HitObjectContainer.cs @@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.UI /// /// All currently in-use s. /// - public IEnumerable Objects => InternalChildren.OfType().OrderBy(h => h.HitObject.StartTime); + public IEnumerable Objects => InternalChildren.Cast().OrderBy(h => h.HitObject.StartTime); /// /// All currently in-use s that are alive. @@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.UI /// /// If this uses pooled objects, this is equivalent to . /// - public IEnumerable AliveObjects => AliveInternalChildren.OfType().OrderBy(h => h.HitObject.StartTime); + public IEnumerable AliveObjects => AliveInternalChildren.Cast().OrderBy(h => h.HitObject.StartTime); /// /// Invoked when a is judged. From d7d77460fb80e8a6d8989297621df505b9b552e7 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 12 Nov 2020 12:55:42 +0900 Subject: [PATCH 14/55] Small refactorings --- osu.Game/Rulesets/UI/DrawableRuleset.cs | 15 ++++++++++++++- osu.Game/Rulesets/UI/HitObjectContainer.cs | 10 ++++++++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index 2c5fce3e86..1a00346d6a 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -233,6 +233,13 @@ namespace osu.Game.Rulesets.UI ResumeOverlay?.Hide(); } + /// + /// Adds a to this . + /// + /// + /// This does not add the to the beatmap. + /// + /// The to add. public void AddHitObject(TObject hitObject) { if (PoolHitObjects) @@ -241,6 +248,13 @@ namespace osu.Game.Rulesets.UI Playfield.Add(CreateDrawableRepresentation(hitObject)); } + /// + /// Removes a from this . + /// + /// + /// This does not remove the from the beatmap. + /// + /// The to remove. public void RemoveHitObject(TObject hitObject) { if (PoolHitObjects) @@ -380,7 +394,6 @@ namespace osu.Game.Rulesets.UI /// Displays an interactive ruleset gameplay instance. /// /// This type is required only for adding non-generic type to the draw hierarchy. - /// Once IDrawable is a thing, this can also become an interface. /// /// [Cached(typeof(DrawableRuleset))] diff --git a/osu.Game/Rulesets/UI/HitObjectContainer.cs b/osu.Game/Rulesets/UI/HitObjectContainer.cs index 12d78dac2b..ff358e2e75 100644 --- a/osu.Game/Rulesets/UI/HitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/HitObjectContainer.cs @@ -100,10 +100,11 @@ namespace osu.Game.Rulesets.UI public virtual void Add(DrawableHitObject hitObject) { bindStartTime(hitObject); - AddInternal(hitObject); hitObject.OnNewResult += onNewResult; hitObject.OnRevertResult += onRevertResult; + + AddInternal(hitObject); } public virtual bool Remove(DrawableHitObject hitObject) @@ -143,7 +144,12 @@ namespace osu.Game.Rulesets.UI unbindAllStartTimes(); } - protected override bool CheckChildrenLife() => base.CheckChildrenLife() | lifetimeManager.Update(Time.Current, Time.Current); + protected override bool CheckChildrenLife() + { + bool aliveChanged = base.CheckChildrenLife(); + aliveChanged |= lifetimeManager.Update(Time.Current, Time.Current); + return aliveChanged; + } private void onNewResult(DrawableHitObject d, JudgementResult r) => NewResult?.Invoke(d, r); private void onRevertResult(DrawableHitObject d, JudgementResult r) => RevertResult?.Invoke(d, r); From f652eb9982b5047dad23f91ed5fb6098d593ca39 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 12 Nov 2020 13:18:44 +0900 Subject: [PATCH 15/55] Remove GetDrawableRepresentation() override, add null hinting --- osu.Game/Rulesets/UI/DrawableRuleset.cs | 21 +++++++++++---------- osu.Game/Rulesets/UI/HitObjectContainer.cs | 2 +- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index 1a00346d6a..357f2d27d9 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -314,9 +314,6 @@ namespace osu.Game.Rulesets.UI } } - public sealed override DrawableHitObject GetDrawableRepresentation(HitObject hitObject) - => base.GetDrawableRepresentation(hitObject) ?? CreateDrawableRepresentation((TObject)hitObject); - /// /// Creates a DrawableHitObject from a HitObject. /// @@ -552,7 +549,7 @@ namespace osu.Game.Rulesets.UI /// /// Registers a pool with this which is to be used whenever - /// representations are requested for the given type (via ). + /// representations are requested for the given type (via ). /// /// The number of drawables to be prepared for initial consumption. /// An optional maximum size after which the pool will no longer be expanded. @@ -574,16 +571,18 @@ namespace osu.Game.Rulesets.UI /// An optional maximum size after which the pool will no longer be expanded. /// The type of retrievable from this pool. /// The . + [NotNull] protected virtual DrawablePool CreatePool(int initialSize, int? maximumSize = null) where TDrawable : DrawableHitObject, new() => new DrawablePool(initialSize, maximumSize); /// - /// Retrieves the drawable representation of a . + /// Attempts to retrieve the poolable representation of a . /// - /// The to retrieve the drawable representation of. - /// The representing . - public virtual DrawableHitObject GetDrawableRepresentation(HitObject hitObject) + /// The to retrieve the representation of. + /// The representing , or null if no poolable representation exists. + [CanBeNull] + public DrawableHitObject GetPooledDrawableRepresentation([NotNull] HitObject hitObject) { if (!pools.TryGetValue(hitObject.GetType(), out var pool)) return null; @@ -612,14 +611,16 @@ namespace osu.Game.Rulesets.UI /// /// The to create the entry for. /// The . - protected abstract HitObjectLifetimeEntry CreateLifetimeEntry(HitObject hitObject); + [NotNull] + protected abstract HitObjectLifetimeEntry CreateLifetimeEntry([NotNull] HitObject hitObject); /// /// Retrieves or creates the for a given . /// /// The to retrieve or create the for. /// The for . - protected HitObjectLifetimeEntry GetLifetimeEntry(HitObject hitObject) + [NotNull] + protected HitObjectLifetimeEntry GetLifetimeEntry([NotNull] HitObject hitObject) { if (lifetimeEntries.TryGetValue(hitObject, out var entry)) return entry; diff --git a/osu.Game/Rulesets/UI/HitObjectContainer.cs b/osu.Game/Rulesets/UI/HitObjectContainer.cs index ff358e2e75..d55288b978 100644 --- a/osu.Game/Rulesets/UI/HitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/HitObjectContainer.cs @@ -70,7 +70,7 @@ namespace osu.Game.Rulesets.UI { Debug.Assert(!drawableMap.ContainsKey(entry)); - var drawable = drawableRuleset.GetDrawableRepresentation(entry.HitObject); + var drawable = drawableRuleset.GetPooledDrawableRepresentation(entry.HitObject); drawable.OnNewResult += onNewResult; drawable.OnRevertResult += onRevertResult; From 5dbbe11fc668b3a54ae2be4a190479ec5aa42126 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 12 Nov 2020 14:04:16 +0900 Subject: [PATCH 16/55] Remove PoolHitObjects, use return value of CreateDrawableRepresentation() instead --- .../Gameplay/TestScenePoolingRuleset.cs | 2 - osu.Game/Rulesets/UI/DrawableRuleset.cs | 49 +++++++++++-------- osu.Game/Rulesets/UI/Playfield.cs | 22 ++++++--- 3 files changed, 44 insertions(+), 29 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs index bb09aec416..c3ae753eae 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs @@ -140,8 +140,6 @@ namespace osu.Game.Tests.Visual.Gameplay RegisterPool(PoolSize); } - protected override bool PoolHitObjects => true; - protected override HitObjectLifetimeEntry CreateLifetimeEntry(TestHitObject hitObject) => new TestHitObjectLifetimeEntry(hitObject); protected override PassThroughInputManager CreateInputManager() => new PassThroughInputManager(); diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index 357f2d27d9..ce4cef4977 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -242,29 +242,38 @@ namespace osu.Game.Rulesets.UI /// The to add. public void AddHitObject(TObject hitObject) { - if (PoolHitObjects) - Playfield.Add(GetLifetimeEntry(hitObject)); + var drawableRepresentation = CreateDrawableRepresentation(hitObject); + + // If a drawable representation exists, use it, otherwise assume the hitobject is being pooled. + if (drawableRepresentation != null) + Playfield.Add(drawableRepresentation); else - Playfield.Add(CreateDrawableRepresentation(hitObject)); + Playfield.Add(GetLifetimeEntry(hitObject)); } /// - /// Removes a from this . + /// Removes a from this . /// /// /// This does not remove the from the beatmap. /// /// The to remove. - public void RemoveHitObject(TObject hitObject) + public bool RemoveHitObject(TObject hitObject) { - if (PoolHitObjects) - Playfield.Remove(GetLifetimeEntry(hitObject)); - else - { - var drawableObject = Playfield.AllHitObjects.SingleOrDefault(d => d.HitObject == hitObject); - if (drawableObject != null) - Playfield.Remove(drawableObject); - } + 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) @@ -539,14 +548,6 @@ namespace osu.Game.Rulesets.UI private readonly Dictionary pools = new Dictionary(); private readonly Dictionary lifetimeEntries = new Dictionary(); - /// - /// Whether this should retrieve pooled s. - /// - /// - /// Pools must be registered with this via in order for s to be retrieved. - /// - protected virtual bool PoolHitObjects => false; - /// /// Registers a pool with this which is to be used whenever /// representations are requested for the given type (via ). @@ -627,6 +628,12 @@ namespace osu.Game.Rulesets.UI return lifetimeEntries[hitObject] = CreateLifetimeEntry(hitObject); } + + /// + /// Removes the for a . + /// + /// The to remove the for. + internal void RemoveLifetimeEntry([NotNull] HitObject hitObject) => lifetimeEntries.Remove(hitObject); } public class BeatmapInvalidForRulesetException : ArgumentException diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index cdaf9364af..7c47f046dc 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -143,7 +143,7 @@ namespace osu.Game.Rulesets.UI private readonly Dictionary lifetimeEntryMap = new Dictionary(); /// - /// Adds a to this . + /// Adds a for a pooled to this . /// /// The controlling the lifetime of the . public void Add(HitObjectLifetimeEntry entry) @@ -154,14 +154,24 @@ namespace osu.Game.Rulesets.UI } /// - /// Removes a to this . + /// Removes a for a pooled from this . /// /// The controlling the lifetime of the . - public void Remove(HitObjectLifetimeEntry entry) + /// Whether the was successfully removed. + public bool Remove(HitObjectLifetimeEntry entry) { - if (HitObjectContainer.Remove(entry)) - OnHitObjectRemoved(entry.HitObject); - lifetimeEntryMap.Remove(entry.HitObject); + if (lifetimeEntryMap.Remove(entry.HitObject)) + { + HitObjectContainer.Remove(entry); + return true; + } + + bool removedFromNested = false; + + if (nestedPlayfields.IsValueCreated) + removedFromNested = nestedPlayfields.Value.Any(p => p.Remove(entry)); + + return removedFromNested; } /// From 1f8d376b85d28a2dbdc5ff373e1ebecb99307c32 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 12 Nov 2020 14:17:12 +0900 Subject: [PATCH 17/55] Replace CreatePool() with non-virtual RegisterPool() overload --- osu.Game/Rulesets/UI/DrawableRuleset.cs | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index ce4cef4977..983667a4dd 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -559,24 +559,23 @@ namespace osu.Game.Rulesets.UI protected void RegisterPool(int initialSize, int? maximumSize = null) where TObject : HitObject where TDrawable : DrawableHitObject, new() + => RegisterPool(new DrawablePool(initialSize, maximumSize)); + + /// + /// Registers a pool with this which is to be used whenever + /// representations are requested for the given type (via ). + /// + /// The to register. + /// The type. + /// The receiver for s. + protected void RegisterPool([NotNull] DrawablePool pool) + where TObject : HitObject + where TDrawable : DrawableHitObject, new() { - var pool = CreatePool(initialSize, maximumSize); pools[typeof(TObject)] = pool; AddInternal(pool); } - /// - /// Creates the to retrieve s of the given type from. - /// - /// The number of hitobject to be prepared for initial consumption. - /// An optional maximum size after which the pool will no longer be expanded. - /// The type of retrievable from this pool. - /// The . - [NotNull] - protected virtual DrawablePool CreatePool(int initialSize, int? maximumSize = null) - where TDrawable : DrawableHitObject, new() - => new DrawablePool(initialSize, maximumSize); - /// /// Attempts to retrieve the poolable representation of a . /// From 16e4e8d032dc44dc97f606922879114e4299ff43 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 12 Nov 2020 14:54:33 +0900 Subject: [PATCH 18/55] Fix possible nullref --- osu.Game/Rulesets/UI/HitObjectContainer.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Rulesets/UI/HitObjectContainer.cs b/osu.Game/Rulesets/UI/HitObjectContainer.cs index d55288b978..7315ce61e7 100644 --- a/osu.Game/Rulesets/UI/HitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/HitObjectContainer.cs @@ -7,6 +7,7 @@ using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Performance; @@ -71,6 +72,9 @@ namespace osu.Game.Rulesets.UI 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; From 653f5bce676d70ff2ef63e435f618dc039864102 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 12 Nov 2020 15:00:58 +0900 Subject: [PATCH 19/55] Reword xmldocs --- osu.Game/Rulesets/UI/DrawableRuleset.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index 983667a4dd..0429936d8e 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -549,11 +549,15 @@ namespace osu.Game.Rulesets.UI private readonly Dictionary lifetimeEntries = new Dictionary(); /// - /// Registers a pool with this which is to be used whenever + /// Registers a default pool with this which is to be used whenever /// representations are requested for the given type (via ). /// - /// The number of drawables to be prepared for initial consumption. - /// An optional maximum size after which the pool will no longer be expanded. + /// The number of s to be initially stored in the pool. + /// + /// The maximum number of s that can be stored in the pool. + /// If this limit is exceeded, every subsequent will be created anew instead of being retrieved from the pool, + /// until some of the existing s are returned to the pool. + /// /// The type. /// The receiver for s. protected void RegisterPool(int initialSize, int? maximumSize = null) @@ -562,7 +566,7 @@ namespace osu.Game.Rulesets.UI => RegisterPool(new DrawablePool(initialSize, maximumSize)); /// - /// Registers a pool with this which is to be used whenever + /// Registers a custom pool with this which is to be used whenever /// representations are requested for the given type (via ). /// /// The to register. From 4e4323595557813493ad09bd2e1175269fe59f0a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 12 Nov 2020 15:33:49 +0900 Subject: [PATCH 20/55] Fix double bind leading to test failures --- osu.Game/Rulesets/UI/Playfield.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index 7c47f046dc..9df3bb10ce 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -120,10 +120,6 @@ namespace osu.Game.Rulesets.UI public virtual void Add(DrawableHitObject h) { HitObjectContainer.Add(h); - - h.OnNewResult += (d, r) => NewResult?.Invoke(d, r); - h.OnRevertResult += (d, r) => RevertResult?.Invoke(d, r); - OnHitObjectAdded(h.HitObject); } From 72a6b756268f4e6218814a706d3dfb84f266b1e8 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 12 Nov 2020 15:34:51 +0900 Subject: [PATCH 21/55] Add back removed event --- osu.Game/Rulesets/UI/Playfield.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index 9df3bb10ce..5e5d17a400 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -159,6 +159,7 @@ namespace osu.Game.Rulesets.UI if (lifetimeEntryMap.Remove(entry.HitObject)) { HitObjectContainer.Remove(entry); + OnHitObjectRemoved(entry.HitObject); return true; } From 974390bda77d551107cb4f08baa63c1a8c8ae1d3 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 12 Nov 2020 15:35:58 +0900 Subject: [PATCH 22/55] Make Add() + Remove() virtual --- osu.Game/Rulesets/UI/Playfield.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index 5e5d17a400..d1cb8ecbbd 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -142,7 +142,7 @@ namespace osu.Game.Rulesets.UI /// Adds a for a pooled to this . /// /// The controlling the lifetime of the . - public void Add(HitObjectLifetimeEntry entry) + public virtual void Add(HitObjectLifetimeEntry entry) { HitObjectContainer.Add(entry); lifetimeEntryMap[entry.HitObject] = entry; @@ -154,7 +154,7 @@ namespace osu.Game.Rulesets.UI /// /// The controlling the lifetime of the . /// Whether the was successfully removed. - public bool Remove(HitObjectLifetimeEntry entry) + public virtual bool Remove(HitObjectLifetimeEntry entry) { if (lifetimeEntryMap.Remove(entry.HitObject)) { From cf91962865aa659859fb8f73f2f994c8f6027018 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 12 Nov 2020 16:58:40 +0900 Subject: [PATCH 23/55] Fix test failures due to on-the-fly starttime changes --- osu.Game/Rulesets/UI/HitObjectContainer.cs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/UI/HitObjectContainer.cs b/osu.Game/Rulesets/UI/HitObjectContainer.cs index 7315ce61e7..1bb1fd4983 100644 --- a/osu.Game/Rulesets/UI/HitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/HitObjectContainer.cs @@ -57,6 +57,14 @@ namespace osu.Game.Rulesets.UI lifetimeManager.EntryBecameDead += entryBecameDead; } + protected override void LoadComplete() + { + base.LoadComplete(); + + // Application of hitobject 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); @@ -163,7 +171,12 @@ namespace osu.Game.Rulesets.UI private void bindStartTime(DrawableHitObject hitObject) { var bindable = hitObject.StartTimeBindable.GetBoundCopy(); - bindable.BindValueChanged(_ => SortInternal()); + + bindable.BindValueChanged(_ => + { + if (IsLoaded) + SortInternal(); + }); startTimeMap[hitObject] = bindable; } From e44a8b3934d769e8455e306e18949be30ee89227 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 12 Nov 2020 17:07:20 +0900 Subject: [PATCH 24/55] Resort as early as possible --- osu.Game/Rulesets/UI/HitObjectContainer.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Rulesets/UI/HitObjectContainer.cs b/osu.Game/Rulesets/UI/HitObjectContainer.cs index 1bb1fd4983..4164681ffc 100644 --- a/osu.Game/Rulesets/UI/HitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/HitObjectContainer.cs @@ -57,11 +57,11 @@ namespace osu.Game.Rulesets.UI lifetimeManager.EntryBecameDead += entryBecameDead; } - protected override void LoadComplete() + protected override void LoadAsyncComplete() { - base.LoadComplete(); + base.LoadAsyncComplete(); - // Application of hitobject during load() may have changed their start times, so ensure the correct sorting order. + // Application of hitobjects during load() may have changed their start times, so ensure the correct sorting order. SortInternal(); } @@ -174,7 +174,7 @@ namespace osu.Game.Rulesets.UI bindable.BindValueChanged(_ => { - if (IsLoaded) + if (LoadState >= LoadState.Ready) SortInternal(); }); From a8c2b798ad5c71cc2dcdc3d061e54148297218ef Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 12 Nov 2020 15:24:45 +0900 Subject: [PATCH 25/55] Add support for nested hitobject pooling --- .../Objects/Drawables/DrawableHitObject.cs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 244cf831c3..2dba83f2be 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -19,6 +19,7 @@ using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Scoring; using osu.Game.Skinning; using osu.Game.Configuration; +using osu.Game.Rulesets.UI; using osuTK.Graphics; namespace osu.Game.Rulesets.Objects.Drawables @@ -126,6 +127,9 @@ namespace osu.Game.Rulesets.Objects.Drawables [CanBeNull] private HitObjectLifetimeEntry lifetimeEntry; + [Resolved(CanBeNull = true)] + private DrawableRuleset drawableRuleset { get; set; } + /// /// Creates a new . /// @@ -195,7 +199,9 @@ namespace osu.Game.Rulesets.Objects.Drawables foreach (var h in HitObject.NestedHitObjects) { - var drawableNested = CreateNestedHitObject(h) ?? throw new InvalidOperationException($"{nameof(CreateNestedHitObject)} returned null for {h.GetType().ReadableName()}."); + var drawableNested = drawableRuleset?.GetPooledDrawableRepresentation(h) + ?? CreateNestedHitObject(h) + ?? throw new InvalidOperationException($"{nameof(CreateNestedHitObject)} returned null for {h.GetType().ReadableName()}."); drawableNested.OnNewResult += onNewResult; drawableNested.OnRevertResult += onRevertResult; @@ -203,6 +209,8 @@ namespace osu.Game.Rulesets.Objects.Drawables nestedHitObjects.Value.Add(drawableNested); AddNestedHitObject(drawableNested); + + drawableNested.OnParentReceived(this); } StartTimeBindable.BindTo(HitObject.StartTimeBindable); @@ -291,6 +299,14 @@ namespace osu.Game.Rulesets.Objects.Drawables { } + /// + /// Invoked when this receives a new parenting . + /// + /// The parenting . + protected virtual void OnParentReceived(DrawableHitObject parent) + { + } + /// /// Invoked by the base to populate samples, once on initial load and potentially again on any change to the samples collection. /// From 44aed19e4e0e1e4098e2f7d405ac10f252d77185 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 11 Nov 2020 12:37:00 +0900 Subject: [PATCH 26/55] Fix mania notelock crashing with overlapping hitwindows --- .../TestSceneHoldNoteInput.cs | 2 +- .../TestSceneOutOfOrderHits.cs | 23 +++++++++++++++++++ .../Objects/Drawables/DrawableHoldNote.cs | 2 +- .../UI/OrderedHitPolicy.cs | 2 +- 4 files changed, 26 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs index 5cb1519196..6c9f184c2c 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs @@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Mania.Tests assertHeadJudgement(HitResult.Miss); assertTickJudgement(HitResult.LargeTickMiss); assertTailJudgement(HitResult.Miss); - assertNoteJudgement(HitResult.IgnoreHit); + assertNoteJudgement(HitResult.IgnoreMiss); } /// diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs index e8c2472c3b..d699921307 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs @@ -54,6 +54,29 @@ namespace osu.Game.Rulesets.Mania.Tests } } + [Test] + public void TestMissAfterNextObjectStartTime() + { + var objects = new List + { + new HoldNote + { + StartTime = 1000, + EndTime = 1200, + }, + new HoldNote + { + StartTime = 1220, + EndTime = 1420 + } + }; + + performTest(objects, new List()); + + addJudgementAssert(objects[0], HitResult.IgnoreMiss); + addJudgementAssert(objects[1], HitResult.IgnoreMiss); + } + private void addJudgementAssert(ManiaHitObject hitObject, HitResult result) { AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judgement is {result}", diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs index d9d740c145..3b3f72157a 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs @@ -233,7 +233,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables { if (Tail.AllJudged) { - ApplyResult(r => r.Type = r.Judgement.MaxResult); + ApplyResult(r => r.Type = Tail.IsHit ? r.Judgement.MaxResult : r.Judgement.MinResult); endHold(); } diff --git a/osu.Game.Rulesets.Mania/UI/OrderedHitPolicy.cs b/osu.Game.Rulesets.Mania/UI/OrderedHitPolicy.cs index 0f9cd48dd8..9bc577a81e 100644 --- a/osu.Game.Rulesets.Mania/UI/OrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Mania/UI/OrderedHitPolicy.cs @@ -44,7 +44,7 @@ namespace osu.Game.Rulesets.Mania.UI /// The that was hit. public void HandleHit(DrawableHitObject hitObject) { - if (!IsHittable(hitObject, hitObject.HitObject.StartTime + hitObject.Result.TimeOffset)) + if (hitObject.IsHit && !IsHittable(hitObject, hitObject.HitObject.StartTime + hitObject.Result.TimeOffset)) throw new InvalidOperationException($"A {hitObject} was hit before it became hittable!"); foreach (var obj in enumerateHitObjectsUpTo(hitObject.HitObject.StartTime)) From ad38867b1d977e9a78abb12c5d685e3b1c9831ab Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 11 Nov 2020 12:53:32 +0900 Subject: [PATCH 27/55] Completely remove check as it can occur for hits too --- .../TestSceneOutOfOrderHits.cs | 51 +++++++++++++++++-- .../UI/OrderedHitPolicy.cs | 4 -- 2 files changed, 47 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs index d699921307..86a142f2f6 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using System.Linq; using NUnit.Framework; @@ -55,19 +56,19 @@ namespace osu.Game.Rulesets.Mania.Tests } [Test] - public void TestMissAfterNextObjectStartTime() + public void TestHoldNoteMissAfterNextObjectStartTime() { var objects = new List { new HoldNote { StartTime = 1000, - EndTime = 1200, + EndTime = 1010, }, new HoldNote { - StartTime = 1220, - EndTime = 1420 + StartTime = 1020, + EndTime = 1030 } }; @@ -77,12 +78,54 @@ namespace osu.Game.Rulesets.Mania.Tests addJudgementAssert(objects[1], HitResult.IgnoreMiss); } + [Test] + public void TestHoldNoteReleasedHitAfterNextObjectStartTime() + { + var objects = new List + { + new HoldNote + { + StartTime = 1000, + EndTime = 1010, + }, + new HoldNote + { + StartTime = 1020, + EndTime = 1030 + } + }; + + var frames = new List + { + new ManiaReplayFrame(1000, ManiaAction.Key1), + new ManiaReplayFrame(1030), + new ManiaReplayFrame(1040, ManiaAction.Key1), + new ManiaReplayFrame(1050) + }; + + performTest(objects, frames); + + addJudgementAssert(objects[0], HitResult.IgnoreHit); + addJudgementAssert("first head", () => ((HoldNote)objects[0]).Head, HitResult.Perfect); + addJudgementAssert("first tail", () => ((HoldNote)objects[0]).Tail, HitResult.Perfect); + + addJudgementAssert(objects[1], HitResult.IgnoreHit); + addJudgementAssert("second head", () => ((HoldNote)objects[1]).Head, HitResult.Great); + addJudgementAssert("second tail", () => ((HoldNote)objects[1]).Tail, HitResult.Perfect); + } + private void addJudgementAssert(ManiaHitObject hitObject, HitResult result) { AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judgement is {result}", () => judgementResults.Single(r => r.HitObject == hitObject).Type == result); } + private void addJudgementAssert(string name, Func hitObject, HitResult result) + { + AddAssert($"{name} judgement is {result}", + () => judgementResults.Single(r => r.HitObject == hitObject()).Type == result); + } + private void addJudgementOffsetAssert(ManiaHitObject hitObject, double offset) { AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judged at {offset}", diff --git a/osu.Game.Rulesets.Mania/UI/OrderedHitPolicy.cs b/osu.Game.Rulesets.Mania/UI/OrderedHitPolicy.cs index 9bc577a81e..961858b62b 100644 --- a/osu.Game.Rulesets.Mania/UI/OrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Mania/UI/OrderedHitPolicy.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Collections.Generic; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Game.Rulesets.Mania.Objects.Drawables; @@ -44,9 +43,6 @@ namespace osu.Game.Rulesets.Mania.UI /// The that was hit. public void HandleHit(DrawableHitObject hitObject) { - if (hitObject.IsHit && !IsHittable(hitObject, hitObject.HitObject.StartTime + hitObject.Result.TimeOffset)) - throw new InvalidOperationException($"A {hitObject} was hit before it became hittable!"); - foreach (var obj in enumerateHitObjectsUpTo(hitObject.HitObject.StartTime)) { if (obj.Judged) From 60f95e897d9c119e3492a5b9e14d687aaddfa0f4 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 11 Nov 2020 12:53:53 +0900 Subject: [PATCH 28/55] Revert unnecessary change --- osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs | 2 +- osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs | 4 ++-- osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs index 6c9f184c2c..5cb1519196 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs @@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Mania.Tests assertHeadJudgement(HitResult.Miss); assertTickJudgement(HitResult.LargeTickMiss); assertTailJudgement(HitResult.Miss); - assertNoteJudgement(HitResult.IgnoreMiss); + assertNoteJudgement(HitResult.IgnoreHit); } /// diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs index 86a142f2f6..cecac38f70 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs @@ -74,8 +74,8 @@ namespace osu.Game.Rulesets.Mania.Tests performTest(objects, new List()); - addJudgementAssert(objects[0], HitResult.IgnoreMiss); - addJudgementAssert(objects[1], HitResult.IgnoreMiss); + addJudgementAssert(objects[0], HitResult.IgnoreHit); + addJudgementAssert(objects[1], HitResult.IgnoreHit); } [Test] diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs index 3b3f72157a..d9d740c145 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs @@ -233,7 +233,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables { if (Tail.AllJudged) { - ApplyResult(r => r.Type = Tail.IsHit ? r.Judgement.MaxResult : r.Judgement.MinResult); + ApplyResult(r => r.Type = r.Judgement.MaxResult); endHold(); } From d9750fc043d7dc85151f365363909a855bf9cd33 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 11 Nov 2020 15:49:45 +0900 Subject: [PATCH 29/55] Remove duplicate instantiation of externalLinkOpener --- osu.Game/OsuGame.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 6b768cc8fc..1a1ebcc2d4 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -681,7 +681,6 @@ namespace osu.Game loadComponentSingleFile(new AccountCreationOverlay(), topMostOverlayContent.Add, true); loadComponentSingleFile(new DialogOverlay(), topMostOverlayContent.Add, true); - loadComponentSingleFile(externalLinkOpener = new ExternalLinkOpener(), topMostOverlayContent.Add); chatOverlay.State.ValueChanged += state => channelManager.HighPollRate.Value = state.NewValue == Visibility.Visible; From 81c9663e763b4d0268bc41e6bfb09ac91e5b2442 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 10 Nov 2020 13:24:32 +0100 Subject: [PATCH 30/55] Move {-> Default}KiaiHitExplosion --- .../UI/{KiaiHitExplosion.cs => DefaultKiaiHitExplosion.cs} | 4 ++-- osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) rename osu.Game.Rulesets.Taiko/UI/{KiaiHitExplosion.cs => DefaultKiaiHitExplosion.cs} (92%) diff --git a/osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.cs b/osu.Game.Rulesets.Taiko/UI/DefaultKiaiHitExplosion.cs similarity index 92% rename from osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.cs rename to osu.Game.Rulesets.Taiko/UI/DefaultKiaiHitExplosion.cs index 067d390894..32c9f3ec4f 100644 --- a/osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.cs +++ b/osu.Game.Rulesets.Taiko/UI/DefaultKiaiHitExplosion.cs @@ -13,14 +13,14 @@ using osu.Game.Rulesets.Taiko.Objects; namespace osu.Game.Rulesets.Taiko.UI { - public class KiaiHitExplosion : CircularContainer + public class DefaultKiaiHitExplosion : CircularContainer { public override bool RemoveWhenNotAlive => true; public readonly DrawableHitObject JudgedObject; private readonly HitType type; - public KiaiHitExplosion(DrawableHitObject judgedObject, HitType type) + public DefaultKiaiHitExplosion(DrawableHitObject judgedObject, HitType type) { JudgedObject = judgedObject; this.type = type; diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index 120cf264c3..03895dfd68 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Taiko.UI public const float DEFAULT_HEIGHT = 178; private Container hitExplosionContainer; - private Container kiaiExplosionContainer; + private Container kiaiExplosionContainer; private JudgementContainer judgementContainer; private ScrollingHitObjectContainer drumRollHitContainer; internal Drawable HitTarget; @@ -97,7 +97,7 @@ namespace osu.Game.Rulesets.Taiko.UI drumRollHitContainer = new DrumRollHitContainer() } }, - kiaiExplosionContainer = new Container + kiaiExplosionContainer = new Container { Name = "Kiai hit explosions", RelativeSizeAxes = Axes.Both, @@ -219,7 +219,7 @@ namespace osu.Game.Rulesets.Taiko.UI { hitExplosionContainer.Add(new HitExplosion(drawableObject, result)); if (drawableObject.HitObject.Kiai) - kiaiExplosionContainer.Add(new KiaiHitExplosion(drawableObject, type)); + kiaiExplosionContainer.Add(new DefaultKiaiHitExplosion(drawableObject, type)); } private class ProxyContainer : LifetimeManagementContainer From 00a486ab516ab6ac65471177c0f2dd3b36758d5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 10 Nov 2020 13:35:49 +0100 Subject: [PATCH 31/55] Reintroduce KiaiHitExplosion as skinnable --- .../TaikoSkinComponents.cs | 1 + .../UI/DefaultKiaiHitExplosion.cs | 9 +--- .../UI/KiaiHitExplosion.cs | 47 +++++++++++++++++++ osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs | 6 +-- 4 files changed, 52 insertions(+), 11 deletions(-) create mode 100644 osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.cs diff --git a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs index 132d8f8868..bf48898dd2 100644 --- a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs +++ b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs @@ -18,6 +18,7 @@ namespace osu.Game.Rulesets.Taiko TaikoExplosionMiss, TaikoExplosionOk, TaikoExplosionGreat, + TaikoExplosionKiai, Scroller, Mascot, } diff --git a/osu.Game.Rulesets.Taiko/UI/DefaultKiaiHitExplosion.cs b/osu.Game.Rulesets.Taiko/UI/DefaultKiaiHitExplosion.cs index 32c9f3ec4f..7ce8b016d5 100644 --- a/osu.Game.Rulesets.Taiko/UI/DefaultKiaiHitExplosion.cs +++ b/osu.Game.Rulesets.Taiko/UI/DefaultKiaiHitExplosion.cs @@ -8,7 +8,6 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; -using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Taiko.Objects; namespace osu.Game.Rulesets.Taiko.UI @@ -17,19 +16,13 @@ namespace osu.Game.Rulesets.Taiko.UI { public override bool RemoveWhenNotAlive => true; - public readonly DrawableHitObject JudgedObject; private readonly HitType type; - public DefaultKiaiHitExplosion(DrawableHitObject judgedObject, HitType type) + public DefaultKiaiHitExplosion(HitType type) { - JudgedObject = judgedObject; this.type = type; - Anchor = Anchor.CentreLeft; - Origin = Anchor.Centre; - RelativeSizeAxes = Axes.Both; - Size = new Vector2(TaikoHitObject.DEFAULT_SIZE, 1); Blending = BlendingParameters.Additive; diff --git a/osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.cs b/osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.cs new file mode 100644 index 0000000000..20900a9352 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.cs @@ -0,0 +1,47 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Skinning; +using osuTK; + +namespace osu.Game.Rulesets.Taiko.UI +{ + public class KiaiHitExplosion : Container + { + public override bool RemoveWhenNotAlive => true; + + [Cached(typeof(DrawableHitObject))] + public readonly DrawableHitObject JudgedObject; + + private readonly HitType hitType; + + private SkinnableDrawable skinnable; + + public override double LifetimeStart => skinnable.Drawable.LifetimeStart; + + public override double LifetimeEnd => skinnable.Drawable.LifetimeEnd; + + public KiaiHitExplosion(DrawableHitObject judgedObject, HitType hitType) + { + JudgedObject = judgedObject; + this.hitType = hitType; + + Anchor = Anchor.CentreLeft; + Origin = Anchor.Centre; + + RelativeSizeAxes = Axes.Both; + Size = new Vector2(TaikoHitObject.DEFAULT_SIZE, 1); + } + + [BackgroundDependencyLoader] + private void load() + { + Child = skinnable = new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.TaikoExplosionKiai), _ => new DefaultKiaiHitExplosion(hitType)); + } + } +} diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index 03895dfd68..120cf264c3 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Taiko.UI public const float DEFAULT_HEIGHT = 178; private Container hitExplosionContainer; - private Container kiaiExplosionContainer; + private Container kiaiExplosionContainer; private JudgementContainer judgementContainer; private ScrollingHitObjectContainer drumRollHitContainer; internal Drawable HitTarget; @@ -97,7 +97,7 @@ namespace osu.Game.Rulesets.Taiko.UI drumRollHitContainer = new DrumRollHitContainer() } }, - kiaiExplosionContainer = new Container + kiaiExplosionContainer = new Container { Name = "Kiai hit explosions", RelativeSizeAxes = Axes.Both, @@ -219,7 +219,7 @@ namespace osu.Game.Rulesets.Taiko.UI { hitExplosionContainer.Add(new HitExplosion(drawableObject, result)); if (drawableObject.HitObject.Kiai) - kiaiExplosionContainer.Add(new DefaultKiaiHitExplosion(drawableObject, type)); + kiaiExplosionContainer.Add(new KiaiHitExplosion(drawableObject, type)); } private class ProxyContainer : LifetimeManagementContainer From 646833a059991206691530888626748ce34e7501 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 10 Nov 2020 14:07:39 +0100 Subject: [PATCH 32/55] Add test scene --- .../Skinning/TestSceneKiaiHitExplosion.cs | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneKiaiHitExplosion.cs diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneKiaiHitExplosion.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneKiaiHitExplosion.cs new file mode 100644 index 0000000000..b558709592 --- /dev/null +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneKiaiHitExplosion.cs @@ -0,0 +1,37 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.UI; + +namespace osu.Game.Rulesets.Taiko.Tests.Skinning +{ + [TestFixture] + public class TestSceneKiaiHitExplosion : TaikoSkinnableTestScene + { + [Test] + public void TestKiaiHits() + { + AddStep("rim hit", () => SetContents(() => getContentFor(createHit(HitType.Rim)))); + AddStep("centre hit", () => SetContents(() => getContentFor(createHit(HitType.Centre)))); + } + + private Drawable getContentFor(DrawableTestHit hit) + { + return new Container + { + RelativeSizeAxes = Axes.Both, + Child = new KiaiHitExplosion(hit, hit.HitObject.Type) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } + }; + } + + private DrawableTestHit createHit(HitType type) => new DrawableTestHit(new Hit { StartTime = Time.Current, Type = type }); + } +} From 21709ba4bce68efffe1e1a3dffcc6157ee2b2697 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 10 Nov 2020 14:08:27 +0100 Subject: [PATCH 33/55] Do not lookup default kiai explosion if skin has own --- .../Skinning/TaikoLegacySkinTransformer.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs index c88480d18f..ddbf20b827 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs @@ -114,6 +114,13 @@ namespace osu.Game.Rulesets.Taiko.Skinning return null; + case TaikoSkinComponents.TaikoExplosionKiai: + // suppress the default kiai explosion if the skin brings its own sprites. + if (hasExplosion.Value) + return Drawable.Empty(); + + return null; + case TaikoSkinComponents.Scroller: if (GetTexture("taiko-slider") != null) return new LegacyTaikoScroller(); From 0d5cac89b3d2f3b6d4de66da1caafa2424935d52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 10 Nov 2020 14:50:19 +0100 Subject: [PATCH 34/55] Explicitly set lifetime to ensure empty drawables are cleaned up --- .../Skinning/TaikoLegacySkinTransformer.cs | 3 ++- osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.cs | 6 ++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs index ddbf20b827..880af3fbd8 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs @@ -116,8 +116,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning case TaikoSkinComponents.TaikoExplosionKiai: // suppress the default kiai explosion if the skin brings its own sprites. + // the drawable needs to expire as soon as possible to avoid accumulating empty drawables on the playfield. if (hasExplosion.Value) - return Drawable.Empty(); + return KiaiHitExplosion.EmptyExplosion(); return null; diff --git a/osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.cs b/osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.cs index 20900a9352..326cb23897 100644 --- a/osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.cs +++ b/osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.cs @@ -43,5 +43,11 @@ namespace osu.Game.Rulesets.Taiko.UI { Child = skinnable = new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.TaikoExplosionKiai), _ => new DefaultKiaiHitExplosion(hitType)); } + + /// + /// Helper function to use when an explosion is not desired. + /// Lifetime is set to avoid accumulating empty drawables in the parent container. + /// + public static Drawable EmptyExplosion() => Empty().With(d => d.LifetimeEnd = double.MinValue); } } From 1f83769bb270b2a1e33d4baf038fa243a460d864 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 11 Nov 2020 09:11:33 +0100 Subject: [PATCH 35/55] Inline empty explosion in legacy transformer --- .../Skinning/TaikoLegacySkinTransformer.cs | 2 +- osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.cs | 6 ------ 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs index 880af3fbd8..96fb065e79 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs @@ -118,7 +118,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning // suppress the default kiai explosion if the skin brings its own sprites. // the drawable needs to expire as soon as possible to avoid accumulating empty drawables on the playfield. if (hasExplosion.Value) - return KiaiHitExplosion.EmptyExplosion(); + return Drawable.Empty().With(d => d.LifetimeEnd = double.MinValue); return null; diff --git a/osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.cs b/osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.cs index 326cb23897..20900a9352 100644 --- a/osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.cs +++ b/osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.cs @@ -43,11 +43,5 @@ namespace osu.Game.Rulesets.Taiko.UI { Child = skinnable = new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.TaikoExplosionKiai), _ => new DefaultKiaiHitExplosion(hitType)); } - - /// - /// Helper function to use when an explosion is not desired. - /// Lifetime is set to avoid accumulating empty drawables in the parent container. - /// - public static Drawable EmptyExplosion() => Empty().With(d => d.LifetimeEnd = double.MinValue); } } From 8a2addbf3d56bb4aa07b92d3d305e32dd8a037fc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 11 Nov 2020 18:03:04 +0900 Subject: [PATCH 36/55] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index bbe8426316..5078fee1cf 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,6 +52,6 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 8f0cc58594..405fb1a6ca 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -26,7 +26,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index f766e0ec03..099ecd8319 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -88,7 +88,7 @@ - + From 7177dd5d82f6880255be463b6341fef931d7ad5e Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Thu, 12 Nov 2020 03:11:29 +0300 Subject: [PATCH 37/55] Add counter to most played beatmaps section in user overlay --- .../Historical/PaginatedMostPlayedBeatmapContainer.cs | 4 +++- osu.Game/Users/User.cs | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Profile/Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs b/osu.Game/Overlays/Profile/Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs index 8f19cd900c..556f3139dd 100644 --- a/osu.Game/Overlays/Profile/Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs @@ -16,7 +16,7 @@ namespace osu.Game.Overlays.Profile.Sections.Historical public class PaginatedMostPlayedBeatmapContainer : PaginatedContainer { public PaginatedMostPlayedBeatmapContainer(Bindable user) - : base(user, "Most Played Beatmaps", "No records. :(") + : base(user, "Most Played Beatmaps", "No records. :(", CounterVisibilityState.AlwaysVisible) { ItemsPerPage = 5; } @@ -27,6 +27,8 @@ namespace osu.Game.Overlays.Profile.Sections.Historical ItemsContainer.Direction = FillDirection.Vertical; } + protected override int GetCount(User user) => user.BeatmapPlaycountsCount; + protected override APIRequest> CreateRequest() => new GetUserMostPlayedBeatmapsRequest(User.Value.Id, VisiblePages++, ItemsPerPage); diff --git a/osu.Game/Users/User.cs b/osu.Game/Users/User.cs index 2a76a963e1..d7e78d5b35 100644 --- a/osu.Game/Users/User.cs +++ b/osu.Game/Users/User.cs @@ -144,6 +144,9 @@ namespace osu.Game.Users [JsonProperty(@"scores_first_count")] public int ScoresFirstCount; + [JsonProperty(@"beatmap_playcounts_count")] + public int BeatmapPlaycountsCount; + [JsonProperty] private string[] playstyle { From 7548db7ecc159638e016553844b5e0add7475128 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 11 Nov 2020 16:35:48 +0900 Subject: [PATCH 38/55] Fix hitobjects sometimes not fading in completely with HD mod --- osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs | 10 ++++++++-- .../Rulesets/Objects/Drawables/DrawableHitObject.cs | 9 +++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs index 025e202666..cf7faca9b9 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs @@ -27,17 +27,23 @@ namespace osu.Game.Rulesets.Osu.Mods public override void ApplyToDrawableHitObjects(IEnumerable drawables) { foreach (var d in drawables) - d.ApplyCustomUpdateState += applyFadeInAdjustment; + { + d.HitObjectApplied += applyFadeInAdjustment; + applyFadeInAdjustment(d); + } base.ApplyToDrawableHitObjects(drawables); } - private void applyFadeInAdjustment(DrawableHitObject hitObject, ArmedState state) + private void applyFadeInAdjustment(DrawableHitObject hitObject) { if (!(hitObject is DrawableOsuHitObject d)) return; d.HitObject.TimeFadeIn = d.HitObject.TimePreempt * fade_in_duration_multiplier; + + foreach (var nested in d.NestedHitObjects) + applyFadeInAdjustment(nested); } private double lastSliderHeadFadeOutStartTime; diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 244cf831c3..6a0e416967 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -26,8 +26,16 @@ namespace osu.Game.Rulesets.Objects.Drawables [Cached(typeof(DrawableHitObject))] public abstract class DrawableHitObject : SkinReloadableDrawable { + /// + /// Invoked after this 's applied has had its defaults applied. + /// public event Action DefaultsApplied; + /// + /// Invoked after a has been applied to this . + /// + public event Action HitObjectApplied; + /// /// The currently represented by this . /// @@ -215,6 +223,7 @@ namespace osu.Game.Rulesets.Objects.Drawables HitObject.DefaultsApplied += onDefaultsApplied; OnApply(hitObject); + HitObjectApplied?.Invoke(this); // If not loaded, the state update happens in LoadComplete(). Otherwise, the update is scheduled to allow for lifetime updates. if (IsLoaded) From fe347c8661839179421ff7e4f7ce75d034d81a43 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 12 Nov 2020 18:30:32 +0900 Subject: [PATCH 39/55] Add playfield support for keeping hitobjects alive --- osu.Game/Rulesets/UI/Playfield.cs | 35 +++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index d1cb8ecbbd..454880c885 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -187,6 +187,41 @@ namespace osu.Game.Rulesets.UI { } + /// + /// Sets whether to keep a given always alive within this or any nested . + /// + /// The to set. + /// Whether to keep always alive. + public void SetKeepAlive(HitObject hitObject, bool keepAlive) + { + if (lifetimeEntryMap.TryGetValue(hitObject, out var entry)) + { + entry.KeepAlive = keepAlive; + return; + } + + if (!nestedPlayfields.IsValueCreated) + return; + + foreach (var p in nestedPlayfields.Value) + p.SetKeepAlive(hitObject, keepAlive); + } + + /// + /// Keeps all s alive within this and all nested s. + /// + public void KeepAllAlive() + { + foreach (var (_, entry) in lifetimeEntryMap) + entry.KeepAlive = true; + + if (!nestedPlayfields.IsValueCreated) + return; + + foreach (var p in nestedPlayfields.Value) + p.KeepAllAlive(); + } + /// /// The cursor currently being used by this . May be null if no cursor is provided. /// From 243e913e4a9129a2f1043d2ca8ba414828b9cedb Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 12 Nov 2020 18:32:20 +0900 Subject: [PATCH 40/55] Add hitobject usage events --- osu.Game/Rulesets/UI/HitObjectContainer.cs | 20 ++++++++++++++++++++ osu.Game/Rulesets/UI/Playfield.cs | 20 ++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/osu.Game/Rulesets/UI/HitObjectContainer.cs b/osu.Game/Rulesets/UI/HitObjectContainer.cs index 4164681ffc..1797f0acb8 100644 --- a/osu.Game/Rulesets/UI/HitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/HitObjectContainer.cs @@ -42,6 +42,22 @@ namespace osu.Game.Rulesets.UI /// public event Action RevertResult; + /// + /// Invoked when a becomes used by a . + /// + /// + /// If this uses pooled objects, this represents the time when the s become alive. + /// + public event Action HitObjectUsageBegan; + + /// + /// Invoked when a becomes unused by a . + /// + /// + /// If this uses pooled objects, this represents the time when the s become dead. + /// + public event Action HitObjectUsageFinished; + private readonly Dictionary startTimeMap = new Dictionary(); private readonly Dictionary drawableMap = new Dictionary(); private readonly LifetimeEntryManager lifetimeManager = new LifetimeEntryManager(); @@ -88,6 +104,8 @@ namespace osu.Game.Rulesets.UI bindStartTime(drawable); AddInternal(drawableMap[entry] = drawable, false); + + HitObjectUsageBegan?.Invoke(entry.HitObject); } private void removeDrawable(HitObjectLifetimeEntry entry) @@ -103,6 +121,8 @@ namespace osu.Game.Rulesets.UI unbindStartTime(drawable); RemoveInternal(drawable); + + HitObjectUsageFinished?.Invoke(entry.HitObject); } #endregion diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index 454880c885..8f2be81c36 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -29,6 +29,22 @@ namespace osu.Game.Rulesets.UI /// public event Action RevertResult; + /// + /// Invoked when a becomes used by a . + /// + /// + /// If this uses pooled objects, this represents the time when the s become alive. + /// + public event Action HitObjectUsageBegan; + + /// + /// Invoked when a becomes unused by a . + /// + /// + /// If this uses pooled objects, this represents the time when the s become dead. + /// + public event Action HitObjectUsageFinished; + /// /// The contained in this Playfield. /// @@ -88,6 +104,8 @@ namespace osu.Game.Rulesets.UI { 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); })); } @@ -247,6 +265,8 @@ namespace osu.Game.Rulesets.UI 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); } From 8aaa500431b490adfd898fb722c75f3582f6f179 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 12 Nov 2020 18:34:50 +0900 Subject: [PATCH 41/55] Add lifetime extensions --- osu.Game/Rulesets/UI/HitObjectContainer.cs | 12 +++++++- osu.Game/Rulesets/UI/Playfield.cs | 36 ++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/UI/HitObjectContainer.cs b/osu.Game/Rulesets/UI/HitObjectContainer.cs index 1797f0acb8..bca2466968 100644 --- a/osu.Game/Rulesets/UI/HitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/HitObjectContainer.cs @@ -58,6 +58,16 @@ namespace osu.Game.Rulesets.UI /// public event Action HitObjectUsageFinished; + /// + /// The amount of time prior to the current time within which s should be considered alive. + /// + public double PastLifetimeExtension { get; set; } + + /// + /// The amount of time after the current time within which s should be considered alive. + /// + public double FutureLifetimeExtension { get; set; } + private readonly Dictionary startTimeMap = new Dictionary(); private readonly Dictionary drawableMap = new Dictionary(); private readonly LifetimeEntryManager lifetimeManager = new LifetimeEntryManager(); @@ -179,7 +189,7 @@ namespace osu.Game.Rulesets.UI protected override bool CheckChildrenLife() { bool aliveChanged = base.CheckChildrenLife(); - aliveChanged |= lifetimeManager.Update(Time.Current, Time.Current); + aliveChanged |= lifetimeManager.Update(Time.Current - PastLifetimeExtension, Time.Current + FutureLifetimeExtension); return aliveChanged; } diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index 8f2be81c36..5794ff348c 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -240,6 +240,42 @@ namespace osu.Game.Rulesets.UI p.KeepAllAlive(); } + /// + /// The amount of time prior to the current time within which s should be considered alive. + /// + public double PastLifetimeExtension + { + get => HitObjectContainer.PastLifetimeExtension; + set + { + HitObjectContainer.PastLifetimeExtension = value; + + if (!nestedPlayfields.IsValueCreated) + return; + + foreach (var nested in nestedPlayfields.Value) + nested.PastLifetimeExtension = value; + } + } + + /// + /// The amount of time after the current time within which s should be considered alive. + /// + public double FutureLifetimeExtension + { + get => HitObjectContainer.FutureLifetimeExtension; + set + { + HitObjectContainer.FutureLifetimeExtension = value; + + if (!nestedPlayfields.IsValueCreated) + return; + + foreach (var nested in nestedPlayfields.Value) + nested.FutureLifetimeExtension = value; + } + } + /// /// The cursor currently being used by this . May be null if no cursor is provided. /// From 261ddd2b4a687c12245a963653aea3e76ff6e724 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 12 Nov 2020 18:48:25 +0900 Subject: [PATCH 42/55] Fix samples not being disposed --- .../Objects/Drawables/DrawableHitObject.cs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 244cf831c3..6af445939b 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -10,6 +10,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Primitives; using osu.Framework.Logging; using osu.Framework.Threading; @@ -126,6 +127,8 @@ namespace osu.Game.Rulesets.Objects.Drawables [CanBeNull] private HitObjectLifetimeEntry lifetimeEntry; + private Container samplesContainer; + /// /// Creates a new . /// @@ -142,6 +145,9 @@ namespace osu.Game.Rulesets.Objects.Drawables private void load(OsuConfigManager config) { config.BindWith(OsuSetting.PositionalHitSounds, userPositionalHitSounds); + + // Explicit non-virtual function call. + base.AddInternal(samplesContainer = new Container { RelativeSizeAxes = Axes.Both }); } protected override void LoadAsyncComplete() @@ -296,11 +302,8 @@ namespace osu.Game.Rulesets.Objects.Drawables /// protected virtual void LoadSamples() { - if (Samples != null) - { - RemoveInternal(Samples); - Samples = null; - } + samplesContainer.Clear(); + Samples = null; var samples = GetSamples().ToArray(); @@ -313,8 +316,7 @@ namespace osu.Game.Rulesets.Objects.Drawables + $" This is an indication that {nameof(HitObject.ApplyDefaults)} has not been invoked on {this}."); } - Samples = new PausableSkinnableSound(samples.Select(s => HitObject.SampleControlPoint.ApplyTo(s))); - AddInternal(Samples); + samplesContainer.Add(Samples = new PausableSkinnableSound(samples.Select(s => HitObject.SampleControlPoint.ApplyTo(s)))); } private void onSamplesChanged(object sender, NotifyCollectionChangedEventArgs e) => LoadSamples(); From 35329aa976747372fdfaa05079557b3abcfaecf4 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 13 Nov 2020 14:33:23 +0900 Subject: [PATCH 43/55] Reduce the number of state updates --- osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 56b725da0e..01f0e42d92 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -170,7 +170,6 @@ namespace osu.Game.Rulesets.Objects.Drawables { base.LoadComplete(); - StartTimeBindable.BindValueChanged(_ => updateState(State.Value, true)); comboIndexBindable.BindValueChanged(_ => updateComboColour(), true); updateState(ArmedState.Idle, true); @@ -220,6 +219,8 @@ namespace osu.Game.Rulesets.Objects.Drawables } StartTimeBindable.BindTo(HitObject.StartTimeBindable); + StartTimeBindable.BindValueChanged(onStartTimeChanged); + if (HitObject is IHasComboInformation combo) comboIndexBindable.BindTo(combo.ComboIndexBindable); @@ -249,9 +250,11 @@ namespace osu.Game.Rulesets.Objects.Drawables StartTimeBindable.UnbindFrom(HitObject.StartTimeBindable); if (HitObject is IHasComboInformation combo) comboIndexBindable.UnbindFrom(combo.ComboIndexBindable); - samplesBindable.UnbindFrom(HitObject.SamplesBindable); + // Changes in start time trigger state updates. When a new hitobject is applied, OnApply() automatically performs a state update anyway. + StartTimeBindable.ValueChanged -= onStartTimeChanged; + // When a new hitobject is applied, the samples will be cleared before re-populating. // In order to stop this needless update, the event is unbound and re-bound as late as possible in Apply(). samplesBindable.CollectionChanged -= onSamplesChanged; @@ -330,6 +333,8 @@ namespace osu.Game.Rulesets.Objects.Drawables private void onSamplesChanged(object sender, NotifyCollectionChangedEventArgs e) => LoadSamples(); + private void onStartTimeChanged(ValueChangedEvent startTime) => updateState(State.Value, true); + private void onNewResult(DrawableHitObject drawableHitObject, JudgementResult result) => OnNewResult?.Invoke(drawableHitObject, result); private void onRevertResult(DrawableHitObject drawableHitObject, JudgementResult result) => OnRevertResult?.Invoke(drawableHitObject, result); From a07d4a7915aa262bfdfe64e3ce5ba3465a2da523 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 13 Nov 2020 14:42:41 +0900 Subject: [PATCH 44/55] Remove unnecessary dictionary for now --- osu.Game/Rulesets/UI/Playfield.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index d1cb8ecbbd..573a57d701 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -136,8 +136,6 @@ namespace osu.Game.Rulesets.UI return false; } - private readonly Dictionary lifetimeEntryMap = new Dictionary(); - /// /// Adds a for a pooled to this . /// @@ -145,7 +143,6 @@ namespace osu.Game.Rulesets.UI public virtual void Add(HitObjectLifetimeEntry entry) { HitObjectContainer.Add(entry); - lifetimeEntryMap[entry.HitObject] = entry; OnHitObjectAdded(entry.HitObject); } @@ -156,9 +153,8 @@ namespace osu.Game.Rulesets.UI /// Whether the was successfully removed. public virtual bool Remove(HitObjectLifetimeEntry entry) { - if (lifetimeEntryMap.Remove(entry.HitObject)) + if (HitObjectContainer.Remove(entry)) { - HitObjectContainer.Remove(entry); OnHitObjectRemoved(entry.HitObject); return true; } From 4236dd826d7a834c7dc9056bfbfaf2dfa0d99e6b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 13 Nov 2020 14:57:09 +0900 Subject: [PATCH 45/55] Improve documentation and make abstract again --- .../Visual/Gameplay/TestScenePoolingRuleset.cs | 2 ++ osu.Game/Rulesets/UI/DrawableRuleset.cs | 13 +++++++++---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs index c3ae753eae..242eaf7b7d 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs @@ -142,6 +142,8 @@ namespace osu.Game.Tests.Visual.Gameplay protected override HitObjectLifetimeEntry CreateLifetimeEntry(TestHitObject hitObject) => new TestHitObjectLifetimeEntry(hitObject); + public override DrawableHitObject CreateDrawableRepresentation(TestHitObject h) => null; + protected override PassThroughInputManager CreateInputManager() => new PassThroughInputManager(); protected override Playfield CreatePlayfield() => new TestPlayfield(); diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index 0429936d8e..c912348604 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -324,11 +324,16 @@ namespace osu.Game.Rulesets.UI } /// - /// Creates a DrawableHitObject from a HitObject. + /// Creates a to represent a . /// - /// The HitObject to make drawable. - /// The DrawableHitObject. - public virtual DrawableHitObject CreateDrawableRepresentation(TObject h) => null; + /// + /// If this method returns null, then this will assume the requested type is being pooled, + /// and will instead attempt to retrieve the s at the point they should become alive via pools registered through + /// or . + /// + /// The to represent. + /// The representing . + public abstract DrawableHitObject CreateDrawableRepresentation(TObject h); public void Attach(KeyCounterDisplay keyCounter) => (KeyBindingInputManager as ICanAttachKeyCounter)?.Attach(keyCounter); From a9fc7572ed6ae0a9061c4d5d9ab8340829d59e5d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 13 Nov 2020 15:33:27 +0900 Subject: [PATCH 46/55] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 5078fee1cf..6e3d5eec1f 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,6 +52,6 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 405fb1a6ca..1850ee3488 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -26,7 +26,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 099ecd8319..2ac23f1503 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -88,7 +88,7 @@ - + From 4ef2e9548c01dce805c1ce02d89e40626146531a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 12 Nov 2020 19:52:02 +0900 Subject: [PATCH 47/55] Pass HitObjectComposer to BlueprintContainer via ctor --- .../Edit/ManiaBlueprintContainer.cs | 5 +- .../Edit/ManiaHitObjectComposer.cs | 5 +- .../Edit/OsuBlueprintContainer.cs | 5 +- .../Edit/OsuHitObjectComposer.cs | 5 +- .../Edit/TaikoBlueprintContainer.cs | 5 +- .../Edit/TaikoHitObjectComposer.cs | 5 +- .../TestSceneTimelineBlueprintContainer.cs | 2 +- osu.Game/Rulesets/Edit/HitObjectComposer.cs | 7 +- .../Compose/Components/BlueprintContainer.cs | 74 +++++++++++++------ .../Components/ComposeBlueprintContainer.cs | 26 +++---- .../Timeline/TimelineBlueprintContainer.cs | 3 +- .../Screens/Edit/Compose/ComposeScreen.cs | 2 +- 12 files changed, 83 insertions(+), 61 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaBlueprintContainer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaBlueprintContainer.cs index cea27498c3..2fa3f378ff 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaBlueprintContainer.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaBlueprintContainer.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Generic; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Mania.Edit.Blueprints; using osu.Game.Rulesets.Mania.Objects.Drawables; @@ -12,8 +11,8 @@ namespace osu.Game.Rulesets.Mania.Edit { public class ManiaBlueprintContainer : ComposeBlueprintContainer { - public ManiaBlueprintContainer(IEnumerable drawableHitObjects) - : base(drawableHitObjects) + public ManiaBlueprintContainer(HitObjectComposer composer) + : base(composer) { } diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs index 7e2469a794..01d572447b 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs @@ -13,7 +13,6 @@ using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces; using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; -using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Screens.Edit.Compose.Components; @@ -89,8 +88,8 @@ namespace osu.Game.Rulesets.Mania.Edit return drawableRuleset; } - protected override ComposeBlueprintContainer CreateBlueprintContainer(IEnumerable hitObjects) - => new ManiaBlueprintContainer(hitObjects); + protected override ComposeBlueprintContainer CreateBlueprintContainer() + => new ManiaBlueprintContainer(this); protected override IReadOnlyList CompositionTools => new HitObjectCompositionTool[] { diff --git a/osu.Game.Rulesets.Osu/Edit/OsuBlueprintContainer.cs b/osu.Game.Rulesets.Osu/Edit/OsuBlueprintContainer.cs index 330f34b85c..a68ed34e6b 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuBlueprintContainer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuBlueprintContainer.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Generic; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles; @@ -14,8 +13,8 @@ namespace osu.Game.Rulesets.Osu.Edit { public class OsuBlueprintContainer : ComposeBlueprintContainer { - public OsuBlueprintContainer(IEnumerable drawableHitObjects) - : base(drawableHitObjects) + public OsuBlueprintContainer(HitObjectComposer composer) + : base(composer) { } diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index edd684d886..bfa8ab4431 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -16,7 +16,6 @@ using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; -using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.UI; using osu.Game.Screens.Edit.Components.TernaryButtons; @@ -80,8 +79,8 @@ namespace osu.Game.Rulesets.Osu.Edit updateDistanceSnapGrid(); } - protected override ComposeBlueprintContainer CreateBlueprintContainer(IEnumerable hitObjects) - => new OsuBlueprintContainer(hitObjects); + protected override ComposeBlueprintContainer CreateBlueprintContainer() + => new OsuBlueprintContainer(this); private DistanceSnapGrid distanceSnapGrid; private Container distanceSnapGridContainer; diff --git a/osu.Game.Rulesets.Taiko/Edit/TaikoBlueprintContainer.cs b/osu.Game.Rulesets.Taiko/Edit/TaikoBlueprintContainer.cs index 35227b3c64..8b41448c9d 100644 --- a/osu.Game.Rulesets.Taiko/Edit/TaikoBlueprintContainer.cs +++ b/osu.Game.Rulesets.Taiko/Edit/TaikoBlueprintContainer.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Generic; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Taiko.Edit.Blueprints; @@ -11,8 +10,8 @@ namespace osu.Game.Rulesets.Taiko.Edit { public class TaikoBlueprintContainer : ComposeBlueprintContainer { - public TaikoBlueprintContainer(IEnumerable hitObjects) - : base(hitObjects) + public TaikoBlueprintContainer(HitObjectComposer composer) + : base(composer) { } diff --git a/osu.Game.Rulesets.Taiko/Edit/TaikoHitObjectComposer.cs b/osu.Game.Rulesets.Taiko/Edit/TaikoHitObjectComposer.cs index cdc9672a8e..161799c980 100644 --- a/osu.Game.Rulesets.Taiko/Edit/TaikoHitObjectComposer.cs +++ b/osu.Game.Rulesets.Taiko/Edit/TaikoHitObjectComposer.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit.Tools; -using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Screens.Edit.Compose.Components; @@ -24,7 +23,7 @@ namespace osu.Game.Rulesets.Taiko.Edit new SwellCompositionTool() }; - protected override ComposeBlueprintContainer CreateBlueprintContainer(IEnumerable hitObjects) - => new TaikoBlueprintContainer(hitObjects); + protected override ComposeBlueprintContainer CreateBlueprintContainer() + => new TaikoBlueprintContainer(this); } } diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimelineBlueprintContainer.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimelineBlueprintContainer.cs index e931be044c..5da63eddf1 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneTimelineBlueprintContainer.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneTimelineBlueprintContainer.cs @@ -10,7 +10,7 @@ namespace osu.Game.Tests.Visual.Editing [TestFixture] public class TestSceneTimelineBlueprintContainer : TimelineTestScene { - public override Drawable CreateTestComponent() => new TimelineBlueprintContainer(); + public override Drawable CreateTestComponent() => new TimelineBlueprintContainer(null); protected override void LoadComplete() { diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index c9dd061b48..b90aa6863a 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -104,7 +104,7 @@ namespace osu.Game.Rulesets.Edit drawableRulesetWrapper, // layers above playfield drawableRulesetWrapper.CreatePlayfieldAdjustmentContainer() - .WithChild(BlueprintContainer = CreateBlueprintContainer(HitObjects)) + .WithChild(BlueprintContainer = CreateBlueprintContainer()) } }, new FillFlowContainer @@ -182,9 +182,8 @@ namespace osu.Game.Rulesets.Edit /// /// Construct a relevant blueprint container. This will manage hitobject selection/placement input handling and display logic. /// - /// A live collection of all s in the editor beatmap. - protected virtual ComposeBlueprintContainer CreateBlueprintContainer(IEnumerable hitObjects) - => new ComposeBlueprintContainer(hitObjects); + protected virtual ComposeBlueprintContainer CreateBlueprintContainer() + => new ComposeBlueprintContainer(this); /// /// Construct a drawable ruleset for the provided ruleset. diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index b67f6a6ba6..3145616cf7 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using System.Collections.Specialized; using System.Diagnostics; using System.Linq; @@ -44,12 +45,16 @@ namespace osu.Game.Screens.Edit.Compose.Components protected EditorBeatmap Beatmap { get; private set; } private readonly BindableList selectedHitObjects = new BindableList(); + private readonly HitObjectComposer composer; + private readonly Dictionary blueprintMap = new Dictionary(); [Resolved(canBeNull: true)] private IPositionSnapProvider snapProvider { get; set; } - protected BlueprintContainer() + protected BlueprintContainer(HitObjectComposer composer) { + this.composer = composer; + RelativeSizeAxes = Axes.Both; } @@ -68,8 +73,12 @@ namespace osu.Game.Screens.Edit.Compose.Components DragBox.CreateProxy().With(p => p.Depth = float.MinValue) }); - foreach (var obj in Beatmap.HitObjects) - AddBlueprintFor(obj); + // For non-pooled rulesets, hitobjects are already present in the playfield which allows the blueprints to be loaded in the async context. + if (composer != null) + { + foreach (var obj in composer.HitObjects) + addBlueprintFor(obj.HitObject); + } selectedHitObjects.BindTo(Beatmap.SelectedHitObjects); selectedHitObjects.CollectionChanged += (selectedObjects, args) => @@ -94,7 +103,7 @@ namespace osu.Game.Screens.Edit.Compose.Components { base.LoadComplete(); - Beatmap.HitObjectAdded += AddBlueprintFor; + Beatmap.HitObjectAdded += addBlueprintFor; Beatmap.HitObjectRemoved += removeBlueprintFor; } @@ -247,29 +256,17 @@ namespace osu.Game.Screens.Edit.Compose.Components #region Blueprint Addition/Removal - private void removeBlueprintFor(HitObject hitObject) + private void addBlueprintFor(HitObject hitObject) { - var blueprint = SelectionBlueprints.SingleOrDefault(m => m.HitObject == hitObject); - if (blueprint == null) + if (blueprintMap.ContainsKey(hitObject)) return; - blueprint.Deselect(); - - blueprint.Selected -= onBlueprintSelected; - blueprint.Deselected -= onBlueprintDeselected; - - SelectionBlueprints.Remove(blueprint); - - if (movementBlueprint == blueprint) - finishSelectionMovement(); - } - - protected virtual void AddBlueprintFor(HitObject hitObject) - { var blueprint = CreateBlueprintFor(hitObject); if (blueprint == null) return; + blueprintMap[hitObject] = blueprint; + blueprint.Selected += onBlueprintSelected; blueprint.Deselected += onBlueprintDeselected; @@ -277,6 +274,41 @@ namespace osu.Game.Screens.Edit.Compose.Components blueprint.Select(); SelectionBlueprints.Add(blueprint); + + OnBlueprintAdded(hitObject); + } + + private void removeBlueprintFor(HitObject hitObject) + { + if (!blueprintMap.Remove(hitObject, out var blueprint)) + return; + + blueprint.Deselect(); + blueprint.Selected -= onBlueprintSelected; + blueprint.Deselected -= onBlueprintDeselected; + + SelectionBlueprints.Remove(blueprint); + + if (movementBlueprint == blueprint) + finishSelectionMovement(); + + OnBlueprintRemoved(hitObject); + } + + /// + /// Called after a blueprint has been added. + /// + /// The for which the blueprint has been added. + protected virtual void OnBlueprintAdded(HitObject hitObject) + { + } + + /// + /// Called after a blueprint has been removed. + /// + /// The for which the blueprint has been removed. + protected virtual void OnBlueprintRemoved(HitObject hitObject) + { } #endregion @@ -456,7 +488,7 @@ namespace osu.Game.Screens.Edit.Compose.Components if (Beatmap != null) { - Beatmap.HitObjectAdded -= AddBlueprintFor; + Beatmap.HitObjectAdded -= addBlueprintFor; Beatmap.HitObjectRemoved -= removeBlueprintFor; } } diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index 1527d20f54..27190e9aad 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -27,22 +27,18 @@ namespace osu.Game.Screens.Edit.Compose.Components /// public class ComposeBlueprintContainer : BlueprintContainer { - [Resolved] - private HitObjectComposer composer { get; set; } - - private PlacementBlueprint currentPlacement; - - private readonly Container placementBlueprintContainer; - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; + private readonly HitObjectComposer composer; + private readonly Container placementBlueprintContainer; + + private PlacementBlueprint currentPlacement; private InputManager inputManager; - private readonly IEnumerable drawableHitObjects; - - public ComposeBlueprintContainer(IEnumerable drawableHitObjects) + public ComposeBlueprintContainer(HitObjectComposer composer) + : base(composer) { - this.drawableHitObjects = drawableHitObjects; + this.composer = composer; placementBlueprintContainer = new Container { @@ -186,7 +182,7 @@ namespace osu.Game.Screens.Edit.Compose.Components protected sealed override SelectionBlueprint CreateBlueprintFor(HitObject hitObject) { - var drawable = drawableHitObjects.FirstOrDefault(d => d.HitObject == hitObject); + var drawable = composer.HitObjects.FirstOrDefault(d => d.HitObject == hitObject); if (drawable == null) return null; @@ -196,11 +192,11 @@ namespace osu.Game.Screens.Edit.Compose.Components public virtual OverlaySelectionBlueprint CreateBlueprintFor(DrawableHitObject hitObject) => null; - protected override void AddBlueprintFor(HitObject hitObject) + protected override void OnBlueprintAdded(HitObject hitObject) { - refreshTool(); + base.OnBlueprintAdded(hitObject); - base.AddBlueprintFor(hitObject); + refreshTool(); // on successful placement, the new combo button should be reset as this is the most common user interaction. if (Beatmap.SelectedHitObjects.Count == 0) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs index 10913a8bb9..078a158e3d 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs @@ -31,7 +31,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private SelectionBlueprint placementBlueprint; - public TimelineBlueprintContainer() + public TimelineBlueprintContainer(HitObjectComposer composer) + : base(composer) { RelativeSizeAxes = Axes.Both; Anchor = Anchor.Centre; diff --git a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs index 5282b4d998..d9948aa23c 100644 --- a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs +++ b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs @@ -53,6 +53,6 @@ namespace osu.Game.Screens.Edit.Compose return beatmapSkinProvider.WithChild(rulesetSkinProvider.WithChild(composer)); } - protected override Drawable CreateTimelineContent() => composer == null ? base.CreateTimelineContent() : new TimelineBlueprintContainer(); + protected override Drawable CreateTimelineContent() => composer == null ? base.CreateTimelineContent() : new TimelineBlueprintContainer(composer); } } From 3957697c4840072fc9e0b128d1e7981f91ee2988 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 13 Nov 2020 17:08:20 +0900 Subject: [PATCH 48/55] Add pooling support to the editor --- .../Edit/DrawableEditRulesetWrapper.cs | 9 ++---- .../Compose/Components/BlueprintContainer.cs | 28 ++++++++++++++++++- 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/osu.Game/Rulesets/Edit/DrawableEditRulesetWrapper.cs b/osu.Game/Rulesets/Edit/DrawableEditRulesetWrapper.cs index 8ed7885101..c60d4c7834 100644 --- a/osu.Game/Rulesets/Edit/DrawableEditRulesetWrapper.cs +++ b/osu.Game/Rulesets/Edit/DrawableEditRulesetWrapper.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -65,17 +64,13 @@ namespace osu.Game.Rulesets.Edit private void addHitObject(HitObject hitObject) { - var drawableObject = drawableRuleset.CreateDrawableRepresentation((TObject)hitObject); - - drawableRuleset.Playfield.Add(drawableObject); + drawableRuleset.AddHitObject((TObject)hitObject); drawableRuleset.Playfield.PostProcess(); } private void removeHitObject(HitObject hitObject) { - var drawableObject = Playfield.AllHitObjects.Single(d => d.HitObject == hitObject); - - drawableRuleset.Playfield.Remove(drawableObject); + drawableRuleset.RemoveHitObject((TObject)hitObject); drawableRuleset.Playfield.PostProcess(); } diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index 3145616cf7..450c42f7fe 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -105,6 +105,16 @@ namespace osu.Game.Screens.Edit.Compose.Components Beatmap.HitObjectAdded += addBlueprintFor; Beatmap.HitObjectRemoved += removeBlueprintFor; + + if (composer != null) + { + // For pooled rulesets, blueprints must be added for hitobjects already "current" as they would've not been "current" during the async load addition process above. + foreach (var obj in composer.HitObjects) + addBlueprintFor(obj.HitObject); + + composer.Playfield.HitObjectUsageBegan += addBlueprintFor; + composer.Playfield.HitObjectUsageFinished += removeBlueprintFor; + } } protected virtual Container CreateSelectionBlueprintContainer() => @@ -381,7 +391,13 @@ namespace osu.Game.Screens.Edit.Compose.Components /// /// Selects all s. /// - private void selectAll() => SelectionBlueprints.ToList().ForEach(m => m.Select()); + private void selectAll() + { + composer.Playfield.KeepAllAlive(); + + // Scheduled to allow the change in lifetime to take place. + Schedule(() => SelectionBlueprints.ToList().ForEach(m => m.Select())); + } /// /// Deselects all selected s. @@ -392,12 +408,16 @@ namespace osu.Game.Screens.Edit.Compose.Components { SelectionHandler.HandleSelected(blueprint); SelectionBlueprints.ChangeChildDepth(blueprint, 1); + + composer.Playfield.SetKeepAlive(blueprint.HitObject, true); } private void onBlueprintDeselected(SelectionBlueprint blueprint) { SelectionHandler.HandleDeselected(blueprint); SelectionBlueprints.ChangeChildDepth(blueprint, 0); + + composer.Playfield.SetKeepAlive(blueprint.HitObject, false); } #endregion @@ -491,6 +511,12 @@ namespace osu.Game.Screens.Edit.Compose.Components Beatmap.HitObjectAdded -= addBlueprintFor; Beatmap.HitObjectRemoved -= removeBlueprintFor; } + + if (composer != null) + { + composer.Playfield.HitObjectUsageBegan -= addBlueprintFor; + composer.Playfield.HitObjectUsageFinished -= removeBlueprintFor; + } } } } From 0219aff7bc7cbec5f4e41d533b5fbf17fd3a7c9b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 13 Nov 2020 17:10:29 +0900 Subject: [PATCH 49/55] Add timeline visible range adjustment --- .../Compose/Components/BlueprintContainer.cs | 29 ++++++++++--------- .../Components/ComposeBlueprintContainer.cs | 9 ++---- .../Compose/Components/Timeline/Timeline.cs | 5 ++++ .../Timeline/TimelineBlueprintContainer.cs | 8 +++-- 4 files changed, 29 insertions(+), 22 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index 450c42f7fe..3229719d5a 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -35,6 +35,8 @@ namespace osu.Game.Screens.Edit.Compose.Components protected SelectionHandler SelectionHandler { get; private set; } + protected readonly HitObjectComposer Composer; + [Resolved(CanBeNull = true)] private IEditorChangeHandler changeHandler { get; set; } @@ -45,7 +47,6 @@ namespace osu.Game.Screens.Edit.Compose.Components protected EditorBeatmap Beatmap { get; private set; } private readonly BindableList selectedHitObjects = new BindableList(); - private readonly HitObjectComposer composer; private readonly Dictionary blueprintMap = new Dictionary(); [Resolved(canBeNull: true)] @@ -53,7 +54,7 @@ namespace osu.Game.Screens.Edit.Compose.Components protected BlueprintContainer(HitObjectComposer composer) { - this.composer = composer; + Composer = composer; RelativeSizeAxes = Axes.Both; } @@ -74,9 +75,9 @@ namespace osu.Game.Screens.Edit.Compose.Components }); // For non-pooled rulesets, hitobjects are already present in the playfield which allows the blueprints to be loaded in the async context. - if (composer != null) + if (Composer != null) { - foreach (var obj in composer.HitObjects) + foreach (var obj in Composer.HitObjects) addBlueprintFor(obj.HitObject); } @@ -106,14 +107,14 @@ namespace osu.Game.Screens.Edit.Compose.Components Beatmap.HitObjectAdded += addBlueprintFor; Beatmap.HitObjectRemoved += removeBlueprintFor; - if (composer != null) + 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) + foreach (var obj in Composer.HitObjects) addBlueprintFor(obj.HitObject); - composer.Playfield.HitObjectUsageBegan += addBlueprintFor; - composer.Playfield.HitObjectUsageFinished += removeBlueprintFor; + Composer.Playfield.HitObjectUsageBegan += addBlueprintFor; + Composer.Playfield.HitObjectUsageFinished += removeBlueprintFor; } } @@ -393,7 +394,7 @@ namespace osu.Game.Screens.Edit.Compose.Components /// private void selectAll() { - composer.Playfield.KeepAllAlive(); + Composer.Playfield.KeepAllAlive(); // Scheduled to allow the change in lifetime to take place. Schedule(() => SelectionBlueprints.ToList().ForEach(m => m.Select())); @@ -409,7 +410,7 @@ namespace osu.Game.Screens.Edit.Compose.Components SelectionHandler.HandleSelected(blueprint); SelectionBlueprints.ChangeChildDepth(blueprint, 1); - composer.Playfield.SetKeepAlive(blueprint.HitObject, true); + Composer.Playfield.SetKeepAlive(blueprint.HitObject, true); } private void onBlueprintDeselected(SelectionBlueprint blueprint) @@ -417,7 +418,7 @@ namespace osu.Game.Screens.Edit.Compose.Components SelectionHandler.HandleDeselected(blueprint); SelectionBlueprints.ChangeChildDepth(blueprint, 0); - composer.Playfield.SetKeepAlive(blueprint.HitObject, false); + Composer.Playfield.SetKeepAlive(blueprint.HitObject, false); } #endregion @@ -512,10 +513,10 @@ namespace osu.Game.Screens.Edit.Compose.Components Beatmap.HitObjectRemoved -= removeBlueprintFor; } - if (composer != null) + if (Composer != null) { - composer.Playfield.HitObjectUsageBegan -= addBlueprintFor; - composer.Playfield.HitObjectUsageFinished -= removeBlueprintFor; + Composer.Playfield.HitObjectUsageBegan -= addBlueprintFor; + Composer.Playfield.HitObjectUsageFinished -= removeBlueprintFor; } } } diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index 27190e9aad..0d2e2360b1 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -29,7 +29,6 @@ namespace osu.Game.Screens.Edit.Compose.Components { public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; - private readonly HitObjectComposer composer; private readonly Container placementBlueprintContainer; private PlacementBlueprint currentPlacement; @@ -38,8 +37,6 @@ namespace osu.Game.Screens.Edit.Compose.Components public ComposeBlueprintContainer(HitObjectComposer composer) : base(composer) { - this.composer = composer; - placementBlueprintContainer = new Container { RelativeSizeAxes = Axes.Both @@ -158,7 +155,7 @@ namespace osu.Game.Screens.Edit.Compose.Components private void updatePlacementPosition() { - var snapResult = composer.SnapScreenSpacePositionToValidTime(inputManager.CurrentState.Mouse.Position); + var snapResult = Composer.SnapScreenSpacePositionToValidTime(inputManager.CurrentState.Mouse.Position); currentPlacement.UpdatePosition(snapResult); } @@ -169,7 +166,7 @@ namespace osu.Game.Screens.Edit.Compose.Components { base.Update(); - if (composer.CursorInPlacementArea) + if (Composer.CursorInPlacementArea) createPlacement(); else if (currentPlacement?.PlacementActive == false) removePlacement(); @@ -182,7 +179,7 @@ namespace osu.Game.Screens.Edit.Compose.Components protected sealed override SelectionBlueprint CreateBlueprintFor(HitObject hitObject) { - var drawable = composer.HitObjects.FirstOrDefault(d => d.HitObject == hitObject); + var drawable = Composer.HitObjects.FirstOrDefault(d => d.HitObject == hitObject); if (drawable == null) return null; diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs index 7233faa955..f6675902fc 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs @@ -219,6 +219,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline [Resolved] private IBeatSnapProvider beatSnapProvider { get; set; } + /// + /// The total amount of time visible on the timeline. + /// + public double VisibleRange => track.Length / Zoom; + public SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) => new SnapResult(screenSpacePosition, beatSnapProvider.SnapTime(getTimeFromPosition(Content.ToLocalSpace(screenSpacePosition)))); diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs index 078a158e3d..0271b2def9 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs @@ -26,9 +26,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private EditorBeatmap beatmap { get; set; } private DragEvent lastDragEvent; - private Bindable placement; - private SelectionBlueprint placementBlueprint; public TimelineBlueprintContainer(HitObjectComposer composer) @@ -98,6 +96,12 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline if (lastDragEvent != null) OnDrag(lastDragEvent); + if (Composer != null) + { + Composer.Playfield.PastLifetimeExtension = timeline.VisibleRange / 2; + Composer.Playfield.FutureLifetimeExtension = timeline.VisibleRange / 2; + } + base.Update(); } From 688a442fb3fc1bdb895212dce631ef716996a875 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 13 Nov 2020 17:26:46 +0900 Subject: [PATCH 50/55] Add missing dictionary --- osu.Game/Rulesets/UI/Playfield.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index e27375bd37..f12db57199 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -154,6 +154,8 @@ namespace osu.Game.Rulesets.UI return false; } + private readonly Dictionary lifetimeEntryMap = new Dictionary(); + /// /// Adds a for a pooled to this . /// @@ -161,6 +163,7 @@ namespace osu.Game.Rulesets.UI public virtual void Add(HitObjectLifetimeEntry entry) { HitObjectContainer.Add(entry); + lifetimeEntryMap[entry.HitObject] = entry; OnHitObjectAdded(entry.HitObject); } @@ -173,6 +176,7 @@ namespace osu.Game.Rulesets.UI { if (HitObjectContainer.Remove(entry)) { + lifetimeEntryMap.Remove(entry.HitObject); OnHitObjectRemoved(entry.HitObject); return true; } From 864e4006b96e2f1c2948ebc086e18f9c44d4f0ad Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 13 Nov 2020 17:51:01 +0900 Subject: [PATCH 51/55] Fix timeline test --- .../TestSceneTimelineBlueprintContainer.cs | 2 +- .../Visual/Editing/TimelineTestScene.cs | 6 ++++- osu.Game.Tests/WaveformTestBeatmap.cs | 25 +++---------------- 3 files changed, 10 insertions(+), 23 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimelineBlueprintContainer.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimelineBlueprintContainer.cs index 5da63eddf1..6b54bcb4f0 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneTimelineBlueprintContainer.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneTimelineBlueprintContainer.cs @@ -10,7 +10,7 @@ namespace osu.Game.Tests.Visual.Editing [TestFixture] public class TestSceneTimelineBlueprintContainer : TimelineTestScene { - public override Drawable CreateTestComponent() => new TimelineBlueprintContainer(null); + public override Drawable CreateTestComponent() => new TimelineBlueprintContainer(Composer); protected override void LoadComplete() { diff --git a/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs b/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs index fdb8781563..63bb018d6e 100644 --- a/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs +++ b/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs @@ -21,21 +21,25 @@ namespace osu.Game.Tests.Visual.Editing { protected TimelineArea TimelineArea { get; private set; } + protected HitObjectComposer Composer { get; private set; } + [BackgroundDependencyLoader] private void load(AudioManager audio) { Beatmap.Value = new WaveformTestBeatmap(audio); var playable = Beatmap.Value.GetPlayableBeatmap(Beatmap.Value.BeatmapInfo.Ruleset); - var editorBeatmap = new EditorBeatmap(playable); Dependencies.Cache(editorBeatmap); Dependencies.CacheAs(editorBeatmap); + Composer = playable.BeatmapInfo.Ruleset.CreateInstance().CreateHitObjectComposer().With(d => d.Alpha = 0); + AddRange(new Drawable[] { editorBeatmap, + Composer, new FillFlowContainer { AutoSizeAxes = Axes.Both, diff --git a/osu.Game.Tests/WaveformTestBeatmap.cs b/osu.Game.Tests/WaveformTestBeatmap.cs index f9613d9e25..8c8c827404 100644 --- a/osu.Game.Tests/WaveformTestBeatmap.cs +++ b/osu.Game.Tests/WaveformTestBeatmap.cs @@ -8,10 +8,9 @@ using osu.Framework.Audio; using osu.Framework.Audio.Track; using osu.Framework.Graphics.Textures; using osu.Game.Beatmaps; -using osu.Game.Beatmaps.Formats; -using osu.Game.IO; using osu.Game.IO.Archives; -using osu.Game.Rulesets.Catch; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Osu; using osu.Game.Tests.Beatmaps; using osu.Game.Tests.Resources; @@ -25,8 +24,8 @@ namespace osu.Game.Tests private readonly Beatmap beatmap; private readonly ITrackStore trackStore; - public WaveformTestBeatmap(AudioManager audioManager) - : this(audioManager, new WaveformBeatmap()) + public WaveformTestBeatmap(AudioManager audioManager, RulesetInfo rulesetInfo = null) + : this(audioManager, new TestBeatmap(rulesetInfo ?? new OsuRuleset().RulesetInfo)) { } @@ -63,21 +62,5 @@ namespace osu.Game.Tests return reader.Filenames.First(f => f.EndsWith(".mp3", StringComparison.Ordinal)); } } - - private class WaveformBeatmap : TestBeatmap - { - public WaveformBeatmap() - : base(new CatchRuleset().RulesetInfo) - { - } - - protected override Beatmap CreateBeatmap() - { - using (var reader = getZipReader()) - using (var beatmapStream = reader.GetStream(reader.Filenames.First(f => f.EndsWith(".osu", StringComparison.Ordinal)))) - using (var beatmapReader = new LineBufferedReader(beatmapStream)) - return Decoder.GetDecoder(beatmapReader).Decode(beatmapReader); - } - } } } From 92189e35ccb160bb41d6a4a52a9073f1b072bf0d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 13 Nov 2020 18:52:53 +0900 Subject: [PATCH 52/55] Make playfield KeepAlive methods internal --- osu.Game/Rulesets/UI/Playfield.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index f12db57199..fb2a60399b 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -210,7 +210,7 @@ namespace osu.Game.Rulesets.UI /// /// The to set. /// Whether to keep always alive. - public void SetKeepAlive(HitObject hitObject, bool keepAlive) + internal void SetKeepAlive(HitObject hitObject, bool keepAlive) { if (lifetimeEntryMap.TryGetValue(hitObject, out var entry)) { @@ -228,7 +228,7 @@ namespace osu.Game.Rulesets.UI /// /// Keeps all s alive within this and all nested s. /// - public void KeepAllAlive() + internal void KeepAllAlive() { foreach (var (_, entry) in lifetimeEntryMap) entry.KeepAlive = true; From 4a4219fd117edb714c9493bd0674ccca78df8c91 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 13 Nov 2020 18:53:37 +0900 Subject: [PATCH 53/55] Add region --- osu.Game/Rulesets/UI/Playfield.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index fb2a60399b..e2578e9822 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -154,8 +154,6 @@ namespace osu.Game.Rulesets.UI return false; } - private readonly Dictionary lifetimeEntryMap = new Dictionary(); - /// /// Adds a for a pooled to this . /// @@ -205,6 +203,10 @@ namespace osu.Game.Rulesets.UI { } + #region Editor logic + + private readonly Dictionary lifetimeEntryMap = new Dictionary(); + /// /// Sets whether to keep a given always alive within this or any nested . /// @@ -276,6 +278,8 @@ namespace osu.Game.Rulesets.UI } } + #endregion + /// /// The cursor currently being used by this . May be null if no cursor is provided. /// From d83b479c8122a0819f81819ef4967c80cff1b483 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 13 Nov 2020 18:54:49 +0900 Subject: [PATCH 54/55] Internalise lifetime extensions and events too --- osu.Game/Rulesets/UI/HitObjectContainer.cs | 8 +- osu.Game/Rulesets/UI/Playfield.cs | 186 ++++++++++----------- 2 files changed, 97 insertions(+), 97 deletions(-) diff --git a/osu.Game/Rulesets/UI/HitObjectContainer.cs b/osu.Game/Rulesets/UI/HitObjectContainer.cs index bca2466968..25fb7ab9f3 100644 --- a/osu.Game/Rulesets/UI/HitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/HitObjectContainer.cs @@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.UI /// /// If this uses pooled objects, this represents the time when the s become alive. /// - public event Action HitObjectUsageBegan; + internal event Action HitObjectUsageBegan; /// /// Invoked when a becomes unused by a . @@ -56,17 +56,17 @@ namespace osu.Game.Rulesets.UI /// /// If this uses pooled objects, this represents the time when the s become dead. /// - public event Action HitObjectUsageFinished; + internal event Action HitObjectUsageFinished; /// /// The amount of time prior to the current time within which s should be considered alive. /// - public double PastLifetimeExtension { get; set; } + internal double PastLifetimeExtension { get; set; } /// /// The amount of time after the current time within which s should be considered alive. /// - public double FutureLifetimeExtension { get; set; } + internal double FutureLifetimeExtension { get; set; } private readonly Dictionary startTimeMap = new Dictionary(); private readonly Dictionary drawableMap = new Dictionary(); diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index e2578e9822..6747145d50 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -29,22 +29,6 @@ namespace osu.Game.Rulesets.UI /// public event Action RevertResult; - /// - /// Invoked when a becomes used by a . - /// - /// - /// If this uses pooled objects, this represents the time when the s become alive. - /// - public event Action HitObjectUsageBegan; - - /// - /// Invoked when a becomes unused by a . - /// - /// - /// If this uses pooled objects, this represents the time when the s become dead. - /// - public event Action HitObjectUsageFinished; - /// /// The contained in this Playfield. /// @@ -203,83 +187,6 @@ namespace osu.Game.Rulesets.UI { } - #region Editor logic - - private readonly Dictionary lifetimeEntryMap = new Dictionary(); - - /// - /// Sets whether to keep a given always alive within this or any nested . - /// - /// The to set. - /// Whether to keep always alive. - internal void SetKeepAlive(HitObject hitObject, bool keepAlive) - { - if (lifetimeEntryMap.TryGetValue(hitObject, out var entry)) - { - entry.KeepAlive = keepAlive; - return; - } - - if (!nestedPlayfields.IsValueCreated) - return; - - foreach (var p in nestedPlayfields.Value) - p.SetKeepAlive(hitObject, keepAlive); - } - - /// - /// Keeps all s alive within this and all nested s. - /// - internal void KeepAllAlive() - { - foreach (var (_, entry) in lifetimeEntryMap) - entry.KeepAlive = true; - - if (!nestedPlayfields.IsValueCreated) - return; - - foreach (var p in nestedPlayfields.Value) - p.KeepAllAlive(); - } - - /// - /// The amount of time prior to the current time within which s should be considered alive. - /// - public double PastLifetimeExtension - { - get => HitObjectContainer.PastLifetimeExtension; - set - { - HitObjectContainer.PastLifetimeExtension = value; - - if (!nestedPlayfields.IsValueCreated) - return; - - foreach (var nested in nestedPlayfields.Value) - nested.PastLifetimeExtension = value; - } - } - - /// - /// The amount of time after the current time within which s should be considered alive. - /// - public double FutureLifetimeExtension - { - get => HitObjectContainer.FutureLifetimeExtension; - set - { - HitObjectContainer.FutureLifetimeExtension = value; - - if (!nestedPlayfields.IsValueCreated) - return; - - foreach (var nested in nestedPlayfields.Value) - nested.FutureLifetimeExtension = value; - } - } - - #endregion - /// /// The cursor currently being used by this . May be null if no cursor is provided. /// @@ -339,6 +246,99 @@ namespace osu.Game.Rulesets.UI /// protected virtual HitObjectContainer CreateHitObjectContainer() => new HitObjectContainer(); + #region Editor logic + + /// + /// Invoked when a becomes used by a . + /// + /// + /// If this uses pooled objects, this represents the time when the s become alive. + /// + internal event Action HitObjectUsageBegan; + + /// + /// Invoked when a becomes unused by a . + /// + /// + /// If this uses pooled objects, this represents the time when the s become dead. + /// + internal event Action HitObjectUsageFinished; + + private readonly Dictionary lifetimeEntryMap = new Dictionary(); + + /// + /// Sets whether to keep a given always alive within this or any nested . + /// + /// The to set. + /// Whether to keep always alive. + internal void SetKeepAlive(HitObject hitObject, bool keepAlive) + { + if (lifetimeEntryMap.TryGetValue(hitObject, out var entry)) + { + entry.KeepAlive = keepAlive; + return; + } + + if (!nestedPlayfields.IsValueCreated) + return; + + foreach (var p in nestedPlayfields.Value) + p.SetKeepAlive(hitObject, keepAlive); + } + + /// + /// Keeps all s alive within this and all nested s. + /// + internal void KeepAllAlive() + { + foreach (var (_, entry) in lifetimeEntryMap) + entry.KeepAlive = true; + + if (!nestedPlayfields.IsValueCreated) + return; + + foreach (var p in nestedPlayfields.Value) + p.KeepAllAlive(); + } + + /// + /// The amount of time prior to the current time within which s should be considered alive. + /// + internal double PastLifetimeExtension + { + get => HitObjectContainer.PastLifetimeExtension; + set + { + HitObjectContainer.PastLifetimeExtension = value; + + if (!nestedPlayfields.IsValueCreated) + return; + + foreach (var nested in nestedPlayfields.Value) + nested.PastLifetimeExtension = value; + } + } + + /// + /// The amount of time after the current time within which s should be considered alive. + /// + internal double FutureLifetimeExtension + { + get => HitObjectContainer.FutureLifetimeExtension; + set + { + HitObjectContainer.FutureLifetimeExtension = value; + + if (!nestedPlayfields.IsValueCreated) + return; + + foreach (var nested in nestedPlayfields.Value) + nested.FutureLifetimeExtension = value; + } + } + + #endregion + public class InvisibleCursorContainer : GameplayCursorContainer { protected override Drawable CreateCursor() => new InvisibleCursor(); From b4d4f5456c762f7867ef5a3db4e814a125348f38 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 14 Nov 2020 01:49:48 +0900 Subject: [PATCH 55/55] Fix broken fail judgement test --- .../Visual/Gameplay/TestSceneFailJudgement.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs index d80efb2c6e..745932315c 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs @@ -6,6 +6,7 @@ using System.Linq; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; using osu.Game.Screens.Play; namespace osu.Game.Tests.Visual.Gameplay @@ -21,8 +22,14 @@ namespace osu.Game.Tests.Visual.Gameplay protected override void AddCheckSteps() { AddUntilStep("wait for fail", () => Player.HasFailed); - AddUntilStep("wait for multiple judged objects", () => ((FailPlayer)Player).DrawableRuleset.Playfield.AllHitObjects.Count(h => h.AllJudged) > 1); - AddAssert("total judgements == 1", () => ((FailPlayer)Player).HealthProcessor.JudgedHits >= 1); + AddUntilStep("wait for multiple judgements", () => ((FailPlayer)Player).ScoreProcessor.JudgedHits > 1); + AddAssert("total number of results == 1", () => + { + var score = new ScoreInfo(); + ((FailPlayer)Player).ScoreProcessor.PopulateScore(score); + + return score.Statistics.Values.Sum() == 1; + }); } private class FailPlayer : TestPlayer