Example #1
0
        protected HitObject GetMostValidObject()
        {
            // The most optimal lookup case we have is when an object is alive. There are usually very few alive objects so there's no drawbacks in attempting this lookup each time.
            var hitObject = hitObjectContainer.AliveObjects.FirstOrDefault(h => h.Result?.HasResult != true)?.HitObject;

            // In the case a next object isn't available in drawable form, we need to do a somewhat expensive traversal to get a valid sound to play.
            if (hitObject == null)
            {
                // This lookup can be skipped if the last entry is still valid (in the future and not yet hit).
                if (fallbackObject == null || fallbackObject.Result?.HasResult == true)
                {
                    // We need to use lifetime entries to find the next object (we can't just use `hitObjectContainer.Objects` due to pooling - it may even be empty).
                    // If required, we can make this lookup more efficient by adding support to get next-future-entry in LifetimeEntryManager.
                    fallbackObject = hitObjectContainer.Entries
                                     .Where(e => e.Result?.HasResult != true)
                                     .OrderBy(e => e.HitObject.StartTime)
                                     .FirstOrDefault();

                    // In the case there are no unjudged objects, the last hit object should be used instead.
                    fallbackObject ??= hitObjectContainer.Entries.LastOrDefault();
                }

                hitObject = fallbackObject?.HitObject;
            }

            return(hitObject);
        }
        protected override void RemoveDrawable(HitObjectLifetimeEntry entry, DrawableHitObject drawable)
        {
            base.RemoveDrawable(entry, drawable);

            drawable.DefaultsApplied -= invalidateHitObject;
            layoutComputed.Remove(drawable);
        }
        protected override void AddDrawable(HitObjectLifetimeEntry entry, DrawableHitObject drawable)
        {
            base.AddDrawable(entry, drawable);

            invalidateHitObject(drawable);
            drawable.DefaultsApplied += invalidateHitObject;
        }
Example #4
0
 /// <summary>
 /// Creates a new <see cref="DrawableHitObject"/>.
 /// </summary>
 /// <param name="initialHitObject">
 /// The <see cref="HitObject"/> to be initially applied to this <see cref="DrawableHitObject"/>.
 /// If <c>null</c>, a hitobject is expected to be later applied via <see cref="Apply(osu.Game.Rulesets.Objects.HitObjectLifetimeEntry)"/> (or automatically via pooling).
 /// </param>
 protected DrawableHitObject([CanBeNull] HitObject initialHitObject = null)
 {
     if (initialHitObject != null)
     {
         lifetimeEntry = new SyntheticHitObjectEntry(initialHitObject);
         ensureEntryHasResult();
     }
 }
Example #5
0
 public void Apply([NotNull] HitObject hitObject, [CanBeNull] HitObjectLifetimeEntry lifetimeEntry)
 {
     if (lifetimeEntry != null)
     {
         Apply(lifetimeEntry);
     }
     else
     {
         Apply(hitObject);
     }
 }
Example #6
0
        /// <summary>
        /// Removes the currently applied <see cref="HitObject"/>
        /// </summary>
        private void free()
        {
            if (!hasHitObjectApplied)
            {
                return;
            }

            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;

            // Release the samples for other hitobjects to use.
            if (Samples != null)
            {
                Samples.Samples = null;
            }

            if (nestedHitObjects.IsValueCreated)
            {
                foreach (var obj in nestedHitObjects.Value)
                {
                    obj.OnNewResult            -= onNewResult;
                    obj.OnRevertResult         -= onRevertResult;
                    obj.ApplyCustomUpdateState -= onApplyCustomUpdateState;
                }

                nestedHitObjects.Value.Clear();
                ClearNestedHitObjects();
            }

            HitObject.DefaultsApplied -= onDefaultsApplied;

            OnFree();

            HitObject       = null;
            ParentHitObject = null;
            Result          = null;
            lifetimeEntry   = null;

            clearExistingStateTransforms();

            hasHitObjectApplied = false;
        }
Example #7
0
        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);
        }
Example #8
0
        private void addDrawable(HitObjectLifetimeEntry entry)
        {
            Debug.Assert(!drawableMap.ContainsKey(entry));

            var drawable = pooledObjectProvider.GetPooledDrawableRepresentation(entry.HitObject);

            if (drawable == null)
            {
                throw new InvalidOperationException($"A drawable representation could not be retrieved for hitobject type: {entry.HitObject.GetType().ReadableName()}.");
            }

            drawable.OnNewResult    += onNewResult;
            drawable.OnRevertResult += onRevertResult;

            bindStartTime(drawable);
            AddInternal(drawableMap[entry] = drawable, false);

            HitObjectUsageBegan?.Invoke(entry.HitObject);
        }
