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