// 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 osu.Framework.Extensions.IEnumerableExtensions; using osuTK; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables.Connections; using osu.Game.Rulesets.UI; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.UI.Cursor; using osu.Game.Skinning; namespace osu.Game.Rulesets.Osu.UI { public class OsuPlayfield : Playfield { private readonly ApproachCircleProxyContainer approachCircles; private readonly JudgementContainer judgementLayer; private readonly FollowPointRenderer followPoints; public static readonly Vector2 BASE_SIZE = new Vector2(512, 384); protected override GameplayCursorContainer CreateCursor() => new OsuCursorContainer(); public OsuPlayfield() { InternalChildren = new Drawable[] { followPoints = new FollowPointRenderer { RelativeSizeAxes = Axes.Both, Depth = 2, }, judgementLayer = new JudgementContainer { RelativeSizeAxes = Axes.Both, Depth = 1, }, // Todo: This should not exist, but currently helps to reduce LOH allocations due to unbinding skin source events on judgement disposal // Todo: Remove when hitobjects are properly pooled new SkinProvidingContainer(null) { Child = HitObjectContainer, }, approachCircles = new ApproachCircleProxyContainer { RelativeSizeAxes = Axes.Both, Depth = -1, }, }; } public override void Add(DrawableHitObject h) { h.OnNewResult += onNewResult; h.OnLoadComplete += d => { if (d is IDrawableHitObjectWithProxiedApproach c) approachCircles.Add(c.ProxiedLayer.CreateProxy()); }; base.Add(h); DrawableOsuHitObject osuHitObject = (DrawableOsuHitObject)h; osuHitObject.CheckHittable = checkHittable; followPoints.AddFollowPoints(osuHitObject); } public override bool Remove(DrawableHitObject h) { bool result = base.Remove(h); if (result) { DrawableOsuHitObject osuHitObject = (DrawableOsuHitObject)h; osuHitObject.CheckHittable = null; followPoints.RemoveFollowPoints(osuHitObject); } return result; } private bool checkHittable(DrawableOsuHitObject osuHitObject) { DrawableHitObject lastObject = osuHitObject; // Get the last hitobject that contributes to note lock while ((lastObject = HitObjectContainer.AliveObjects.GetPrevious(lastObject)) != null) { if (contributesToNoteLock(lastObject.HitObject)) break; } // If there is no previous object alive, allow the hit. if (lastObject == null) return true; // Ensure that either the last object has received a judgement or the hit time occurs at or after the last object's start time. // Simultaneous hitobjects are allowed to be hit at the same time value to account for edge-cases such as Centipede. if (lastObject.Judged || Time.Current >= lastObject.HitObject.StartTime) return true; return false; } private void onNewResult(DrawableHitObject judgedObject, JudgementResult result) { missAllEarlier(result); if (!judgedObject.DisplayResult || !DisplayJudgements.Value) return; DrawableOsuJudgement explosion = new DrawableOsuJudgement(result, judgedObject) { Origin = Anchor.Centre, Position = ((OsuHitObject)judgedObject.HitObject).StackedEndPosition, Scale = new Vector2(((OsuHitObject)judgedObject.HitObject).Scale) }; judgementLayer.Add(explosion); } /// /// Misses all s occurring earlier than the start time of a judged . /// /// The of the judged . private void missAllEarlier(JudgementResult result) { if (!causesNoteLockMisses(result.HitObject)) return; // The minimum start time required for hitobjects so that they aren't missed. double minimumTime = result.HitObject.StartTime; foreach (var obj in HitObjectContainer.AliveObjects) { if (obj.HitObject.StartTime >= minimumTime) break; performMiss(obj); foreach (var n in obj.NestedHitObjects) { if (n.HitObject.StartTime >= minimumTime) break; performMiss(n); } } } private void performMiss(DrawableHitObject obj) { if (!(obj is DrawableOsuHitObject osuObject)) throw new InvalidOperationException($"{obj.GetType()} is not a {nameof(DrawableOsuHitObject)}."); // Hitobjects that have already been judged cannot be missed. if (osuObject.Judged) return; if (!causesNoteLockMisses(obj.HitObject)) return; osuObject.MissForcefully(); } /// /// Whether a is contributes to note lock. /// Future contributing s will not be hittable until the start time of the last contributing is reached. /// /// The to test. /// Whether causes note lock. private bool contributesToNoteLock(HitObject hitObject) => hitObject is HitCircle || hitObject is Slider; /// /// Whether a can be missed and causes other s to be missed when hit out-of-order during note lock. /// /// The to test. /// Whether contributes to note lock misses. private bool causesNoteLockMisses(HitObject hitObject) => hitObject is HitCircle && !(hitObject is SliderTailCircle); public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => HitObjectContainer.ReceivePositionalInputAt(screenSpacePos); private class ApproachCircleProxyContainer : LifetimeManagementContainer { public void Add(Drawable approachCircleProxy) => AddInternal(approachCircleProxy); } } }