Example #9
0
        private void removeDrawable(HitObjectLifetimeEntry entry)
        {
            Debug.Assert(drawableMap.ContainsKey(entry));

            var drawable = drawableMap[entry];

            // OnKilled can potentially change the hitobject's result, so it needs to run first before unbinding.
            drawable.OnKilled();
            drawable.OnNewResult    -= onNewResult;
            drawable.OnRevertResult -= onRevertResult;

            drawableMap.Remove(entry);

            OnRemove(drawable);
            unbindStartTime(drawable);
            RemoveInternal(drawable);

            HitObjectUsageFinished?.Invoke(entry.HitObject);
        }
Example #10
0
        public void TestCorrectHitObject()
        {
            HitObjectLifetimeEntry nextObjectEntry = null;

            AddAssert("no alive objects", () => getNextAliveObject() == null);

            AddAssert("check initially correct object", () => sampleTriggerSource.GetMostValidObject() == beatmap.HitObjects[0]);

            AddUntilStep("get next object", () =>
            {
                var nextDrawableObject = getNextAliveObject();

                if (nextDrawableObject != null)
                {
                    nextObjectEntry = nextDrawableObject.Entry;
                    InputManager.MoveMouseTo(nextDrawableObject.ScreenSpaceDrawQuad.Centre);
                    return(true);
                }

                return(false);
            });

            AddUntilStep("hit first hitobject", () =>
            {
                InputManager.Click(MouseButton.Left);
                return(nextObjectEntry.Result.HasResult);
            });

            AddAssert("check correct object after hit", () => sampleTriggerSource.GetMostValidObject() == beatmap.HitObjects[1]);

            AddUntilStep("check correct object after miss", () => sampleTriggerSource.GetMostValidObject() == beatmap.HitObjects[2]);
            AddUntilStep("check correct object after miss", () => sampleTriggerSource.GetMostValidObject() == beatmap.HitObjects[3]);

            AddUntilStep("no alive objects", () => getNextAliveObject() == null);
            AddAssert("check correct object after none alive", () => sampleTriggerSource.GetMostValidObject() == beatmap.HitObjects[3]);
        }
Example #11
0
        /// <summary>
        /// Applies a new <see cref="HitObject"/> to be represented by this <see cref="DrawableHitObject"/>.
        /// </summary>
        /// <param name="hitObject">The <see cref="HitObject"/> to apply.</param>
        /// <param name="lifetimeEntry">The <see cref="HitObjectLifetimeEntry"/> controlling the lifetime of <paramref name="hitObject"/>.</param>
        public void Apply([NotNull] HitObject hitObject, [CanBeNull] HitObjectLifetimeEntry lifetimeEntry)
        {
            free();

            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;
            }
            else
            {
                LifetimeStart = HitObject.StartTime - InitialLifetimeOffset;
            }

            // 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 pooledDrawableNested = pooledObjectProvider?.GetPooledDrawableRepresentation(h, this);
                var drawableNested       = pooledDrawableNested
                                           ?? CreateNestedHitObject(h)
                                           ?? throw new InvalidOperationException($"{nameof(CreateNestedHitObject)} returned null for {h.GetType().ReadableName()}.");

                // Only invoke the event for non-pooled DHOs, otherwise the event will be fired by the playfield.
                if (pooledDrawableNested == null)
                {
                    OnNestedDrawableCreated?.Invoke(drawableNested);
                }

                drawableNested.OnNewResult            += onNewResult;
                drawableNested.OnRevertResult         += onRevertResult;
                drawableNested.ApplyCustomUpdateState += onApplyCustomUpdateState;

                // This is only necessary for non-pooled DHOs. For pooled DHOs, this is handled inside GetPooledDrawableRepresentation().
                // Must be done before the nested DHO is added to occur before the nested Apply()!
                drawableNested.ParentHitObject = this;

                nestedHitObjects.Value.Add(drawableNested);
                AddNestedHitObject(drawableNested);
            }

            StartTimeBindable.BindTo(HitObject.StartTimeBindable);
            StartTimeBindable.BindValueChanged(onStartTimeChanged);

            if (HitObject is IHasComboInformation combo)
            {
                comboIndexBindable.BindTo(combo.ComboIndexBindable);
            }

            samplesBindable.BindTo(HitObject.SamplesBindable);
            samplesBindable.BindCollectionChanged(onSamplesChanged, true);

            HitObject.DefaultsApplied += onDefaultsApplied;

            OnApply();
            HitObjectApplied?.Invoke(this);

            // If not loaded, the state update happens in LoadComplete().
            if (IsLoaded)
            {
                if (Result.IsHit)
                {
                    updateState(ArmedState.Hit, true);
                }
                else if (Result.HasResult)
                {
                    updateState(ArmedState.Miss, true);
                }
                else
                {
                    updateState(ArmedState.Idle, true);
                }
            }

            hasHitObjectApplied = true;
        }
