// 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 osu.Framework.Graphics; using osu.Game.Rulesets.Objects.Drawables; 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. /// public HitObjectContainer HitObjectContainer => hitObjectContainerLazy.Value; private readonly Lazy hitObjectContainerLazy; /// /// A function that converts gamefield coordinates to screen space. /// public Func GamefieldToScreenSpace => HitObjectContainer.ToScreenSpace; /// /// A function that converts screen space coordinates to gamefield. /// public Func ScreenSpaceToGamefield => HitObjectContainer.ToLocalSpace; /// /// All the s contained in this and all . /// public IEnumerable AllHitObjects { get { if (HitObjectContainer == null) return Enumerable.Empty(); var enumerable = HitObjectContainer.Objects; if (nestedPlayfields.IsValueCreated) enumerable = enumerable.Concat(NestedPlayfields.SelectMany(p => p.AllHitObjects)); return enumerable; } } /// /// All s nested inside this . /// public IEnumerable NestedPlayfields => nestedPlayfields.IsValueCreated ? nestedPlayfields.Value : Enumerable.Empty(); private readonly Lazy> nestedPlayfields = new Lazy>(); /// /// Whether judgements should be displayed by this and and all nested s. /// public readonly BindableBool DisplayJudgements = new BindableBool(true); /// /// Creates a new . /// protected Playfield() { RelativeSizeAxes = Axes.Both; 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)] private IReadOnlyList mods { get; set; } [BackgroundDependencyLoader] private void load() { Cursor = CreateCursor(); if (Cursor != null) { // initial showing of the cursor will be handed by MenuCursorContainer (via DrawableRuleset's IProvideCursor implementation). Cursor.Hide(); AddInternal(Cursor); } } /// /// Performs post-processing tasks (if any) after all DrawableHitObjects are loaded into this Playfield. /// public virtual void PostProcess() => NestedPlayfields.ForEach(p => p.PostProcess()); /// /// Adds a DrawableHitObject to this Playfield. /// /// The DrawableHitObject to add. public virtual void Add(DrawableHitObject h) { HitObjectContainer.Add(h); OnHitObjectAdded(h.HitObject); } /// /// Remove a DrawableHitObject from this Playfield. /// /// The DrawableHitObject to remove. public virtual bool Remove(DrawableHitObject h) { if (!HitObjectContainer.Remove(h)) return false; OnHitObjectRemoved(h.HitObject); return false; } /// /// Adds a for a pooled to this . /// /// The controlling the lifetime of the . public virtual void Add(HitObjectLifetimeEntry entry) { HitObjectContainer.Add(entry); lifetimeEntryMap[entry.HitObject] = entry; OnHitObjectAdded(entry.HitObject); } /// /// Removes a for a pooled from this . /// /// The controlling the lifetime of the . /// Whether the was successfully removed. public virtual bool Remove(HitObjectLifetimeEntry entry) { if (HitObjectContainer.Remove(entry)) { lifetimeEntryMap.Remove(entry.HitObject); OnHitObjectRemoved(entry.HitObject); return true; } bool removedFromNested = false; if (nestedPlayfields.IsValueCreated) removedFromNested = nestedPlayfields.Value.Any(p => p.Remove(entry)); return removedFromNested; } /// /// 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) { } #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. /// public GameplayCursorContainer Cursor { get; private set; } /// /// Provide a cursor which is to be used for gameplay. /// /// /// The default provided cursor is invisible when inside the bounds of the . /// /// The cursor, or null to show the menu cursor. protected virtual GameplayCursorContainer CreateCursor() => new InvisibleCursorContainer(); /// /// Registers a as a nested . /// This does not add the to the draw hierarchy. /// /// The to add. 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); } protected override void LoadComplete() { base.LoadComplete(); // in the case a consumer forgets to add the HitObjectContainer, we will add it here. if (HitObjectContainer.Parent == null) AddInternal(HitObjectContainer); } protected override void Update() { base.Update(); if (mods != null) { foreach (var mod in mods) { if (mod is IUpdatableByPlayfield updatable) updatable.Update(this); } } } /// /// Creates the container that will be used to contain the s. /// protected virtual HitObjectContainer CreateHitObjectContainer() => new HitObjectContainer(); public class InvisibleCursorContainer : GameplayCursorContainer { protected override Drawable CreateCursor() => new InvisibleCursor(); private class InvisibleCursor : Drawable { } } } }