protected sealed override void OnApply(HitObjectLifetimeEntry entry) { // LifetimeStart is already computed using HitObjectLifetimeEntry's InitialLifetimeOffset. // We override this with DHO's InitialLifetimeOffset for a non-pooled DHO. if (entry is SyntheticHitObjectEntry) LifetimeStart = HitObject.StartTime - InitialLifetimeOffset; 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); } }
/// <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; }
/// <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); var drawableNested = pooledDrawableNested ?? CreateNestedHitObject(h) ?? throw new InvalidOperationException($"{nameof(CreateNestedHitObject)} returned null for {h.GetType().ReadableName()}."); // Invoke the event only if this nested object is just created by `CreateNestedHitObject`. if (pooledDrawableNested == null) { OnNestedDrawableCreated?.Invoke(drawableNested); } 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; }