Example #12
0
        /// <summary>
        /// Applies a new <see cref="HitObject"/> to be represented by this <see cref="DrawableHitObject"/>.
        /// </summary>
        /// <param name="hitObject">The <see cref="HitObject"/> to apply.</param>
        /// <param name="lifetimeEntry">The <see cref="HitObjectLifetimeEntry"/> controlling the lifetime of <paramref name="hitObject"/>.</param>
        public void Apply([NotNull] HitObject hitObject, [CanBeNull] HitObjectLifetimeEntry lifetimeEntry)
        {
            free();

            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;
            }
            else
            {
                LifetimeStart = HitObject.StartTime - InitialLifetimeOffset;
            }

            // 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 = pooledObjectProvider?.GetPooledDrawableRepresentation(h)
                                     ?? CreateNestedHitObject(h)
                                     ?? throw new InvalidOperationException($"{nameof(CreateNestedHitObject)} returned null for {h.GetType().ReadableName()}.");

                drawableNested.OnNewResult            += onNewResult;
                drawableNested.OnRevertResult         += onRevertResult;
                drawableNested.ApplyCustomUpdateState += onApplyCustomUpdateState;

                nestedHitObjects.Value.Add(drawableNested);
                AddNestedHitObject(drawableNested);

                drawableNested.OnParentReceived(this);
            }

            StartTimeBindable.BindTo(HitObject.StartTimeBindable);
            StartTimeBindable.BindValueChanged(onStartTimeChanged);

            if (HitObject is IHasComboInformation combo)
            {
                comboIndexBindable.BindTo(combo.ComboIndexBindable);
            }

            samplesBindable.BindTo(HitObject.SamplesBindable);
            samplesBindable.BindCollectionChanged(onSamplesChanged, true);

            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)
            {
                Schedule(() => updateState(ArmedState.Idle, true));
            }

            hasHitObjectApplied = true;
        }
Example #13
0
 public bool Remove(HitObjectLifetimeEntry entry) => lifetimeManager.RemoveEntry(entry);
Example #14
0
 public void Add(HitObjectLifetimeEntry entry) => lifetimeManager.AddEntry(entry);
Example #15
0
        /// <summary>
        /// Applies a new <see cref="HitObjectLifetimeEntry"/> to be represented by this <see cref="DrawableHitObject"/>.
        /// </summary>
        public void Apply([NotNull] HitObjectLifetimeEntry newEntry)
        {
            free();

            lifetimeEntry = newEntry;

            // LifetimeStart is already computed using HitObjectLifetimeEntry's InitialLifetimeOffset.
            // We override this with DHO's InitialLifetimeOffset for a non-pooled DHO.
            if (newEntry is SyntheticHitObjectEntry)
            {
                lifetimeEntry.LifetimeStart = HitObject.StartTime - InitialLifetimeOffset;
            }

            LifetimeStart = lifetimeEntry.LifetimeStart;
            LifetimeEnd   = lifetimeEntry.LifetimeEnd;

            ensureEntryHasResult();

            foreach (var h in HitObject.NestedHitObjects)
            {
                var pooledDrawableNested = pooledObjectProvider?.GetPooledDrawableRepresentation(h, this);
                var drawableNested       = pooledDrawableNested
                                           ?? CreateNestedHitObject(h)
                                           ?? throw new InvalidOperationException($"{nameof(CreateNestedHitObject)} returned null for {h.GetType().ReadableName()}.");

                // Only invoke the event for non-pooled DHOs, otherwise the event will be fired by the playfield.
                if (pooledDrawableNested == null)
                {
                    OnNestedDrawableCreated?.Invoke(drawableNested);
                }

                drawableNested.OnNewResult            += onNewResult;
                drawableNested.OnRevertResult         += onRevertResult;
                drawableNested.ApplyCustomUpdateState += onApplyCustomUpdateState;

                // This is only necessary for non-pooled DHOs. For pooled DHOs, this is handled inside GetPooledDrawableRepresentation().
                // Must be done before the nested DHO is added to occur before the nested Apply()!
                drawableNested.ParentHitObject = this;

                nestedHitObjects.Add(drawableNested);
                AddNestedHitObject(drawableNested);
            }

            StartTimeBindable.BindTo(HitObject.StartTimeBindable);
            StartTimeBindable.BindValueChanged(onStartTimeChanged);

            if (HitObject is IHasComboInformation combo)
            {
                comboIndexBindable.BindTo(combo.ComboIndexBindable);
            }

            samplesBindable.BindTo(HitObject.SamplesBindable);
            samplesBindable.BindCollectionChanged(onSamplesChanged, true);

            HitObject.DefaultsApplied += onDefaultsApplied;

            OnApply();
            HitObjectApplied?.Invoke(this);

            // If not loaded, the state update happens in LoadComplete().
            if (IsLoaded)
            {
                if (Result.IsHit)
                {
                    updateState(ArmedState.Hit, true);
                }
                else if (Result.HasResult)
                {
                    updateState(ArmedState.Miss, true);
                }
                else
                {
                    updateState(ArmedState.Idle, true);
                }
            }

            hasEntryApplied = true;
        }