// Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Configuration; using osu.Framework.Graphics.Primitives; using osu.Game.Audio; using osu.Game.Graphics; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Scoring; using osu.Game.Skinning; using OpenTK; using OpenTK.Graphics; namespace osu.Game.Rulesets.Objects.Drawables { public abstract class DrawableHitObject : SkinReloadableDrawable, IHasAccentColour { public readonly HitObject HitObject; /// /// The colour used for various elements of this DrawableHitObject. /// public virtual Color4 AccentColour { get; set; } = Color4.Gray; // Todo: Rulesets should be overriding the resources instead, but we need to figure out where/when to apply overrides first protected virtual string SampleNamespace => null; protected SkinnableSound Samples; protected virtual IEnumerable GetSamples() => HitObject.Samples; private readonly Lazy> nestedHitObjects = new Lazy>(); public bool HasNestedHitObjects => nestedHitObjects.IsValueCreated; public IReadOnlyList NestedHitObjects => nestedHitObjects.Value; public event Action OnJudgement; public event Action OnJudgementRemoved; public IReadOnlyList Judgements => judgements; private readonly List judgements = new List(); /// /// Whether a visible judgement should be displayed when this representation is hit. /// public virtual bool DisplayJudgement => true; /// /// Whether this and all of its nested s have been hit. /// public bool IsHit => Judgements.Any(j => j.Final && j.IsHit) && (!HasNestedHitObjects || NestedHitObjects.All(n => n.IsHit)); /// /// Whether this and all of its nested s have been judged. /// public bool AllJudged => (!ProvidesJudgement || judgementFinalized) && (!HasNestedHitObjects || NestedHitObjects.All(h => h.AllJudged)); /// /// Whether this can be judged. /// protected virtual bool ProvidesJudgement => true; private bool judgementOccurred; private bool judgementFinalized => judgements.LastOrDefault()?.Final == true; public bool Interactive = true; public override bool HandleKeyboardInput => Interactive; public override bool HandleMouseInput => Interactive; public override bool RemoveWhenNotAlive => false; public override bool RemoveCompletedTransforms => false; protected override bool RequiresChildrenUpdate => true; public readonly Bindable State = new Bindable(); protected DrawableHitObject(HitObject hitObject) { HitObject = hitObject; } [BackgroundDependencyLoader] private void load() { var samples = GetSamples().ToArray(); if (samples.Any()) { if (HitObject.SampleControlPoint == null) throw new ArgumentNullException(nameof(HitObject.SampleControlPoint), $"{nameof(HitObject)}s must always have an attached {nameof(HitObject.SampleControlPoint)}." + $" This is an indication that {nameof(HitObject.ApplyDefaults)} has not been invoked on {this}."); AddInternal(Samples = new SkinnableSound(samples.Select(s => new SampleInfo { Bank = s.Bank ?? HitObject.SampleControlPoint.SampleBank, Name = s.Name, Volume = s.Volume > 0 ? s.Volume : HitObject.SampleControlPoint.SampleVolume, Namespace = SampleNamespace }).ToArray())); } } protected override void SkinChanged(ISkinSource skin, bool allowFallback) { base.SkinChanged(skin, allowFallback); if (HitObject is IHasComboIndex combo) AccentColour = skin.GetComboColour(combo) ?? Color4.White; } protected override void LoadComplete() { base.LoadComplete(); State.ValueChanged += state => { UpdateState(state); // apply any custom state overrides ApplyCustomUpdateState?.Invoke(this, state); if (State == ArmedState.Hit) PlaySamples(); }; State.TriggerChange(); } protected abstract void UpdateState(ArmedState state); /// /// Bind to apply a custom state which can override the default implementation. /// public event Action ApplyCustomUpdateState; /// /// Plays all the hitsounds for this . /// public void PlaySamples() => Samples?.Play(); protected override void Update() { base.Update(); var endTime = (HitObject as IHasEndTime)?.EndTime ?? HitObject.StartTime; while (judgements.Count > 0) { var lastJudgement = judgements[judgements.Count - 1]; if (lastJudgement.TimeOffset + endTime <= Time.Current) break; judgements.RemoveAt(judgements.Count - 1); State.Value = ArmedState.Idle; OnJudgementRemoved?.Invoke(this, lastJudgement); } } protected override void UpdateAfterChildren() { base.UpdateAfterChildren(); UpdateJudgement(false); } protected virtual void AddNested(DrawableHitObject h) { h.OnJudgement += (d, j) => OnJudgement?.Invoke(d, j); h.OnJudgementRemoved += (d, j) => OnJudgementRemoved?.Invoke(d, j); h.ApplyCustomUpdateState += (d, j) => ApplyCustomUpdateState?.Invoke(d, j); nestedHitObjects.Value.Add(h); } /// /// Notifies that a new judgement has occurred for this . /// /// The . protected void AddJudgement(Judgement judgement) { judgementOccurred = true; // Ensure that the judgement is given a valid time offset, because this may not get set by the caller var endTime = (HitObject as IHasEndTime)?.EndTime ?? HitObject.StartTime; judgement.TimeOffset = Time.Current - endTime; judgements.Add(judgement); switch (judgement.Result) { case HitResult.None: break; case HitResult.Miss: State.Value = ArmedState.Miss; break; default: State.Value = ArmedState.Hit; break; } OnJudgement?.Invoke(this, judgement); } /// /// Processes this , checking if any judgements have occurred. /// /// Whether the user triggered this process. /// Whether a judgement has occurred from this or any nested s. protected bool UpdateJudgement(bool userTriggered) { judgementOccurred = false; if (AllJudged) return false; if (HasNestedHitObjects) foreach (var d in NestedHitObjects) judgementOccurred |= d.UpdateJudgement(userTriggered); if (!ProvidesJudgement || judgementFinalized || judgementOccurred) return judgementOccurred; var endTime = (HitObject as IHasEndTime)?.EndTime ?? HitObject.StartTime; CheckForJudgements(userTriggered, Time.Current - endTime); return judgementOccurred; } /// /// Checks if any judgements have occurred for this . This method must construct /// all s and notify of them through . /// /// Whether the user triggered this check. /// The offset from the end time at which this check occurred. A > 0 /// implies that this check occurred after the end time of . protected virtual void CheckForJudgements(bool userTriggered, double timeOffset) { } /// /// The screen-space point that causes this to be selected in the Editor. /// public virtual Vector2 SelectionPoint => ScreenSpaceDrawQuad.Centre; /// /// The screen-space quad that outlines this for selections in the Editor. /// public virtual Quad SelectionQuad => ScreenSpaceDrawQuad; } public abstract class DrawableHitObject : DrawableHitObject where TObject : HitObject { public new readonly TObject HitObject; protected DrawableHitObject(TObject hitObject) : base(hitObject) { HitObject = hitObject; } } }