From d2775680e66129c34297246ce047914d2b94fd90 Mon Sep 17 00:00:00 2001 From: Chandler Stowell Date: Wed, 24 Jan 2024 13:13:45 -0500 Subject: [PATCH 01/26] use stack to pass action state when applying hit results this removes closure allocations --- .../Drawables/DrawableEmptyFreeformHitObject.cs | 2 +- .../Drawables/DrawablePippidonHitObject.cs | 7 ++++++- .../Drawables/DrawableEmptyScrollingHitObject.cs | 2 +- .../Drawables/DrawablePippidonHitObject.cs | 7 ++++++- .../Objects/Drawables/DrawableCatchHitObject.cs | 7 ++++++- .../Objects/Drawables/DrawableHoldNote.cs | 2 +- .../Objects/Drawables/DrawableHoldNoteBody.cs | 2 +- .../Objects/Drawables/DrawableManiaHitObject.cs | 2 +- .../Objects/Drawables/DrawableNote.cs | 4 ++-- .../Objects/Drawables/DrawableHitCircle.cs | 15 ++++++++------- .../Objects/Drawables/DrawableOsuHitObject.cs | 4 ++-- .../Objects/Drawables/DrawableSlider.cs | 13 ++++++++----- .../Objects/Drawables/DrawableSpinner.cs | 12 ++++++------ .../Objects/Drawables/DrawableSpinnerTick.cs | 2 +- .../Objects/Drawables/DrawableDrumRoll.cs | 7 +++++-- .../Objects/Drawables/DrawableDrumRollTick.cs | 11 +++++++---- .../Objects/Drawables/DrawableFlyingHit.cs | 2 +- .../Objects/Drawables/DrawableHit.cs | 12 ++++++------ .../Objects/Drawables/DrawableStrongNestedHit.cs | 2 +- .../Objects/Drawables/DrawableSwell.cs | 8 ++++++-- .../Objects/Drawables/DrawableSwellTick.cs | 5 ++++- .../Objects/Drawables/DrawableHitObject.cs | 13 +++++++++++-- 22 files changed, 91 insertions(+), 50 deletions(-) diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Objects/Drawables/DrawableEmptyFreeformHitObject.cs b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Objects/Drawables/DrawableEmptyFreeformHitObject.cs index 744e207b57..e8f511bc4b 100644 --- a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Objects/Drawables/DrawableEmptyFreeformHitObject.cs +++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Objects/Drawables/DrawableEmptyFreeformHitObject.cs @@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.EmptyFreeform.Objects.Drawables { if (timeOffset >= 0) // todo: implement judgement logic - ApplyResult(r => r.Type = HitResult.Perfect); + ApplyResult(static r => r.Type = HitResult.Perfect); } protected override void UpdateHitStateTransforms(ArmedState state) diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Objects/Drawables/DrawablePippidonHitObject.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Objects/Drawables/DrawablePippidonHitObject.cs index c5ada4288d..a8bb57ba18 100644 --- a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Objects/Drawables/DrawablePippidonHitObject.cs +++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Objects/Drawables/DrawablePippidonHitObject.cs @@ -49,7 +49,12 @@ namespace osu.Game.Rulesets.Pippidon.Objects.Drawables protected override void CheckForResult(bool userTriggered, double timeOffset) { if (timeOffset >= 0) - ApplyResult(r => r.Type = IsHovered ? HitResult.Perfect : HitResult.Miss); + { + ApplyResult(static (r, isHovered) => + { + r.Type = isHovered ? HitResult.Perfect : HitResult.Miss; + }, IsHovered); + } } protected override double InitialLifetimeOffset => time_preempt; diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Objects/Drawables/DrawableEmptyScrollingHitObject.cs b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Objects/Drawables/DrawableEmptyScrollingHitObject.cs index a3c3b89105..070a802aea 100644 --- a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Objects/Drawables/DrawableEmptyScrollingHitObject.cs +++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Objects/Drawables/DrawableEmptyScrollingHitObject.cs @@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.EmptyScrolling.Objects.Drawables { if (timeOffset >= 0) // todo: implement judgement logic - ApplyResult(r => r.Type = HitResult.Perfect); + ApplyResult(static r => r.Type = HitResult.Perfect); } protected override void UpdateHitStateTransforms(ArmedState state) diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Objects/Drawables/DrawablePippidonHitObject.cs b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Objects/Drawables/DrawablePippidonHitObject.cs index d198fa81cb..9983ec20b0 100644 --- a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Objects/Drawables/DrawablePippidonHitObject.cs +++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Objects/Drawables/DrawablePippidonHitObject.cs @@ -49,7 +49,12 @@ namespace osu.Game.Rulesets.Pippidon.Objects.Drawables protected override void CheckForResult(bool userTriggered, double timeOffset) { if (timeOffset >= 0) - ApplyResult(r => r.Type = currentLane.Value == HitObject.Lane ? HitResult.Perfect : HitResult.Miss); + { + ApplyResult(static (r, pippidonHitObject) => + { + r.Type = pippidonHitObject.currentLane.Value == pippidonHitObject.HitObject.Lane ? HitResult.Perfect : HitResult.Miss; + }, this); + } } protected override void UpdateHitStateTransforms(ArmedState state) diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs index 7f8c17861d..5a921f36f5 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs @@ -63,7 +63,12 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables if (CheckPosition == null) return; if (timeOffset >= 0 && Result != null) - ApplyResult(r => r.Type = CheckPosition.Invoke(HitObject) ? r.Judgement.MaxResult : r.Judgement.MinResult); + { + ApplyResult(static (r, state) => + { + r.Type = state.CheckPosition.Invoke(state.HitObject) ? r.Judgement.MaxResult : r.Judgement.MinResult; + }, this); + } } protected override void UpdateHitStateTransforms(ArmedState state) diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs index 3490d50871..e5056d5167 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs @@ -265,7 +265,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables if (Tail.AllJudged) { if (Tail.IsHit) - ApplyResult(r => r.Type = r.Judgement.MaxResult); + ApplyResult(static r => r.Type = r.Judgement.MaxResult); else MissForcefully(); } diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteBody.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteBody.cs index 1b2efbafdf..317da0580c 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteBody.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteBody.cs @@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables { if (AllJudged) return; - ApplyResult(r => r.Type = hit ? r.Judgement.MaxResult : r.Judgement.MinResult); + ApplyResult(static (r, hit) => r.Type = hit ? r.Judgement.MaxResult : r.Judgement.MinResult, hit); } } } diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs index 8498fd36de..dea0817869 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs @@ -87,7 +87,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables /// /// Causes this to get missed, disregarding all conditions in implementations of . /// - public virtual void MissForcefully() => ApplyResult(r => r.Type = r.Judgement.MinResult); + public virtual void MissForcefully() => ApplyResult(static r => r.Type = r.Judgement.MinResult); } public abstract partial class DrawableManiaHitObject : DrawableManiaHitObject diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs index 680009bc4c..985007f905 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs @@ -89,7 +89,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables if (!userTriggered) { if (!HitObject.HitWindows.CanBeHit(timeOffset)) - ApplyResult(r => r.Type = r.Judgement.MinResult); + ApplyResult(static r => r.Type = r.Judgement.MinResult); return; } @@ -100,7 +100,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables result = GetCappedResult(result); - ApplyResult(r => r.Type = result); + ApplyResult(static (r, result) => r.Type = result, result); } /// diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs index 0d665cad0c..8284229d82 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -155,7 +155,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (!userTriggered) { if (!HitObject.HitWindows.CanBeHit(timeOffset)) - ApplyResult(r => r.Type = r.Judgement.MinResult); + ApplyResult(static r => r.Type = r.Judgement.MinResult); return; } @@ -169,19 +169,20 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (result == HitResult.None || clickAction != ClickAction.Hit) return; - ApplyResult(r => + ApplyResult(static (r, state) => { + var (hitCircle, hitResult) = state; var circleResult = (OsuHitCircleJudgementResult)r; // Todo: This should also consider misses, but they're a little more interesting to handle, since we don't necessarily know the position at the time of a miss. - if (result.IsHit()) + if (hitResult.IsHit()) { - var localMousePosition = ToLocalSpace(inputManager.CurrentState.Mouse.Position); - circleResult.CursorPositionAtHit = HitObject.StackedPosition + (localMousePosition - DrawSize / 2); + var localMousePosition = hitCircle.ToLocalSpace(hitCircle.inputManager.CurrentState.Mouse.Position); + circleResult.CursorPositionAtHit = hitCircle.HitObject.StackedPosition + (localMousePosition - hitCircle.DrawSize / 2); } - circleResult.Type = result; - }); + circleResult.Type = hitResult; + }, (this, result)); } /// diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs index 5b379a0d90..cc06d009c9 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs @@ -100,12 +100,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables /// /// Causes this to get hit, disregarding all conditions in implementations of . /// - public void HitForcefully() => ApplyResult(r => r.Type = r.Judgement.MaxResult); + public void HitForcefully() => ApplyResult(static r => r.Type = r.Judgement.MaxResult); /// /// Causes this to get missed, disregarding all conditions in implementations of . /// - public void MissForcefully() => ApplyResult(r => r.Type = r.Judgement.MinResult); + public void MissForcefully() => ApplyResult(static r => r.Type = r.Judgement.MinResult); private RectangleF parentScreenSpaceRectangle => ((DrawableOsuHitObject)ParentHitObject)?.parentScreenSpaceRectangle ?? Parent!.ScreenSpaceDrawQuad.AABBFloat; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index baec200107..3c298cc6af 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -292,10 +292,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (HitObject.ClassicSliderBehaviour) { // Classic behaviour means a slider is judged proportionally to the number of nested hitobjects hit. This is the classic osu!stable scoring. - ApplyResult(r => + ApplyResult(static (r, nestedHitObjects) => { - int totalTicks = NestedHitObjects.Count; - int hitTicks = NestedHitObjects.Count(h => h.IsHit); + int totalTicks = nestedHitObjects.Count; + int hitTicks = nestedHitObjects.Count(h => h.IsHit); if (hitTicks == totalTicks) r.Type = HitResult.Great; @@ -306,13 +306,16 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables double hitFraction = (double)hitTicks / totalTicks; r.Type = hitFraction >= 0.5 ? HitResult.Ok : HitResult.Meh; } - }); + }, NestedHitObjects); } else { // If only the nested hitobjects are judged, then the slider's own judgement is ignored for scoring purposes. // But the slider needs to still be judged with a reasonable hit/miss result for visual purposes (hit/miss transforms, etc). - ApplyResult(r => r.Type = NestedHitObjects.Any(h => h.Result.IsHit) ? r.Judgement.MaxResult : r.Judgement.MinResult); + ApplyResult(static (r, nestedHitObjects) => + { + r.Type = nestedHitObjects.Any(h => h.Result.IsHit) ? r.Judgement.MaxResult : r.Judgement.MinResult; + }, NestedHitObjects); } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index bf4b07eaab..d21d02c8ce 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -258,17 +258,17 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables foreach (var tick in ticks.Where(t => !t.Result.HasResult)) tick.TriggerResult(false); - ApplyResult(r => + ApplyResult(static (r, spinner) => { - if (Progress >= 1) + if (spinner.Progress >= 1) r.Type = HitResult.Great; - else if (Progress > .9) + else if (spinner.Progress > .9) r.Type = HitResult.Ok; - else if (Progress > .75) + else if (spinner.Progress > .75) r.Type = HitResult.Meh; - else if (Time.Current >= HitObject.EndTime) + else if (spinner.Time.Current >= spinner.HitObject.EndTime) r.Type = r.Judgement.MinResult; - }); + }, this); } protected override void Update() diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs index 5b55533edd..1c3ff29118 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs @@ -35,6 +35,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables /// Apply a judgement result. /// /// Whether this tick was reached. - internal void TriggerResult(bool hit) => ApplyResult(r => r.Type = hit ? r.Judgement.MaxResult : r.Judgement.MinResult); + internal void TriggerResult(bool hit) => ApplyResult(static (r, hit) => r.Type = hit ? r.Judgement.MaxResult : r.Judgement.MinResult, hit); } } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs index 2bf0c04adf..d3fe363857 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs @@ -143,7 +143,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables if (timeOffset < 0) return; - ApplyResult(r => r.Type = r.Judgement.MaxResult); + ApplyResult(static r => r.Type = r.Judgement.MaxResult); } protected override void UpdateHitStateTransforms(ArmedState state) @@ -192,7 +192,10 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables if (!ParentHitObject.Judged) return; - ApplyResult(r => r.Type = ParentHitObject.IsHit ? r.Judgement.MaxResult : r.Judgement.MinResult); + ApplyResult(static (r, parentHitObject) => + { + r.Type = parentHitObject.IsHit ? r.Judgement.MaxResult : r.Judgement.MinResult; + }, ParentHitObject); } public override bool OnPressed(KeyBindingPressEvent e) => false; diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs index c900165d34..de9a3a31c5 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs @@ -49,14 +49,14 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables if (!userTriggered) { if (timeOffset > HitObject.HitWindow) - ApplyResult(r => r.Type = r.Judgement.MinResult); + ApplyResult(static r => r.Type = r.Judgement.MinResult); return; } if (Math.Abs(timeOffset) > HitObject.HitWindow) return; - ApplyResult(r => r.Type = r.Judgement.MaxResult); + ApplyResult(static r => r.Type = r.Judgement.MaxResult); } public override void OnKilled() @@ -64,7 +64,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables base.OnKilled(); if (Time.Current > HitObject.GetEndTime() && !Judged) - ApplyResult(r => r.Type = r.Judgement.MinResult); + ApplyResult(static r => r.Type = r.Judgement.MinResult); } protected override void UpdateHitStateTransforms(ArmedState state) @@ -105,7 +105,10 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables if (!ParentHitObject.Judged) return; - ApplyResult(r => r.Type = ParentHitObject.IsHit ? r.Judgement.MaxResult : r.Judgement.MinResult); + ApplyResult(static (r, parentHitObject) => + { + r.Type = parentHitObject.IsHit ? r.Judgement.MaxResult : r.Judgement.MinResult; + }, ParentHitObject); } public override bool OnPressed(KeyBindingPressEvent e) => false; diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableFlyingHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableFlyingHit.cs index a039ce3407..1332b9e950 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableFlyingHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableFlyingHit.cs @@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables protected override void LoadComplete() { base.LoadComplete(); - ApplyResult(r => r.Type = r.Judgement.MaxResult); + ApplyResult(static r => r.Type = r.Judgement.MaxResult); } protected override void LoadSamples() diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs index 1ef426854e..c3bd76bf81 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs @@ -99,7 +99,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables if (!userTriggered) { if (!HitObject.HitWindows.CanBeHit(timeOffset)) - ApplyResult(r => r.Type = r.Judgement.MinResult); + ApplyResult(static r => r.Type = r.Judgement.MinResult); return; } @@ -108,9 +108,9 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables return; if (!validActionPressed) - ApplyResult(r => r.Type = r.Judgement.MinResult); + ApplyResult(static r => r.Type = r.Judgement.MinResult); else - ApplyResult(r => r.Type = result); + ApplyResult(static (r, result) => r.Type = result, result); } public override bool OnPressed(KeyBindingPressEvent e) @@ -209,19 +209,19 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables if (!ParentHitObject.Result.IsHit) { - ApplyResult(r => r.Type = r.Judgement.MinResult); + ApplyResult(static r => r.Type = r.Judgement.MinResult); return; } if (!userTriggered) { if (timeOffset - ParentHitObject.Result.TimeOffset > SECOND_HIT_WINDOW) - ApplyResult(r => r.Type = r.Judgement.MinResult); + ApplyResult(static r => r.Type = r.Judgement.MinResult); return; } if (Math.Abs(timeOffset - ParentHitObject.Result.TimeOffset) <= SECOND_HIT_WINDOW) - ApplyResult(r => r.Type = r.Judgement.MaxResult); + ApplyResult(static r => r.Type = r.Judgement.MaxResult); } public override bool OnPressed(KeyBindingPressEvent e) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableStrongNestedHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableStrongNestedHit.cs index 724d59edcd..4080c14066 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableStrongNestedHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableStrongNestedHit.cs @@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables // it can happen that the hit window of the nested strong hit extends past the lifetime of the parent object. // this is a safety to prevent such cases from causing the nested hit to never be judged and as such prevent gameplay from completing. if (!Judged && Time.Current > ParentHitObject?.HitObject.GetEndTime()) - ApplyResult(r => r.Type = r.Judgement.MinResult); + ApplyResult(static r => r.Type = r.Judgement.MinResult); } } } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs index e4a083f218..d48b78283b 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs @@ -206,7 +206,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables expandingRing.ScaleTo(1f + Math.Min(target_ring_scale - 1f, (target_ring_scale - 1f) * completion * 1.3f), 260, Easing.OutQuint); if (numHits == HitObject.RequiredHits) - ApplyResult(r => r.Type = r.Judgement.MaxResult); + ApplyResult(static r => r.Type = r.Judgement.MaxResult); } else { @@ -227,7 +227,11 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables tick.TriggerResult(false); } - ApplyResult(r => r.Type = numHits == HitObject.RequiredHits ? r.Judgement.MaxResult : r.Judgement.MinResult); + ApplyResult(static (r, state) => + { + var (numHits, hitObject) = state; + r.Type = numHits == hitObject.RequiredHits ? r.Judgement.MaxResult : r.Judgement.MinResult; + }, (numHits, HitObject)); } } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs index 3a5c006962..ad1d09bc7b 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs @@ -30,7 +30,10 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables public void TriggerResult(bool hit) { HitObject.StartTime = Time.Current; - ApplyResult(r => r.Type = hit ? r.Judgement.MaxResult : r.Judgement.MinResult); + ApplyResult(static (r, hit) => + { + r.Type = hit ? r.Judgement.MaxResult : r.Judgement.MinResult; + }, hit); } protected override void CheckForResult(bool userTriggered, double timeOffset) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index bce28361cb..9acd7b3c0f 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -687,12 +687,14 @@ namespace osu.Game.Rulesets.Objects.Drawables /// the of the . /// /// The callback that applies changes to the . - protected void ApplyResult(Action application) + /// The state passed to the callback. + /// The type of the state information that is passed to the callback method. + protected void ApplyResult(Action application, TState state) { if (Result.HasResult) throw new InvalidOperationException("Cannot apply result on a hitobject that already has a result."); - application?.Invoke(Result); + application?.Invoke(Result, state); if (!Result.HasResult) throw new InvalidOperationException($"{GetType().ReadableName()} applied a {nameof(JudgementResult)} but did not update {nameof(JudgementResult.Type)}."); @@ -714,6 +716,13 @@ namespace osu.Game.Rulesets.Objects.Drawables OnNewResult?.Invoke(this, Result); } + /// + /// Applies the of this , notifying responders such as + /// the of the . + /// + /// The callback that applies changes to the . + protected void ApplyResult(Action application) => ApplyResult((r, _) => application?.Invoke(r), null); + /// /// Processes this , checking if a scoring result has occurred. /// From 93bd3ce5ae73eb6f00649e09ace1acd2224ecc95 Mon Sep 17 00:00:00 2001 From: Chandler Stowell Date: Thu, 25 Jan 2024 11:25:41 -0500 Subject: [PATCH 02/26] update `DrawableHitCircle.ApplyResult` to pass `this` to its callback --- .../DrawableEmptyFreeformHitObject.cs | 2 +- .../Drawables/DrawablePippidonHitObject.cs | 6 ++--- .../DrawableEmptyScrollingHitObject.cs | 2 +- .../Drawables/DrawablePippidonHitObject.cs | 5 ++-- .../Drawables/DrawableCatchHitObject.cs | 7 +++--- .../Objects/Drawables/DrawableHoldNote.cs | 2 +- .../Objects/Drawables/DrawableHoldNoteBody.cs | 9 ++++++- .../Drawables/DrawableManiaHitObject.cs | 2 +- .../Objects/Drawables/DrawableNote.cs | 16 +++++++++---- .../TestSceneHitCircle.cs | 2 +- .../TestSceneHitCircleLateFade.cs | 2 +- .../Objects/Drawables/DrawableHitCircle.cs | 19 ++++++++------- .../Objects/Drawables/DrawableOsuHitObject.cs | 4 ++-- .../Objects/Drawables/DrawableSlider.cs | 14 +++++------ .../Objects/Drawables/DrawableSpinner.cs | 5 ++-- .../Objects/Drawables/DrawableSpinnerTick.cs | 12 +++++++++- .../Objects/Drawables/DrawableDrumRoll.cs | 8 +++---- .../Objects/Drawables/DrawableDrumRollTick.cs | 13 +++++----- .../Objects/Drawables/DrawableFlyingHit.cs | 2 +- .../Objects/Drawables/DrawableHit.cs | 24 ++++++++++++------- .../Drawables/DrawableStrongNestedHit.cs | 2 +- .../Objects/Drawables/DrawableSwell.cs | 16 +++++++------ .../Objects/Drawables/DrawableSwellTick.cs | 10 +++++--- .../Gameplay/TestSceneDrawableHitObject.cs | 2 +- .../Gameplay/TestScenePoolingRuleset.cs | 8 +++---- .../Objects/Drawables/DrawableHitObject.cs | 13 ++-------- 26 files changed, 120 insertions(+), 87 deletions(-) diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Objects/Drawables/DrawableEmptyFreeformHitObject.cs b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Objects/Drawables/DrawableEmptyFreeformHitObject.cs index e8f511bc4b..3ad8f06fb4 100644 --- a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Objects/Drawables/DrawableEmptyFreeformHitObject.cs +++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Objects/Drawables/DrawableEmptyFreeformHitObject.cs @@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.EmptyFreeform.Objects.Drawables { if (timeOffset >= 0) // todo: implement judgement logic - ApplyResult(static r => r.Type = HitResult.Perfect); + ApplyResult(static (r, hitObject) => r.Type = HitResult.Perfect); } protected override void UpdateHitStateTransforms(ArmedState state) diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Objects/Drawables/DrawablePippidonHitObject.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Objects/Drawables/DrawablePippidonHitObject.cs index a8bb57ba18..925f2d04bf 100644 --- a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Objects/Drawables/DrawablePippidonHitObject.cs +++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Objects/Drawables/DrawablePippidonHitObject.cs @@ -50,10 +50,10 @@ namespace osu.Game.Rulesets.Pippidon.Objects.Drawables { if (timeOffset >= 0) { - ApplyResult(static (r, isHovered) => + ApplyResult(static (r, hitObject) => { - r.Type = isHovered ? HitResult.Perfect : HitResult.Miss; - }, IsHovered); + r.Type = hitObject.IsHovered ? HitResult.Perfect : HitResult.Miss; + }); } } diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Objects/Drawables/DrawableEmptyScrollingHitObject.cs b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Objects/Drawables/DrawableEmptyScrollingHitObject.cs index 070a802aea..408bbea717 100644 --- a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Objects/Drawables/DrawableEmptyScrollingHitObject.cs +++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Objects/Drawables/DrawableEmptyScrollingHitObject.cs @@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.EmptyScrolling.Objects.Drawables { if (timeOffset >= 0) // todo: implement judgement logic - ApplyResult(static r => r.Type = HitResult.Perfect); + ApplyResult(static (r, hitObject) => r.Type = HitResult.Perfect); } protected override void UpdateHitStateTransforms(ArmedState state) diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Objects/Drawables/DrawablePippidonHitObject.cs b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Objects/Drawables/DrawablePippidonHitObject.cs index 9983ec20b0..2c9eac7f65 100644 --- a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Objects/Drawables/DrawablePippidonHitObject.cs +++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Objects/Drawables/DrawablePippidonHitObject.cs @@ -50,10 +50,11 @@ namespace osu.Game.Rulesets.Pippidon.Objects.Drawables { if (timeOffset >= 0) { - ApplyResult(static (r, pippidonHitObject) => + ApplyResult(static (r, hitObject) => { + var pippidonHitObject = (DrawablePippidonHitObject)hitObject; r.Type = pippidonHitObject.currentLane.Value == pippidonHitObject.HitObject.Lane ? HitResult.Perfect : HitResult.Miss; - }, this); + }); } } diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs index 5a921f36f5..721c6aaa59 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs @@ -64,10 +64,11 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables if (timeOffset >= 0 && Result != null) { - ApplyResult(static (r, state) => + ApplyResult(static (r, hitObject) => { - r.Type = state.CheckPosition.Invoke(state.HitObject) ? r.Judgement.MaxResult : r.Judgement.MinResult; - }, this); + var catchHitObject = (DrawableCatchHitObject)hitObject; + r.Type = catchHitObject.CheckPosition!.Invoke(catchHitObject.HitObject) ? r.Judgement.MaxResult : r.Judgement.MinResult; + }); } } diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs index e5056d5167..6c70ab3526 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs @@ -265,7 +265,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables if (Tail.AllJudged) { if (Tail.IsHit) - ApplyResult(static r => r.Type = r.Judgement.MaxResult); + ApplyResult(static (r, _) => r.Type = r.Judgement.MaxResult); else MissForcefully(); } diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteBody.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteBody.cs index 317da0580c..731b1b6298 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteBody.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteBody.cs @@ -11,6 +11,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables public override bool DisplayResult => false; + private bool hit; + public DrawableHoldNoteBody() : this(null) { @@ -25,7 +27,12 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables { if (AllJudged) return; - ApplyResult(static (r, hit) => r.Type = hit ? r.Judgement.MaxResult : r.Judgement.MinResult, hit); + this.hit = hit; + ApplyResult(static (r, hitObject) => + { + var holdNoteBody = (DrawableHoldNoteBody)hitObject; + r.Type = holdNoteBody.hit ? r.Judgement.MaxResult : r.Judgement.MinResult; + }); } } } diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs index dea0817869..2d10fa27cd 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs @@ -87,7 +87,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables /// /// Causes this to get missed, disregarding all conditions in implementations of . /// - public virtual void MissForcefully() => ApplyResult(static r => r.Type = r.Judgement.MinResult); + public virtual void MissForcefully() => ApplyResult(static (r, _) => r.Type = r.Judgement.MinResult); } public abstract partial class DrawableManiaHitObject : DrawableManiaHitObject diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs index 985007f905..a70253798a 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs @@ -38,6 +38,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables private Drawable headPiece; + private HitResult hitResult; + public DrawableNote() : this(null) { @@ -89,18 +91,22 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables if (!userTriggered) { if (!HitObject.HitWindows.CanBeHit(timeOffset)) - ApplyResult(static r => r.Type = r.Judgement.MinResult); + ApplyResult(static (r, _) => r.Type = r.Judgement.MinResult); return; } - var result = HitObject.HitWindows.ResultFor(timeOffset); - if (result == HitResult.None) + hitResult = HitObject.HitWindows.ResultFor(timeOffset); + if (hitResult == HitResult.None) return; - result = GetCappedResult(result); + hitResult = GetCappedResult(hitResult); - ApplyResult(static (r, result) => r.Type = result, result); + ApplyResult(static (r, hitObject) => + { + var note = (DrawableNote)hitObject; + r.Type = note.hitResult; + }); } /// diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs index 30b0451a3b..8d4145f2c1 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs @@ -136,7 +136,7 @@ namespace osu.Game.Rulesets.Osu.Tests if (auto && !userTriggered && timeOffset > hitOffset && CheckHittable?.Invoke(this, Time.Current, HitResult.Great) == ClickAction.Hit) { // force success - ApplyResult(r => r.Type = HitResult.Great); + ApplyResult(static (r, _) => r.Type = HitResult.Great); } else base.CheckForResult(userTriggered, timeOffset); diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleLateFade.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleLateFade.cs index 7824f26251..2d1e9c1270 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleLateFade.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleLateFade.cs @@ -208,7 +208,7 @@ namespace osu.Game.Rulesets.Osu.Tests if (shouldHit && !userTriggered && timeOffset >= 0) { // force success - ApplyResult(r => r.Type = HitResult.Great); + ApplyResult(static (r, _) => r.Type = HitResult.Great); } else base.CheckForResult(userTriggered, timeOffset); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs index 8284229d82..ce5422b180 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -44,6 +44,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private Container scaleContainer; private InputManager inputManager; + private HitResult hitResult; public DrawableHitCircle() : this(null) @@ -155,34 +156,34 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (!userTriggered) { if (!HitObject.HitWindows.CanBeHit(timeOffset)) - ApplyResult(static r => r.Type = r.Judgement.MinResult); + ApplyResult(static (r, _) => r.Type = r.Judgement.MinResult); return; } - var result = ResultFor(timeOffset); - var clickAction = CheckHittable?.Invoke(this, Time.Current, result); + hitResult = ResultFor(timeOffset); + var clickAction = CheckHittable?.Invoke(this, Time.Current, hitResult); if (clickAction == ClickAction.Shake) Shake(); - if (result == HitResult.None || clickAction != ClickAction.Hit) + if (hitResult == HitResult.None || clickAction != ClickAction.Hit) return; - ApplyResult(static (r, state) => + ApplyResult(static (r, hitObject) => { - var (hitCircle, hitResult) = state; + var hitCircle = (DrawableHitCircle)hitObject; var circleResult = (OsuHitCircleJudgementResult)r; // Todo: This should also consider misses, but they're a little more interesting to handle, since we don't necessarily know the position at the time of a miss. - if (hitResult.IsHit()) + if (hitCircle.hitResult.IsHit()) { var localMousePosition = hitCircle.ToLocalSpace(hitCircle.inputManager.CurrentState.Mouse.Position); circleResult.CursorPositionAtHit = hitCircle.HitObject.StackedPosition + (localMousePosition - hitCircle.DrawSize / 2); } - circleResult.Type = hitResult; - }, (this, result)); + circleResult.Type = hitCircle.hitResult; + }); } /// diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs index cc06d009c9..6de60a9d51 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs @@ -100,12 +100,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables /// /// Causes this to get hit, disregarding all conditions in implementations of . /// - public void HitForcefully() => ApplyResult(static r => r.Type = r.Judgement.MaxResult); + public void HitForcefully() => ApplyResult(static (r, _) => r.Type = r.Judgement.MaxResult); /// /// Causes this to get missed, disregarding all conditions in implementations of . /// - public void MissForcefully() => ApplyResult(static r => r.Type = r.Judgement.MinResult); + public void MissForcefully() => ApplyResult(static (r, _) => r.Type = r.Judgement.MinResult); private RectangleF parentScreenSpaceRectangle => ((DrawableOsuHitObject)ParentHitObject)?.parentScreenSpaceRectangle ?? Parent!.ScreenSpaceDrawQuad.AABBFloat; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 3c298cc6af..c0ff258352 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -292,10 +292,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (HitObject.ClassicSliderBehaviour) { // Classic behaviour means a slider is judged proportionally to the number of nested hitobjects hit. This is the classic osu!stable scoring. - ApplyResult(static (r, nestedHitObjects) => + ApplyResult(static (r, hitObject) => { - int totalTicks = nestedHitObjects.Count; - int hitTicks = nestedHitObjects.Count(h => h.IsHit); + int totalTicks = hitObject.NestedHitObjects.Count; + int hitTicks = hitObject.NestedHitObjects.Count(h => h.IsHit); if (hitTicks == totalTicks) r.Type = HitResult.Great; @@ -306,16 +306,16 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables double hitFraction = (double)hitTicks / totalTicks; r.Type = hitFraction >= 0.5 ? HitResult.Ok : HitResult.Meh; } - }, NestedHitObjects); + }); } else { // If only the nested hitobjects are judged, then the slider's own judgement is ignored for scoring purposes. // But the slider needs to still be judged with a reasonable hit/miss result for visual purposes (hit/miss transforms, etc). - ApplyResult(static (r, nestedHitObjects) => + ApplyResult(static (r, hitObject) => { - r.Type = nestedHitObjects.Any(h => h.Result.IsHit) ? r.Judgement.MaxResult : r.Judgement.MinResult; - }, NestedHitObjects); + r.Type = hitObject.NestedHitObjects.Any(h => h.Result.IsHit) ? r.Judgement.MaxResult : r.Judgement.MinResult; + }); } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index d21d02c8ce..3679bc9775 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -258,8 +258,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables foreach (var tick in ticks.Where(t => !t.Result.HasResult)) tick.TriggerResult(false); - ApplyResult(static (r, spinner) => + ApplyResult(static (r, hitObject) => { + var spinner = (DrawableSpinner)hitObject; if (spinner.Progress >= 1) r.Type = HitResult.Great; else if (spinner.Progress > .9) @@ -268,7 +269,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables r.Type = HitResult.Meh; else if (spinner.Time.Current >= spinner.HitObject.EndTime) r.Type = r.Judgement.MinResult; - }, this); + }); } protected override void Update() diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs index 1c3ff29118..628f07a281 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs @@ -11,6 +11,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { public override bool DisplayResult => false; + private bool hit; + public DrawableSpinnerTick() : this(null) { @@ -35,6 +37,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables /// Apply a judgement result. /// /// Whether this tick was reached. - internal void TriggerResult(bool hit) => ApplyResult(static (r, hit) => r.Type = hit ? r.Judgement.MaxResult : r.Judgement.MinResult, hit); + internal void TriggerResult(bool hit) + { + this.hit = hit; + ApplyResult(static (r, hitObject) => + { + var spinnerTick = (DrawableSpinnerTick)hitObject; + r.Type = spinnerTick.hit ? r.Judgement.MaxResult : r.Judgement.MinResult; + }); + } } } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs index d3fe363857..2e40875af1 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs @@ -143,7 +143,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables if (timeOffset < 0) return; - ApplyResult(static r => r.Type = r.Judgement.MaxResult); + ApplyResult(static (r, _) => r.Type = r.Judgement.MaxResult); } protected override void UpdateHitStateTransforms(ArmedState state) @@ -192,10 +192,10 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables if (!ParentHitObject.Judged) return; - ApplyResult(static (r, parentHitObject) => + ApplyResult(static (r, hitObject) => { - r.Type = parentHitObject.IsHit ? r.Judgement.MaxResult : r.Judgement.MinResult; - }, ParentHitObject); + r.Type = hitObject.IsHit ? r.Judgement.MaxResult : r.Judgement.MinResult; + }); } public override bool OnPressed(KeyBindingPressEvent e) => false; diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs index de9a3a31c5..aa678d7043 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs @@ -49,14 +49,14 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables if (!userTriggered) { if (timeOffset > HitObject.HitWindow) - ApplyResult(static r => r.Type = r.Judgement.MinResult); + ApplyResult(static (r, _) => r.Type = r.Judgement.MinResult); return; } if (Math.Abs(timeOffset) > HitObject.HitWindow) return; - ApplyResult(static r => r.Type = r.Judgement.MaxResult); + ApplyResult(static (r, _) => r.Type = r.Judgement.MaxResult); } public override void OnKilled() @@ -64,7 +64,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables base.OnKilled(); if (Time.Current > HitObject.GetEndTime() && !Judged) - ApplyResult(static r => r.Type = r.Judgement.MinResult); + ApplyResult(static (r, _) => r.Type = r.Judgement.MinResult); } protected override void UpdateHitStateTransforms(ArmedState state) @@ -105,10 +105,11 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables if (!ParentHitObject.Judged) return; - ApplyResult(static (r, parentHitObject) => + ApplyResult(static (r, hitObject) => { - r.Type = parentHitObject.IsHit ? r.Judgement.MaxResult : r.Judgement.MinResult; - }, ParentHitObject); + var nestedHit = (StrongNestedHit)hitObject; + r.Type = nestedHit.ParentHitObject!.IsHit ? r.Judgement.MaxResult : r.Judgement.MinResult; + }); } public override bool OnPressed(KeyBindingPressEvent e) => false; diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableFlyingHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableFlyingHit.cs index 1332b9e950..4349dff9f9 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableFlyingHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableFlyingHit.cs @@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables protected override void LoadComplete() { base.LoadComplete(); - ApplyResult(static r => r.Type = r.Judgement.MaxResult); + ApplyResult(static (r, _) => r.Type = r.Judgement.MaxResult); } protected override void LoadSamples() diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs index c3bd76bf81..cf8e4050ee 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs @@ -37,6 +37,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables private double? lastPressHandleTime; + private HitResult hitResult; + private readonly Bindable type = new Bindable(); public DrawableHit() @@ -99,18 +101,24 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables if (!userTriggered) { if (!HitObject.HitWindows.CanBeHit(timeOffset)) - ApplyResult(static r => r.Type = r.Judgement.MinResult); + ApplyResult(static (r, _) => r.Type = r.Judgement.MinResult); return; } - var result = HitObject.HitWindows.ResultFor(timeOffset); - if (result == HitResult.None) + hitResult = HitObject.HitWindows.ResultFor(timeOffset); + if (hitResult == HitResult.None) return; if (!validActionPressed) - ApplyResult(static r => r.Type = r.Judgement.MinResult); + ApplyResult(static (r, _) => r.Type = r.Judgement.MinResult); else - ApplyResult(static (r, result) => r.Type = result, result); + { + ApplyResult(static (r, hitObject) => + { + var drawableHit = (DrawableHit)hitObject; + r.Type = drawableHit.hitResult; + }); + } } public override bool OnPressed(KeyBindingPressEvent e) @@ -209,19 +217,19 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables if (!ParentHitObject.Result.IsHit) { - ApplyResult(static r => r.Type = r.Judgement.MinResult); + ApplyResult(static (r, _) => r.Type = r.Judgement.MinResult); return; } if (!userTriggered) { if (timeOffset - ParentHitObject.Result.TimeOffset > SECOND_HIT_WINDOW) - ApplyResult(static r => r.Type = r.Judgement.MinResult); + ApplyResult(static (r, _) => r.Type = r.Judgement.MinResult); return; } if (Math.Abs(timeOffset - ParentHitObject.Result.TimeOffset) <= SECOND_HIT_WINDOW) - ApplyResult(static r => r.Type = r.Judgement.MaxResult); + ApplyResult(static (r, _) => r.Type = r.Judgement.MaxResult); } public override bool OnPressed(KeyBindingPressEvent e) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableStrongNestedHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableStrongNestedHit.cs index 4080c14066..8f99538448 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableStrongNestedHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableStrongNestedHit.cs @@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables // it can happen that the hit window of the nested strong hit extends past the lifetime of the parent object. // this is a safety to prevent such cases from causing the nested hit to never be judged and as such prevent gameplay from completing. if (!Judged && Time.Current > ParentHitObject?.HitObject.GetEndTime()) - ApplyResult(static r => r.Type = r.Judgement.MinResult); + ApplyResult(static (r, _) => r.Type = r.Judgement.MinResult); } } } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs index d48b78283b..0781ea5e2a 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs @@ -41,6 +41,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables private double? lastPressHandleTime; + private int numHits; + public override bool DisplayResult => false; public DrawableSwell() @@ -192,7 +194,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables nextTick?.TriggerResult(true); - int numHits = ticks.Count(r => r.IsHit); + numHits = ticks.Count(r => r.IsHit); float completion = (float)numHits / HitObject.RequiredHits; @@ -206,14 +208,14 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables expandingRing.ScaleTo(1f + Math.Min(target_ring_scale - 1f, (target_ring_scale - 1f) * completion * 1.3f), 260, Easing.OutQuint); if (numHits == HitObject.RequiredHits) - ApplyResult(static r => r.Type = r.Judgement.MaxResult); + ApplyResult(static (r, _) => r.Type = r.Judgement.MaxResult); } else { if (timeOffset < 0) return; - int numHits = 0; + numHits = 0; foreach (var tick in ticks) { @@ -227,11 +229,11 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables tick.TriggerResult(false); } - ApplyResult(static (r, state) => + ApplyResult(static (r, hitObject) => { - var (numHits, hitObject) = state; - r.Type = numHits == hitObject.RequiredHits ? r.Judgement.MaxResult : r.Judgement.MinResult; - }, (numHits, HitObject)); + var swell = (DrawableSwell)hitObject; + r.Type = swell.numHits == swell.HitObject.RequiredHits ? r.Judgement.MaxResult : r.Judgement.MinResult; + }); } } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs index ad1d09bc7b..557438e5e5 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs @@ -15,6 +15,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { public override bool DisplayResult => false; + private bool hit; + public DrawableSwellTick() : this(null) { @@ -29,11 +31,13 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables public void TriggerResult(bool hit) { + this.hit = hit; HitObject.StartTime = Time.Current; - ApplyResult(static (r, hit) => + ApplyResult(static (r, hitObject) => { - r.Type = hit ? r.Judgement.MaxResult : r.Judgement.MinResult; - }, hit); + var swellTick = (DrawableSwellTick)hitObject; + r.Type = swellTick.hit ? r.Judgement.MaxResult : r.Judgement.MinResult; + }); } protected override void CheckForResult(bool userTriggered, double timeOffset) diff --git a/osu.Game.Tests/Gameplay/TestSceneDrawableHitObject.cs b/osu.Game.Tests/Gameplay/TestSceneDrawableHitObject.cs index 10dbede2e0..bf1e52aab5 100644 --- a/osu.Game.Tests/Gameplay/TestSceneDrawableHitObject.cs +++ b/osu.Game.Tests/Gameplay/TestSceneDrawableHitObject.cs @@ -216,7 +216,7 @@ namespace osu.Game.Tests.Gameplay LifetimeStart = LIFETIME_ON_APPLY; } - public void MissForcefully() => ApplyResult(r => r.Type = HitResult.Miss); + public void MissForcefully() => ApplyResult(static (r, _) => r.Type = HitResult.Miss); protected override void UpdateHitStateTransforms(ArmedState state) { diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs index fea7456472..00bd58e303 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs @@ -431,7 +431,7 @@ namespace osu.Game.Tests.Visual.Gameplay protected override void CheckForResult(bool userTriggered, double timeOffset) { if (timeOffset > HitObject.Duration) - ApplyResult(r => r.Type = r.Judgement.MaxResult); + ApplyResult(static (r, _) => r.Type = r.Judgement.MaxResult); } protected override void UpdateHitStateTransforms(ArmedState state) @@ -468,7 +468,7 @@ namespace osu.Game.Tests.Visual.Gameplay public override void OnKilled() { base.OnKilled(); - ApplyResult(r => r.Type = r.Judgement.MinResult); + ApplyResult(static (r, _) => r.Type = r.Judgement.MinResult); } } @@ -547,7 +547,7 @@ namespace osu.Game.Tests.Visual.Gameplay { base.CheckForResult(userTriggered, timeOffset); if (timeOffset >= 0) - ApplyResult(r => r.Type = r.Judgement.MaxResult); + ApplyResult(static (r, _) => r.Type = r.Judgement.MaxResult); } } @@ -596,7 +596,7 @@ namespace osu.Game.Tests.Visual.Gameplay { base.CheckForResult(userTriggered, timeOffset); if (timeOffset >= 0) - ApplyResult(r => r.Type = r.Judgement.MaxResult); + ApplyResult(static (r, _) => r.Type = r.Judgement.MaxResult); } } diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 9acd7b3c0f..bffe174be1 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -687,14 +687,12 @@ namespace osu.Game.Rulesets.Objects.Drawables /// the of the . /// /// The callback that applies changes to the . - /// The state passed to the callback. - /// The type of the state information that is passed to the callback method. - protected void ApplyResult(Action application, TState state) + protected void ApplyResult(Action application) { if (Result.HasResult) throw new InvalidOperationException("Cannot apply result on a hitobject that already has a result."); - application?.Invoke(Result, state); + application?.Invoke(Result, this); if (!Result.HasResult) throw new InvalidOperationException($"{GetType().ReadableName()} applied a {nameof(JudgementResult)} but did not update {nameof(JudgementResult.Type)}."); @@ -716,13 +714,6 @@ namespace osu.Game.Rulesets.Objects.Drawables OnNewResult?.Invoke(this, Result); } - /// - /// Applies the of this , notifying responders such as - /// the of the . - /// - /// The callback that applies changes to the . - protected void ApplyResult(Action application) => ApplyResult((r, _) => application?.Invoke(r), null); - /// /// Processes this , checking if a scoring result has occurred. /// From 682dab5d8327e24b38914f0599d8822eddf05e93 Mon Sep 17 00:00:00 2001 From: Chandler Stowell Date: Thu, 25 Jan 2024 11:30:52 -0500 Subject: [PATCH 03/26] check if parent was hit in taiko's `DrawableDrumRoll.CheckForResult` --- osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs index 2e40875af1..f68198b967 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs @@ -194,7 +194,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables ApplyResult(static (r, hitObject) => { - r.Type = hitObject.IsHit ? r.Judgement.MaxResult : r.Judgement.MinResult; + var drumRoll = (DrawableDrumRoll)hitObject; + r.Type = drumRoll.ParentHitObject!.IsHit ? r.Judgement.MaxResult : r.Judgement.MinResult; }); } From 347e88f59772f0dc0045f46dc1c8e060ed8b51b9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 26 Jan 2024 16:21:48 +0900 Subject: [PATCH 04/26] Add note about using `static` callback --- osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index bffe174be1..e30ce13f08 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -686,7 +686,7 @@ namespace osu.Game.Rulesets.Objects.Drawables /// Applies the of this , notifying responders such as /// the of the . /// - /// The callback that applies changes to the . + /// The callback that applies changes to the . Using a `static` delegate is recommended to avoid allocation overhead. protected void ApplyResult(Action application) { if (Result.HasResult) From 6cfd2813ede27126f002ace78e3def0e5f7b03f5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 26 Jan 2024 16:52:03 +0900 Subject: [PATCH 05/26] Fix incorrect cast --- osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs index f68198b967..e15298f3ca 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs @@ -194,7 +194,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables ApplyResult(static (r, hitObject) => { - var drumRoll = (DrawableDrumRoll)hitObject; + var drumRoll = (StrongNestedHit)hitObject; r.Type = drumRoll.ParentHitObject!.IsHit ? r.Judgement.MaxResult : r.Judgement.MinResult; }); } From ee4fe1c0683a26bcb8559ac1918d52a88e6a1276 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 28 Jan 2024 23:11:42 +0300 Subject: [PATCH 06/26] Fix relax mod not handling objects close to a previous slider's follow area --- osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs index 40fadfb77e..3679425389 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs @@ -88,7 +88,7 @@ namespace osu.Game.Rulesets.Osu.Mods if (!slider.HeadCircle.IsHit) handleHitCircle(slider.HeadCircle); - requiresHold |= slider.SliderInputManager.IsMouseInFollowArea(true); + requiresHold |= slider.SliderInputManager.IsMouseInFollowArea(slider.Tracking.Value); break; case DrawableSpinner spinner: From 3aefc919675a855f6d13385d4d69ed326db2bc4f Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Wed, 31 Jan 2024 07:54:07 +0300 Subject: [PATCH 07/26] Make AliveDrawableMap public --- .../PooledDrawableWithLifetimeContainer.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Pooling/PooledDrawableWithLifetimeContainer.cs b/osu.Game/Rulesets/Objects/Pooling/PooledDrawableWithLifetimeContainer.cs index 1b0176cae5..1ebdf48ae8 100644 --- a/osu.Game/Rulesets/Objects/Pooling/PooledDrawableWithLifetimeContainer.cs +++ b/osu.Game/Rulesets/Objects/Pooling/PooledDrawableWithLifetimeContainer.cs @@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Objects.Pooling /// /// The enumeration order is undefined. /// - public IEnumerable<(TEntry Entry, TDrawable Drawable)> AliveEntries => aliveDrawableMap.Select(x => (x.Key, x.Value)); + public IEnumerable<(TEntry Entry, TDrawable Drawable)> AliveEntries => AliveDrawableMap.Select(x => (x.Key, x.Value)); /// /// Whether to remove an entry when clock goes backward and crossed its . @@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.Objects.Pooling /// internal double FutureLifetimeExtension { get; set; } - private readonly Dictionary aliveDrawableMap = new Dictionary(); + public readonly Dictionary AliveDrawableMap = new Dictionary(); private readonly HashSet allEntries = new HashSet(); private readonly LifetimeEntryManager lifetimeManager = new LifetimeEntryManager(); @@ -101,10 +101,10 @@ namespace osu.Game.Rulesets.Objects.Pooling private void entryBecameAlive(LifetimeEntry lifetimeEntry) { var entry = (TEntry)lifetimeEntry; - Debug.Assert(!aliveDrawableMap.ContainsKey(entry)); + Debug.Assert(!AliveDrawableMap.ContainsKey(entry)); TDrawable drawable = GetDrawable(entry); - aliveDrawableMap[entry] = drawable; + AliveDrawableMap[entry] = drawable; AddDrawable(entry, drawable); } @@ -119,10 +119,10 @@ namespace osu.Game.Rulesets.Objects.Pooling private void entryBecameDead(LifetimeEntry lifetimeEntry) { var entry = (TEntry)lifetimeEntry; - Debug.Assert(aliveDrawableMap.ContainsKey(entry)); + Debug.Assert(AliveDrawableMap.ContainsKey(entry)); - TDrawable drawable = aliveDrawableMap[entry]; - aliveDrawableMap.Remove(entry); + TDrawable drawable = AliveDrawableMap[entry]; + AliveDrawableMap.Remove(entry); RemoveDrawable(entry, drawable); } @@ -148,7 +148,7 @@ namespace osu.Game.Rulesets.Objects.Pooling foreach (var entry in Entries.ToArray()) Remove(entry); - Debug.Assert(aliveDrawableMap.Count == 0); + Debug.Assert(AliveDrawableMap.Count == 0); } protected override bool CheckChildrenLife() From 6b1de5446ad5dfd21a481ee8f484b92b42851be5 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Wed, 31 Jan 2024 07:54:28 +0300 Subject: [PATCH 08/26] Reduce allocaations in ScrollingHitObjectContainer --- osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs index 39ddb5c753..23ac4378e4 100644 --- a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs @@ -186,9 +186,9 @@ namespace osu.Game.Rulesets.UI.Scrolling // to prevent hit objects displayed in a wrong position for one frame. // Only AliveEntries need to be considered for layout (reduces overhead in the case of scroll speed changes). // We are not using AliveObjects directly to avoid selection/sorting overhead since we don't care about the order at which positions will be updated. - foreach (var entry in AliveEntries) + foreach (var entry in AliveDrawableMap) { - var obj = entry.Drawable; + var obj = entry.Value; updatePosition(obj, Time.Current); From 0642d740149b2daf8876d76b3b55c15709cd6144 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 31 Jan 2024 22:52:57 +0900 Subject: [PATCH 09/26] Expose as ReadOnlyDictionary --- .../PooledDrawableWithLifetimeContainer.cs | 19 +++++++++++-------- .../UI/GameplaySampleTriggerSource.cs | 2 +- osu.Game/Rulesets/UI/HitObjectContainer.cs | 2 +- .../Scrolling/ScrollingHitObjectContainer.cs | 2 +- 4 files changed, 14 insertions(+), 11 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Pooling/PooledDrawableWithLifetimeContainer.cs b/osu.Game/Rulesets/Objects/Pooling/PooledDrawableWithLifetimeContainer.cs index 1ebdf48ae8..aed608cf8f 100644 --- a/osu.Game/Rulesets/Objects/Pooling/PooledDrawableWithLifetimeContainer.cs +++ b/osu.Game/Rulesets/Objects/Pooling/PooledDrawableWithLifetimeContainer.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Diagnostics; using System.Linq; using osu.Framework.Graphics; @@ -35,7 +36,7 @@ namespace osu.Game.Rulesets.Objects.Pooling /// /// The enumeration order is undefined. /// - public IEnumerable<(TEntry Entry, TDrawable Drawable)> AliveEntries => AliveDrawableMap.Select(x => (x.Key, x.Value)); + public readonly ReadOnlyDictionary AliveEntries; /// /// Whether to remove an entry when clock goes backward and crossed its . @@ -53,7 +54,7 @@ namespace osu.Game.Rulesets.Objects.Pooling /// internal double FutureLifetimeExtension { get; set; } - public readonly Dictionary AliveDrawableMap = new Dictionary(); + private readonly Dictionary aliveDrawableMap = new Dictionary(); private readonly HashSet allEntries = new HashSet(); private readonly LifetimeEntryManager lifetimeManager = new LifetimeEntryManager(); @@ -63,6 +64,8 @@ namespace osu.Game.Rulesets.Objects.Pooling lifetimeManager.EntryBecameAlive += entryBecameAlive; lifetimeManager.EntryBecameDead += entryBecameDead; lifetimeManager.EntryCrossedBoundary += entryCrossedBoundary; + + AliveEntries = new ReadOnlyDictionary(aliveDrawableMap); } /// @@ -101,10 +104,10 @@ namespace osu.Game.Rulesets.Objects.Pooling private void entryBecameAlive(LifetimeEntry lifetimeEntry) { var entry = (TEntry)lifetimeEntry; - Debug.Assert(!AliveDrawableMap.ContainsKey(entry)); + Debug.Assert(!aliveDrawableMap.ContainsKey(entry)); TDrawable drawable = GetDrawable(entry); - AliveDrawableMap[entry] = drawable; + aliveDrawableMap[entry] = drawable; AddDrawable(entry, drawable); } @@ -119,10 +122,10 @@ namespace osu.Game.Rulesets.Objects.Pooling private void entryBecameDead(LifetimeEntry lifetimeEntry) { var entry = (TEntry)lifetimeEntry; - Debug.Assert(AliveDrawableMap.ContainsKey(entry)); + Debug.Assert(aliveDrawableMap.ContainsKey(entry)); - TDrawable drawable = AliveDrawableMap[entry]; - AliveDrawableMap.Remove(entry); + TDrawable drawable = aliveDrawableMap[entry]; + aliveDrawableMap.Remove(entry); RemoveDrawable(entry, drawable); } @@ -148,7 +151,7 @@ namespace osu.Game.Rulesets.Objects.Pooling foreach (var entry in Entries.ToArray()) Remove(entry); - Debug.Assert(AliveDrawableMap.Count == 0); + Debug.Assert(aliveDrawableMap.Count == 0); } protected override bool CheckChildrenLife() diff --git a/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs b/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs index b61e8d9674..177520f28f 100644 --- a/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs +++ b/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs @@ -105,7 +105,7 @@ namespace osu.Game.Rulesets.UI // If required, we can make this lookup more efficient by adding support to get next-future-entry in LifetimeEntryManager. var candidate = // Use alive entries first as an optimisation. - hitObjectContainer.AliveEntries.Select(tuple => tuple.Entry).Where(e => !isAlreadyHit(e)).MinBy(e => e.HitObject.StartTime) + hitObjectContainer.AliveEntries.Keys.Where(e => !isAlreadyHit(e)).MinBy(e => e.HitObject.StartTime) ?? hitObjectContainer.Entries.Where(e => !isAlreadyHit(e)).MinBy(e => e.HitObject.StartTime); // In the case there are no non-judged objects, the last hit object should be used instead. diff --git a/osu.Game/Rulesets/UI/HitObjectContainer.cs b/osu.Game/Rulesets/UI/HitObjectContainer.cs index 099be486b3..c2879e6d87 100644 --- a/osu.Game/Rulesets/UI/HitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/HitObjectContainer.cs @@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.UI { public IEnumerable Objects => InternalChildren.Cast().OrderBy(h => h.HitObject.StartTime); - public IEnumerable AliveObjects => AliveEntries.Select(pair => pair.Drawable).OrderBy(h => h.HitObject.StartTime); + public IEnumerable AliveObjects => AliveEntries.Values.OrderBy(h => h.HitObject.StartTime); /// /// Invoked when a is judged. diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs index 23ac4378e4..4e72291b9c 100644 --- a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs @@ -186,7 +186,7 @@ namespace osu.Game.Rulesets.UI.Scrolling // to prevent hit objects displayed in a wrong position for one frame. // Only AliveEntries need to be considered for layout (reduces overhead in the case of scroll speed changes). // We are not using AliveObjects directly to avoid selection/sorting overhead since we don't care about the order at which positions will be updated. - foreach (var entry in AliveDrawableMap) + foreach (var entry in AliveEntries) { var obj = entry.Value; From c114fd8f8963f28b06df951145486689ad0084db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 1 Feb 2024 22:28:49 +0100 Subject: [PATCH 10/26] Allow pp for Sudden Death and Perfect regardless of "restart on fail" setting Closes https://github.com/ppy/osu/issues/26844. --- osu.Game/Rulesets/Mods/ModPerfect.cs | 2 +- osu.Game/Rulesets/Mods/ModSuddenDeath.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModPerfect.cs b/osu.Game/Rulesets/Mods/ModPerfect.cs index f8f498ceb5..5bedf443da 100644 --- a/osu.Game/Rulesets/Mods/ModPerfect.cs +++ b/osu.Game/Rulesets/Mods/ModPerfect.cs @@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Mods public override ModType Type => ModType.DifficultyIncrease; public override double ScoreMultiplier => 1; public override LocalisableString Description => "SS or quit."; - public override bool Ranked => UsesDefaultConfiguration; + public override bool Ranked => true; public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(ModSuddenDeath), typeof(ModAccuracyChallenge) }).ToArray(); diff --git a/osu.Game/Rulesets/Mods/ModSuddenDeath.cs b/osu.Game/Rulesets/Mods/ModSuddenDeath.cs index 62579a168c..d07ff6ce87 100644 --- a/osu.Game/Rulesets/Mods/ModSuddenDeath.cs +++ b/osu.Game/Rulesets/Mods/ModSuddenDeath.cs @@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Mods public override ModType Type => ModType.DifficultyIncrease; public override LocalisableString Description => "Miss and fail."; public override double ScoreMultiplier => 1; - public override bool Ranked => UsesDefaultConfiguration; + public override bool Ranked => true; public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModPerfect)).ToArray(); From 96f66aaa2e4eaa44c167e32af51fde84ad1e6a27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 1 Feb 2024 22:30:26 +0100 Subject: [PATCH 11/26] Allow pp for Accuracy Challenge Addresses https://github.com/ppy/osu/discussions/26919. --- osu.Game/Rulesets/Mods/ModAccuracyChallenge.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Rulesets/Mods/ModAccuracyChallenge.cs b/osu.Game/Rulesets/Mods/ModAccuracyChallenge.cs index 0072c21053..9570cddb0a 100644 --- a/osu.Game/Rulesets/Mods/ModAccuracyChallenge.cs +++ b/osu.Game/Rulesets/Mods/ModAccuracyChallenge.cs @@ -31,6 +31,8 @@ namespace osu.Game.Rulesets.Mods public override bool RequiresConfiguration => false; + public override bool Ranked => true; + public override string SettingDescription => base.SettingDescription.Replace(MinimumAccuracy.ToString(), MinimumAccuracy.Value.ToString("##%", NumberFormatInfo.InvariantInfo)); [SettingSource("Minimum accuracy", "Trigger a failure if your accuracy goes below this value.", SettingControlType = typeof(SettingsPercentageSlider))] From ea76f7a5d88a3060369db0529330c3c28ea35752 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 1 Feb 2024 22:33:49 +0100 Subject: [PATCH 12/26] Allow pp for Double/Half Time's "adjust pitch" setting --- osu.Game/Rulesets/Mods/ModDoubleTime.cs | 2 +- osu.Game/Rulesets/Mods/ModHalfTime.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModDoubleTime.cs b/osu.Game/Rulesets/Mods/ModDoubleTime.cs index 8e430da368..fd5120a767 100644 --- a/osu.Game/Rulesets/Mods/ModDoubleTime.cs +++ b/osu.Game/Rulesets/Mods/ModDoubleTime.cs @@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Mods public override IconUsage? Icon => OsuIcon.ModDoubleTime; public override ModType Type => ModType.DifficultyIncrease; public override LocalisableString Description => "Zoooooooooom..."; - public override bool Ranked => UsesDefaultConfiguration; + public override bool Ranked => SpeedChange.IsDefault; [SettingSource("Speed increase", "The actual increase to apply", SettingControlType = typeof(MultiplierSettingsSlider))] public override BindableNumber SpeedChange { get; } = new BindableDouble(1.5) diff --git a/osu.Game/Rulesets/Mods/ModHalfTime.cs b/osu.Game/Rulesets/Mods/ModHalfTime.cs index 59e40ee9cc..efdf0d6358 100644 --- a/osu.Game/Rulesets/Mods/ModHalfTime.cs +++ b/osu.Game/Rulesets/Mods/ModHalfTime.cs @@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Mods public override IconUsage? Icon => OsuIcon.ModHalftime; public override ModType Type => ModType.DifficultyReduction; public override LocalisableString Description => "Less zoom..."; - public override bool Ranked => UsesDefaultConfiguration; + public override bool Ranked => SpeedChange.IsDefault; [SettingSource("Speed decrease", "The actual decrease to apply", SettingControlType = typeof(MultiplierSettingsSlider))] public override BindableNumber SpeedChange { get; } = new BindableDouble(0.75) From 8dce158a13e05911273c5fb07f77555dc3d0b6ae Mon Sep 17 00:00:00 2001 From: Loreos7 Date: Fri, 2 Feb 2024 04:49:22 +0300 Subject: [PATCH 13/26] localise update-related strings --- osu.Game/Localisation/NotificationsStrings.cs | 22 +++++++++++++++++++ osu.Game/Updater/UpdateManager.cs | 9 ++++---- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/osu.Game/Localisation/NotificationsStrings.cs b/osu.Game/Localisation/NotificationsStrings.cs index fb3dab032d..f4965e4ebe 100644 --- a/osu.Game/Localisation/NotificationsStrings.cs +++ b/osu.Game/Localisation/NotificationsStrings.cs @@ -113,6 +113,28 @@ Please try changing your audio device to a working setting."); /// public static LocalisableString MismatchingBeatmapForReplay => new TranslatableString(getKey(@"mismatching_beatmap_for_replay"), @"Your local copy of the beatmap for this replay appears to be different than expected. You may need to update or re-download it."); + /// + /// "You are now running osu! {version}. + /// Click to see what's new!" + /// + public static LocalisableString GameVersionAfterUpdate(string version) => new TranslatableString(getKey(@"game_version_after_update"), @"You are now running osu! {0}. +Click to see what's new!", version); + + /// + /// "Update ready to install. Click to restart!" + /// + public static LocalisableString UpdateReadyToInstall => new TranslatableString(getKey(@"update_ready_to_install"), @"Update ready to install. Click to restart!"); + + /// + /// "Downloading update..." + /// + public static LocalisableString DownloadingUpdate => new TranslatableString(getKey(@"downloading_update"), @"Downloading update..."); + + /// + /// "Installing update..." + /// + public static LocalisableString InstallingUpdate => new TranslatableString(getKey(@"installing_update"), @"Installing update..."); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Updater/UpdateManager.cs b/osu.Game/Updater/UpdateManager.cs index 190748137a..8f13e0f42a 100644 --- a/osu.Game/Updater/UpdateManager.cs +++ b/osu.Game/Updater/UpdateManager.cs @@ -8,6 +8,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Game.Configuration; using osu.Game.Graphics; +using osu.Game.Localisation; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; using osuTK; @@ -92,7 +93,7 @@ namespace osu.Game.Updater public UpdateCompleteNotification(string version) { this.version = version; - Text = $"You are now running osu! {version}.\nClick to see what's new!"; + Text = NotificationsStrings.GameVersionAfterUpdate(version); } [BackgroundDependencyLoader] @@ -114,7 +115,7 @@ namespace osu.Game.Updater { public UpdateApplicationCompleteNotification() { - Text = @"Update ready to install. Click to restart!"; + Text = NotificationsStrings.UpdateReadyToInstall; } } @@ -166,13 +167,13 @@ namespace osu.Game.Updater { State = ProgressNotificationState.Active; Progress = 0; - Text = @"Downloading update..."; + Text = NotificationsStrings.DownloadingUpdate; } public void StartInstall() { Progress = 0; - Text = @"Installing update..."; + Text = NotificationsStrings.InstallingUpdate; } public void FailDownload() From 4aa27482a9b95ce8049d2a6cac1b1ded0945bc8f Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sat, 3 Feb 2024 19:52:40 +0300 Subject: [PATCH 14/26] Use SlimReadOnlyDictionaryWrapper for AliveEntries --- .../Objects/Pooling/PooledDrawableWithLifetimeContainer.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Pooling/PooledDrawableWithLifetimeContainer.cs b/osu.Game/Rulesets/Objects/Pooling/PooledDrawableWithLifetimeContainer.cs index aed608cf8f..efc10f26e1 100644 --- a/osu.Game/Rulesets/Objects/Pooling/PooledDrawableWithLifetimeContainer.cs +++ b/osu.Game/Rulesets/Objects/Pooling/PooledDrawableWithLifetimeContainer.cs @@ -2,12 +2,13 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; -using System.Collections.ObjectModel; using System.Diagnostics; using System.Linq; +using osu.Framework.Extensions.ListExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Performance; +using osu.Framework.Lists; namespace osu.Game.Rulesets.Objects.Pooling { @@ -36,7 +37,7 @@ namespace osu.Game.Rulesets.Objects.Pooling /// /// The enumeration order is undefined. /// - public readonly ReadOnlyDictionary AliveEntries; + public readonly SlimReadOnlyDictionaryWrapper AliveEntries; /// /// Whether to remove an entry when clock goes backward and crossed its . @@ -65,7 +66,7 @@ namespace osu.Game.Rulesets.Objects.Pooling lifetimeManager.EntryBecameDead += entryBecameDead; lifetimeManager.EntryCrossedBoundary += entryCrossedBoundary; - AliveEntries = new ReadOnlyDictionary(aliveDrawableMap); + AliveEntries = aliveDrawableMap.AsSlimReadOnly(); } /// From e2e3c61c9c9e1a13434a6db6d919299ee459c78e Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sat, 3 Feb 2024 19:54:04 +0300 Subject: [PATCH 15/26] Use AliveEntries where we don't need startTime order --- osu.Game.Rulesets.Osu/Mods/OsuModDepth.cs | 4 +++- osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs | 4 +++- osu.Game.Rulesets.Osu/Mods/OsuModRepel.cs | 4 +++- osu.Game/Screens/Play/FailAnimationContainer.cs | 4 +++- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDepth.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDepth.cs index b70d607ca1..a9111eec1f 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModDepth.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModDepth.cs @@ -75,8 +75,10 @@ namespace osu.Game.Rulesets.Osu.Mods { double time = playfield.Time.Current; - foreach (var drawable in playfield.HitObjectContainer.AliveObjects) + foreach (var entry in playfield.HitObjectContainer.AliveEntries) { + var drawable = entry.Value; + switch (drawable) { case DrawableHitCircle circle: diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs b/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs index befee4af5a..b49fb931d1 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs @@ -49,8 +49,10 @@ namespace osu.Game.Rulesets.Osu.Mods { var cursorPos = playfield.Cursor.AsNonNull().ActiveCursor.DrawPosition; - foreach (var drawable in playfield.HitObjectContainer.AliveObjects) + foreach (var entry in playfield.HitObjectContainer.AliveEntries) { + var drawable = entry.Value; + switch (drawable) { case DrawableHitCircle circle: diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRepel.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRepel.cs index 91feb33931..ced98f0cd5 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRepel.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRepel.cs @@ -48,8 +48,10 @@ namespace osu.Game.Rulesets.Osu.Mods { var cursorPos = playfield.Cursor.AsNonNull().ActiveCursor.DrawPosition; - foreach (var drawable in playfield.HitObjectContainer.AliveObjects) + foreach (var entry in playfield.HitObjectContainer.AliveEntries) { + var drawable = entry.Value; + var destination = Vector2.Clamp(2 * drawable.Position - cursorPos, Vector2.Zero, OsuPlayfield.BASE_SIZE); if (drawable.HitObject is Slider thisSlider) diff --git a/osu.Game/Screens/Play/FailAnimationContainer.cs b/osu.Game/Screens/Play/FailAnimationContainer.cs index 821c67e3cb..ebb0d77726 100644 --- a/osu.Game/Screens/Play/FailAnimationContainer.cs +++ b/osu.Game/Screens/Play/FailAnimationContainer.cs @@ -198,8 +198,10 @@ namespace osu.Game.Screens.Play foreach (var nested in playfield.NestedPlayfields) applyToPlayfield(nested); - foreach (DrawableHitObject obj in playfield.HitObjectContainer.AliveObjects) + foreach (var entry in playfield.HitObjectContainer.AliveEntries) { + var obj = entry.Value; + if (appliedObjects.Contains(obj)) continue; From efe6bb25b14db26e7c0b79494aa3eaf038dd99b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 5 Feb 2024 13:21:01 +0100 Subject: [PATCH 16/26] Refactor result application around again to remove requirement for fields Co-authored-by: Dean Herbert --- .../Objects/Drawables/DrawableHoldNote.cs | 2 +- .../Objects/Drawables/DrawableHoldNoteBody.cs | 12 +++----- .../Drawables/DrawableManiaHitObject.cs | 2 +- .../Objects/Drawables/DrawableNote.cs | 3 +- .../TestSceneHitCircle.cs | 2 +- .../TestSceneHitCircleLateFade.cs | 2 +- .../Objects/Drawables/DrawableHitCircle.cs | 29 ++++++++++--------- .../Objects/Drawables/DrawableOsuHitObject.cs | 4 +-- .../Objects/Drawables/DrawableDrumRoll.cs | 2 +- .../Objects/Drawables/DrawableDrumRollTick.cs | 6 ++-- .../Objects/Drawables/DrawableFlyingHit.cs | 2 +- .../Objects/Drawables/DrawableHit.cs | 10 +++---- .../Drawables/DrawableStrongNestedHit.cs | 2 +- .../Objects/Drawables/DrawableSwell.cs | 2 +- .../Gameplay/TestSceneDrawableHitObject.cs | 2 +- .../Gameplay/TestScenePoolingRuleset.cs | 8 ++--- .../Rulesets/Judgements/JudgementResult.cs | 2 +- .../Objects/Drawables/DrawableHitObject.cs | 17 +++++++++-- 18 files changed, 59 insertions(+), 50 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs index 6c70ab3526..2b55e81788 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs @@ -265,7 +265,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables if (Tail.AllJudged) { if (Tail.IsHit) - ApplyResult(static (r, _) => r.Type = r.Judgement.MaxResult); + ApplyMaxResult(); else MissForcefully(); } diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteBody.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteBody.cs index 731b1b6298..6259033235 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteBody.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteBody.cs @@ -11,8 +11,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables public override bool DisplayResult => false; - private bool hit; - public DrawableHoldNoteBody() : this(null) { @@ -27,12 +25,10 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables { if (AllJudged) return; - this.hit = hit; - ApplyResult(static (r, hitObject) => - { - var holdNoteBody = (DrawableHoldNoteBody)hitObject; - r.Type = holdNoteBody.hit ? r.Judgement.MaxResult : r.Judgement.MinResult; - }); + if (hit) + ApplyMaxResult(); + else + ApplyMinResult(); } } } diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs index 2d10fa27cd..e98622b8bf 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs @@ -87,7 +87,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables /// /// Causes this to get missed, disregarding all conditions in implementations of . /// - public virtual void MissForcefully() => ApplyResult(static (r, _) => r.Type = r.Judgement.MinResult); + public virtual void MissForcefully() => ApplyMinResult(); } public abstract partial class DrawableManiaHitObject : DrawableManiaHitObject diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs index a70253798a..2246552abe 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs @@ -91,12 +91,13 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables if (!userTriggered) { if (!HitObject.HitWindows.CanBeHit(timeOffset)) - ApplyResult(static (r, _) => r.Type = r.Judgement.MinResult); + ApplyMinResult(); return; } hitResult = HitObject.HitWindows.ResultFor(timeOffset); + if (hitResult == HitResult.None) return; diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs index 8d4145f2c1..abe950f9bb 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs @@ -136,7 +136,7 @@ namespace osu.Game.Rulesets.Osu.Tests if (auto && !userTriggered && timeOffset > hitOffset && CheckHittable?.Invoke(this, Time.Current, HitResult.Great) == ClickAction.Hit) { // force success - ApplyResult(static (r, _) => r.Type = HitResult.Great); + ApplyResult(HitResult.Great); } else base.CheckForResult(userTriggered, timeOffset); diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleLateFade.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleLateFade.cs index 2d1e9c1270..838b426cb4 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleLateFade.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleLateFade.cs @@ -208,7 +208,7 @@ namespace osu.Game.Rulesets.Osu.Tests if (shouldHit && !userTriggered && timeOffset >= 0) { // force success - ApplyResult(static (r, _) => r.Type = HitResult.Great); + ApplyResult(HitResult.Great); } else base.CheckForResult(userTriggered, timeOffset); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs index ce5422b180..a014ba2e77 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -44,7 +44,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private Container scaleContainer; private InputManager inputManager; - private HitResult hitResult; public DrawableHitCircle() : this(null) @@ -156,12 +155,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (!userTriggered) { if (!HitObject.HitWindows.CanBeHit(timeOffset)) - ApplyResult(static (r, _) => r.Type = r.Judgement.MinResult); + ApplyMinResult(); return; } - hitResult = ResultFor(timeOffset); + HitResult hitResult = ResultFor(timeOffset); var clickAction = CheckHittable?.Invoke(this, Time.Current, hitResult); if (clickAction == ClickAction.Shake) @@ -170,20 +169,22 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (hitResult == HitResult.None || clickAction != ClickAction.Hit) return; - ApplyResult(static (r, hitObject) => + Vector2? hitPosition = null; + + // Todo: This should also consider misses, but they're a little more interesting to handle, since we don't necessarily know the position at the time of a miss. + if (hitResult.IsHit()) + { + var localMousePosition = ToLocalSpace(inputManager.CurrentState.Mouse.Position); + hitPosition = HitObject.StackedPosition + (localMousePosition - DrawSize / 2); + } + + ApplyResult<(HitResult result, Vector2? position)>((r, state) => { - var hitCircle = (DrawableHitCircle)hitObject; var circleResult = (OsuHitCircleJudgementResult)r; - // Todo: This should also consider misses, but they're a little more interesting to handle, since we don't necessarily know the position at the time of a miss. - if (hitCircle.hitResult.IsHit()) - { - var localMousePosition = hitCircle.ToLocalSpace(hitCircle.inputManager.CurrentState.Mouse.Position); - circleResult.CursorPositionAtHit = hitCircle.HitObject.StackedPosition + (localMousePosition - hitCircle.DrawSize / 2); - } - - circleResult.Type = hitCircle.hitResult; - }); + circleResult.Type = state.result; + circleResult.CursorPositionAtHit = state.position; + }, (hitResult, hitPosition)); } /// diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs index 6de60a9d51..5271c03e08 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs @@ -100,12 +100,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables /// /// Causes this to get hit, disregarding all conditions in implementations of . /// - public void HitForcefully() => ApplyResult(static (r, _) => r.Type = r.Judgement.MaxResult); + public void HitForcefully() => ApplyMaxResult(); /// /// Causes this to get missed, disregarding all conditions in implementations of . /// - public void MissForcefully() => ApplyResult(static (r, _) => r.Type = r.Judgement.MinResult); + public void MissForcefully() => ApplyMinResult(); private RectangleF parentScreenSpaceRectangle => ((DrawableOsuHitObject)ParentHitObject)?.parentScreenSpaceRectangle ?? Parent!.ScreenSpaceDrawQuad.AABBFloat; diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs index e15298f3ca..1af4719b02 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs @@ -143,7 +143,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables if (timeOffset < 0) return; - ApplyResult(static (r, _) => r.Type = r.Judgement.MaxResult); + ApplyMaxResult(); } protected override void UpdateHitStateTransforms(ArmedState state) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs index aa678d7043..0333fd71a9 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs @@ -49,14 +49,14 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables if (!userTriggered) { if (timeOffset > HitObject.HitWindow) - ApplyResult(static (r, _) => r.Type = r.Judgement.MinResult); + ApplyMinResult(); return; } if (Math.Abs(timeOffset) > HitObject.HitWindow) return; - ApplyResult(static (r, _) => r.Type = r.Judgement.MaxResult); + ApplyMaxResult(); } public override void OnKilled() @@ -64,7 +64,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables base.OnKilled(); if (Time.Current > HitObject.GetEndTime() && !Judged) - ApplyResult(static (r, _) => r.Type = r.Judgement.MinResult); + ApplyMinResult(); } protected override void UpdateHitStateTransforms(ArmedState state) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableFlyingHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableFlyingHit.cs index 4349dff9f9..aad9214c5e 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableFlyingHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableFlyingHit.cs @@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables protected override void LoadComplete() { base.LoadComplete(); - ApplyResult(static (r, _) => r.Type = r.Judgement.MaxResult); + ApplyMaxResult(); } protected override void LoadSamples() diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs index cf8e4050ee..ca49ddb7e1 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs @@ -101,7 +101,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables if (!userTriggered) { if (!HitObject.HitWindows.CanBeHit(timeOffset)) - ApplyResult(static (r, _) => r.Type = r.Judgement.MinResult); + ApplyMinResult(); return; } @@ -110,7 +110,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables return; if (!validActionPressed) - ApplyResult(static (r, _) => r.Type = r.Judgement.MinResult); + ApplyMinResult(); else { ApplyResult(static (r, hitObject) => @@ -217,19 +217,19 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables if (!ParentHitObject.Result.IsHit) { - ApplyResult(static (r, _) => r.Type = r.Judgement.MinResult); + ApplyMinResult(); return; } if (!userTriggered) { if (timeOffset - ParentHitObject.Result.TimeOffset > SECOND_HIT_WINDOW) - ApplyResult(static (r, _) => r.Type = r.Judgement.MinResult); + ApplyMinResult(); return; } if (Math.Abs(timeOffset - ParentHitObject.Result.TimeOffset) <= SECOND_HIT_WINDOW) - ApplyResult(static (r, _) => r.Type = r.Judgement.MaxResult); + ApplyMaxResult(); } public override bool OnPressed(KeyBindingPressEvent e) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableStrongNestedHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableStrongNestedHit.cs index 8f99538448..11759927a9 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableStrongNestedHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableStrongNestedHit.cs @@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables // it can happen that the hit window of the nested strong hit extends past the lifetime of the parent object. // this is a safety to prevent such cases from causing the nested hit to never be judged and as such prevent gameplay from completing. if (!Judged && Time.Current > ParentHitObject?.HitObject.GetEndTime()) - ApplyResult(static (r, _) => r.Type = r.Judgement.MinResult); + ApplyMinResult(); } } } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs index 0781ea5e2a..6eb62cce22 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs @@ -208,7 +208,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables expandingRing.ScaleTo(1f + Math.Min(target_ring_scale - 1f, (target_ring_scale - 1f) * completion * 1.3f), 260, Easing.OutQuint); if (numHits == HitObject.RequiredHits) - ApplyResult(static (r, _) => r.Type = r.Judgement.MaxResult); + ApplyMaxResult(); } else { diff --git a/osu.Game.Tests/Gameplay/TestSceneDrawableHitObject.cs b/osu.Game.Tests/Gameplay/TestSceneDrawableHitObject.cs index bf1e52aab5..73177e36e1 100644 --- a/osu.Game.Tests/Gameplay/TestSceneDrawableHitObject.cs +++ b/osu.Game.Tests/Gameplay/TestSceneDrawableHitObject.cs @@ -216,7 +216,7 @@ namespace osu.Game.Tests.Gameplay LifetimeStart = LIFETIME_ON_APPLY; } - public void MissForcefully() => ApplyResult(static (r, _) => r.Type = HitResult.Miss); + public void MissForcefully() => ApplyResult(HitResult.Miss); protected override void UpdateHitStateTransforms(ArmedState state) { diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs index 00bd58e303..b567e8de8d 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs @@ -431,7 +431,7 @@ namespace osu.Game.Tests.Visual.Gameplay protected override void CheckForResult(bool userTriggered, double timeOffset) { if (timeOffset > HitObject.Duration) - ApplyResult(static (r, _) => r.Type = r.Judgement.MaxResult); + ApplyMaxResult(); } protected override void UpdateHitStateTransforms(ArmedState state) @@ -468,7 +468,7 @@ namespace osu.Game.Tests.Visual.Gameplay public override void OnKilled() { base.OnKilled(); - ApplyResult(static (r, _) => r.Type = r.Judgement.MinResult); + ApplyMinResult(); } } @@ -547,7 +547,7 @@ namespace osu.Game.Tests.Visual.Gameplay { base.CheckForResult(userTriggered, timeOffset); if (timeOffset >= 0) - ApplyResult(static (r, _) => r.Type = r.Judgement.MaxResult); + ApplyMaxResult(); } } @@ -596,7 +596,7 @@ namespace osu.Game.Tests.Visual.Gameplay { base.CheckForResult(userTriggered, timeOffset); if (timeOffset >= 0) - ApplyResult(static (r, _) => r.Type = r.Judgement.MaxResult); + ApplyMaxResult(); } } diff --git a/osu.Game/Rulesets/Judgements/JudgementResult.cs b/osu.Game/Rulesets/Judgements/JudgementResult.cs index b781a13929..4b98df50d7 100644 --- a/osu.Game/Rulesets/Judgements/JudgementResult.cs +++ b/osu.Game/Rulesets/Judgements/JudgementResult.cs @@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Judgements /// /// The time at which this occurred. - /// Populated when this is applied via . + /// Populated when this is applied via . /// /// /// This is used instead of to check whether this should be reverted. diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index e30ce13f08..07fab72814 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -682,17 +682,28 @@ namespace osu.Game.Rulesets.Objects.Drawables UpdateResult(false); } + protected void ApplyMaxResult() => ApplyResult((r, _) => r.Type = r.Judgement.MaxResult); + protected void ApplyMinResult() => ApplyResult((r, _) => r.Type = r.Judgement.MinResult); + + protected void ApplyResult(HitResult type) => ApplyResult(static (result, state) => result.Type = state, type); + + [Obsolete("Use overload with state, preferrably with static delegates to avoid allocation overhead.")] // Can be removed 2024-07-26 + protected void ApplyResult(Action application) => ApplyResult((r, _) => application(r), this); + + protected void ApplyResult(Action application) => ApplyResult(application, this); + /// /// Applies the of this , notifying responders such as /// the of the . /// /// The callback that applies changes to the . Using a `static` delegate is recommended to avoid allocation overhead. - protected void ApplyResult(Action application) + /// The state. + protected void ApplyResult(Action application, T state) { if (Result.HasResult) throw new InvalidOperationException("Cannot apply result on a hitobject that already has a result."); - application?.Invoke(Result, this); + application?.Invoke(Result, state); if (!Result.HasResult) throw new InvalidOperationException($"{GetType().ReadableName()} applied a {nameof(JudgementResult)} but did not update {nameof(JudgementResult.Type)}."); @@ -737,7 +748,7 @@ namespace osu.Game.Rulesets.Objects.Drawables /// Checks if a scoring result has occurred for this . /// /// - /// If a scoring result has occurred, this method must invoke to update the result and notify responders. + /// If a scoring result has occurred, this method must invoke to update the result and notify responders. /// /// Whether the user triggered this check. /// The offset from the end time of the at which this check occurred. From 2976f225e0627ffa8cb4efe2ad6060e02a6fa5ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 5 Feb 2024 13:22:58 +0100 Subject: [PATCH 17/26] Improve xmldoc of `state` param --- osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 07fab72814..c5f1878d1f 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -697,7 +697,10 @@ namespace osu.Game.Rulesets.Objects.Drawables /// the of the . /// /// The callback that applies changes to the . Using a `static` delegate is recommended to avoid allocation overhead. - /// The state. + /// + /// Use this parameter to pass any data that requires + /// to apply a result, so that it can remain a `static` delegate and thus not allocate. + /// protected void ApplyResult(Action application, T state) { if (Result.HasResult) From fb80d76b4a814a8b79ce441833515f450ab8e12b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 5 Feb 2024 13:35:41 +0100 Subject: [PATCH 18/26] Apply further changes to remove remaining weirdness --- .../Drawables/DrawableEmptyFreeformHitObject.cs | 2 +- .../Drawables/DrawablePippidonHitObject.cs | 9 ++++----- .../Drawables/DrawableEmptyScrollingHitObject.cs | 3 +-- .../Drawables/DrawablePippidonHitObject.cs | 10 ++++------ .../Objects/Drawables/DrawableCatchHitObject.cs | 9 ++++----- .../Objects/Drawables/DrawableNote.cs | 15 ++++----------- .../Objects/Drawables/DrawableHitCircle.cs | 10 +++++----- .../Objects/Drawables/DrawableSpinnerTick.cs | 12 ++++-------- .../Objects/Drawables/DrawableHit.cs | 14 +++----------- .../Objects/Drawables/DrawableSwell.cs | 15 ++++++--------- .../Objects/Drawables/DrawableSwellTick.cs | 13 +++++-------- 11 files changed, 41 insertions(+), 71 deletions(-) diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Objects/Drawables/DrawableEmptyFreeformHitObject.cs b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Objects/Drawables/DrawableEmptyFreeformHitObject.cs index 3ad8f06fb4..e53fe01157 100644 --- a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Objects/Drawables/DrawableEmptyFreeformHitObject.cs +++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Objects/Drawables/DrawableEmptyFreeformHitObject.cs @@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.EmptyFreeform.Objects.Drawables { if (timeOffset >= 0) // todo: implement judgement logic - ApplyResult(static (r, hitObject) => r.Type = HitResult.Perfect); + ApplyResult(HitResult.Perfect); } protected override void UpdateHitStateTransforms(ArmedState state) diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Objects/Drawables/DrawablePippidonHitObject.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Objects/Drawables/DrawablePippidonHitObject.cs index 925f2d04bf..b1be25727f 100644 --- a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Objects/Drawables/DrawablePippidonHitObject.cs +++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Objects/Drawables/DrawablePippidonHitObject.cs @@ -9,7 +9,6 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osu.Game.Audio; using osu.Game.Rulesets.Objects.Drawables; -using osu.Game.Rulesets.Scoring; using osuTK; using osuTK.Graphics; @@ -50,10 +49,10 @@ namespace osu.Game.Rulesets.Pippidon.Objects.Drawables { if (timeOffset >= 0) { - ApplyResult(static (r, hitObject) => - { - r.Type = hitObject.IsHovered ? HitResult.Perfect : HitResult.Miss; - }); + if (IsHovered) + ApplyMaxResult(); + else + ApplyMinResult(); } } diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Objects/Drawables/DrawableEmptyScrollingHitObject.cs b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Objects/Drawables/DrawableEmptyScrollingHitObject.cs index 408bbea717..adcbd36485 100644 --- a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Objects/Drawables/DrawableEmptyScrollingHitObject.cs +++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Objects/Drawables/DrawableEmptyScrollingHitObject.cs @@ -3,7 +3,6 @@ using osu.Framework.Graphics; using osu.Game.Rulesets.Objects.Drawables; -using osu.Game.Rulesets.Scoring; using osuTK; using osuTK.Graphics; @@ -24,7 +23,7 @@ namespace osu.Game.Rulesets.EmptyScrolling.Objects.Drawables { if (timeOffset >= 0) // todo: implement judgement logic - ApplyResult(static (r, hitObject) => r.Type = HitResult.Perfect); + ApplyMaxResult(); } protected override void UpdateHitStateTransforms(ArmedState state) diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Objects/Drawables/DrawablePippidonHitObject.cs b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Objects/Drawables/DrawablePippidonHitObject.cs index 2c9eac7f65..3ad636a601 100644 --- a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Objects/Drawables/DrawablePippidonHitObject.cs +++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Objects/Drawables/DrawablePippidonHitObject.cs @@ -10,7 +10,6 @@ using osu.Framework.Graphics.Textures; using osu.Game.Audio; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Pippidon.UI; -using osu.Game.Rulesets.Scoring; using osuTK; using osuTK.Graphics; @@ -50,11 +49,10 @@ namespace osu.Game.Rulesets.Pippidon.Objects.Drawables { if (timeOffset >= 0) { - ApplyResult(static (r, hitObject) => - { - var pippidonHitObject = (DrawablePippidonHitObject)hitObject; - r.Type = pippidonHitObject.currentLane.Value == pippidonHitObject.HitObject.Lane ? HitResult.Perfect : HitResult.Miss; - }); + if (currentLane.Value == HitObject.Lane) + ApplyMaxResult(); + else + ApplyMinResult(); } } diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs index 721c6aaa59..64705f9909 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs @@ -64,11 +64,10 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables if (timeOffset >= 0 && Result != null) { - ApplyResult(static (r, hitObject) => - { - var catchHitObject = (DrawableCatchHitObject)hitObject; - r.Type = catchHitObject.CheckPosition!.Invoke(catchHitObject.HitObject) ? r.Judgement.MaxResult : r.Judgement.MinResult; - }); + if (CheckPosition.Invoke(HitObject)) + ApplyMaxResult(); + else + ApplyMinResult(); } } diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs index 2246552abe..f6b92ab405 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs @@ -38,8 +38,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables private Drawable headPiece; - private HitResult hitResult; - public DrawableNote() : this(null) { @@ -96,18 +94,13 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables return; } - hitResult = HitObject.HitWindows.ResultFor(timeOffset); + var result = HitObject.HitWindows.ResultFor(timeOffset); - if (hitResult == HitResult.None) + if (result == HitResult.None) return; - hitResult = GetCappedResult(hitResult); - - ApplyResult(static (r, hitObject) => - { - var note = (DrawableNote)hitObject; - r.Type = note.hitResult; - }); + result = GetCappedResult(result); + ApplyResult(result); } /// diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs index a014ba2e77..b1c9bef6c4 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -160,19 +160,19 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables return; } - HitResult hitResult = ResultFor(timeOffset); - var clickAction = CheckHittable?.Invoke(this, Time.Current, hitResult); + var result = ResultFor(timeOffset); + var clickAction = CheckHittable?.Invoke(this, Time.Current, result); if (clickAction == ClickAction.Shake) Shake(); - if (hitResult == HitResult.None || clickAction != ClickAction.Hit) + if (result == HitResult.None || clickAction != ClickAction.Hit) return; Vector2? hitPosition = null; // Todo: This should also consider misses, but they're a little more interesting to handle, since we don't necessarily know the position at the time of a miss. - if (hitResult.IsHit()) + if (result.IsHit()) { var localMousePosition = ToLocalSpace(inputManager.CurrentState.Mouse.Position); hitPosition = HitObject.StackedPosition + (localMousePosition - DrawSize / 2); @@ -184,7 +184,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables circleResult.Type = state.result; circleResult.CursorPositionAtHit = state.position; - }, (hitResult, hitPosition)); + }, (result, hitPosition)); } /// diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs index 628f07a281..0a77faf924 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs @@ -11,8 +11,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { public override bool DisplayResult => false; - private bool hit; - public DrawableSpinnerTick() : this(null) { @@ -39,12 +37,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables /// Whether this tick was reached. internal void TriggerResult(bool hit) { - this.hit = hit; - ApplyResult(static (r, hitObject) => - { - var spinnerTick = (DrawableSpinnerTick)hitObject; - r.Type = spinnerTick.hit ? r.Judgement.MaxResult : r.Judgement.MinResult; - }); + if (hit) + ApplyMaxResult(); + else + ApplyMinResult(); } } } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs index ca49ddb7e1..4fb69056da 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs @@ -37,8 +37,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables private double? lastPressHandleTime; - private HitResult hitResult; - private readonly Bindable type = new Bindable(); public DrawableHit() @@ -105,20 +103,14 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables return; } - hitResult = HitObject.HitWindows.ResultFor(timeOffset); - if (hitResult == HitResult.None) + var result = HitObject.HitWindows.ResultFor(timeOffset); + if (result == HitResult.None) return; if (!validActionPressed) ApplyMinResult(); else - { - ApplyResult(static (r, hitObject) => - { - var drawableHit = (DrawableHit)hitObject; - r.Type = drawableHit.hitResult; - }); - } + ApplyResult(result); } public override bool OnPressed(KeyBindingPressEvent e) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs index 6eb62cce22..e1fc28fe16 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs @@ -41,8 +41,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables private double? lastPressHandleTime; - private int numHits; - public override bool DisplayResult => false; public DrawableSwell() @@ -194,7 +192,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables nextTick?.TriggerResult(true); - numHits = ticks.Count(r => r.IsHit); + int numHits = ticks.Count(r => r.IsHit); float completion = (float)numHits / HitObject.RequiredHits; @@ -215,7 +213,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables if (timeOffset < 0) return; - numHits = 0; + int numHits = 0; foreach (var tick in ticks) { @@ -229,11 +227,10 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables tick.TriggerResult(false); } - ApplyResult(static (r, hitObject) => - { - var swell = (DrawableSwell)hitObject; - r.Type = swell.numHits == swell.HitObject.RequiredHits ? r.Judgement.MaxResult : r.Judgement.MinResult; - }); + if (numHits == HitObject.RequiredHits) + ApplyMaxResult(); + else + ApplyMinResult(); } } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs index 557438e5e5..04dd01e066 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs @@ -15,8 +15,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { public override bool DisplayResult => false; - private bool hit; - public DrawableSwellTick() : this(null) { @@ -31,13 +29,12 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables public void TriggerResult(bool hit) { - this.hit = hit; HitObject.StartTime = Time.Current; - ApplyResult(static (r, hitObject) => - { - var swellTick = (DrawableSwellTick)hitObject; - r.Type = swellTick.hit ? r.Judgement.MaxResult : r.Judgement.MinResult; - }); + + if (hit) + ApplyMaxResult(); + else + ApplyMinResult(); } protected override void CheckForResult(bool userTriggered, double timeOffset) From 08fac9772082088526845236c1b6dc471cee0e74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 6 Feb 2024 12:27:45 +0100 Subject: [PATCH 19/26] Add resources covering failure case --- .../special-skin/taiko-bar-right@2x.png | Bin 0 -> 39297 bytes .../special-skin/taikohitcircle@2x.png | Bin 0 -> 8720 bytes .../special-skin/taikohitcircleoverlay@2x.png | Bin 0 -> 6478 bytes 3 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/taiko-bar-right@2x.png create mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/taikohitcircle@2x.png create mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/taikohitcircleoverlay@2x.png diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/taiko-bar-right@2x.png b/osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/taiko-bar-right@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..8ad7849e53161dc0e6320582aa54953389236a50 GIT binary patch literal 39297 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 literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/taikohitcircle@2x.png b/osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/taikohitcircle@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..73f856b16b67b84add2959868e4abca51bb7dea3 GIT binary patch literal 8720 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 literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/taikohitcircleoverlay@2x.png b/osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/taikohitcircleoverlay@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..83647266bceeb3a0e5311ecffa0ee7582ca6d6ec GIT binary patch literal 6478 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 A!2kdN literal 0 HcmV?d00001 From 27b97a3c4d720766e4c2468e2890fcaae69d40f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 6 Feb 2024 12:01:35 +0100 Subject: [PATCH 20/26] Convert selected legacy skin sprites to grayscale Matching stable. Closes https://github.com/ppy/osu/issues/9858. I would have liked to apply this in the taiko transformer itself, but the limited accessibility of texture uploads, and as such the raw image data, sort of prevents that... --- osu.Game/Skinning/LegacySkin.cs | 3 + osu.Game/Skinning/LegacyTextureLoaderStore.cs | 92 +++++++++++++++++++ 2 files changed, 95 insertions(+) create mode 100644 osu.Game/Skinning/LegacyTextureLoaderStore.cs diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index cfa5f972d2..816cfc0a2d 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -58,6 +58,9 @@ namespace osu.Game.Skinning { } + protected override IResourceStore CreateTextureLoaderStore(IStorageResourceProvider resources, IResourceStore storage) + => new LegacyTextureLoaderStore(base.CreateTextureLoaderStore(resources, storage)); + protected override void ParseConfigurationStream(Stream stream) { base.ParseConfigurationStream(stream); diff --git a/osu.Game/Skinning/LegacyTextureLoaderStore.cs b/osu.Game/Skinning/LegacyTextureLoaderStore.cs new file mode 100644 index 0000000000..8c466e6aac --- /dev/null +++ b/osu.Game/Skinning/LegacyTextureLoaderStore.cs @@ -0,0 +1,92 @@ +// 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.IO; +using System.Threading; +using System.Threading.Tasks; +using osu.Framework.Graphics.Textures; +using osu.Framework.IO.Stores; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Processing; + +namespace osu.Game.Skinning +{ + public class LegacyTextureLoaderStore : IResourceStore + { + private readonly IResourceStore? wrappedStore; + + public LegacyTextureLoaderStore(IResourceStore? wrappedStore) + { + this.wrappedStore = wrappedStore; + } + + public TextureUpload Get(string name) + { + var textureUpload = wrappedStore?.Get(name); + + if (textureUpload == null) + return null!; + + return shouldConvertToGrayscale(name) + ? convertToGrayscale(textureUpload) + : textureUpload; + } + + public Task GetAsync(string name, CancellationToken cancellationToken = new CancellationToken()) + { + var textureUpload = wrappedStore?.Get(name); + + if (textureUpload == null) + return null!; + + return shouldConvertToGrayscale(name) + ? Task.Run(() => convertToGrayscale(textureUpload), cancellationToken) + : Task.FromResult(textureUpload); + } + + // https://github.com/peppy/osu-stable-reference/blob/013c3010a9d495e3471a9c59518de17006f9ad89/osu!/Graphics/Textures/TextureManager.cs#L91-L96 + private static readonly string[] grayscale_sprites = + { + @"taiko-bar-right", + @"taikobigcircle", + @"taikohitcircle", + @"taikohitcircleoverlay" + }; + + private bool shouldConvertToGrayscale(string name) + { + foreach (string grayscaleSprite in grayscale_sprites) + { + // unfortunately at this level of lookup we can encounter `@2x` scale suffixes in the name, + // so straight equality cannot be used. + if (name.StartsWith(grayscaleSprite, StringComparison.OrdinalIgnoreCase)) + return true; + } + + return false; + } + + private TextureUpload convertToGrayscale(TextureUpload textureUpload) + { + var image = Image.LoadPixelData(textureUpload.Data.ToArray(), textureUpload.Width, textureUpload.Height); + + // stable uses `0.299 * r + 0.587 * g + 0.114 * b` + // (https://github.com/peppy/osu-stable-reference/blob/013c3010a9d495e3471a9c59518de17006f9ad89/osu!/Graphics/Textures/pTexture.cs#L138-L153) + // which matches mode BT.601 (https://en.wikipedia.org/wiki/Grayscale#Luma_coding_in_video_systems) + image.Mutate(i => i.Grayscale(GrayscaleMode.Bt601)); + + return new TextureUpload(image); + } + + public Stream? GetStream(string name) => wrappedStore?.GetStream(name); + + public IEnumerable GetAvailableResources() => wrappedStore?.GetAvailableResources() ?? Array.Empty(); + + public void Dispose() + { + wrappedStore?.Dispose(); + } + } +} From a84f53b1693892973535250163d13dfe8981ee9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 6 Feb 2024 13:03:05 +0100 Subject: [PATCH 21/26] Allow pp for Blinds The mod does impact pp, but it requires no extra difficulty attributes (https://github.com/ppy/osu/pull/26935#issuecomment-1925734171). --- osu.Game.Rulesets.Osu/Mods/OsuModBlinds.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModBlinds.cs b/osu.Game.Rulesets.Osu/Mods/OsuModBlinds.cs index 8b0adbe50f..bb0e984418 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModBlinds.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModBlinds.cs @@ -31,6 +31,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.12 : 1; public override Type[] IncompatibleMods => new[] { typeof(OsuModFlashlight) }; + public override bool Ranked => true; private DrawableOsuBlinds blinds = null!; From 8df593a8e6734f59e9c3dc81b903e8e6dc5f20e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 6 Feb 2024 13:03:38 +0100 Subject: [PATCH 22/26] Allow pp for No Scope Deemed as not affecting difficulty or pp in https://github.com/ppy/osu/pull/26935#issuecomment-1925644008, so can be treated pretty much as nomod. --- osu.Game/Rulesets/Mods/ModNoScope.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Rulesets/Mods/ModNoScope.cs b/osu.Game/Rulesets/Mods/ModNoScope.cs index 5b9dfc0430..dd1bd9a719 100644 --- a/osu.Game/Rulesets/Mods/ModNoScope.cs +++ b/osu.Game/Rulesets/Mods/ModNoScope.cs @@ -22,6 +22,7 @@ namespace osu.Game.Rulesets.Mods public override ModType Type => ModType.Fun; public override IconUsage? Icon => FontAwesome.Solid.EyeSlash; public override double ScoreMultiplier => 1; + public override bool Ranked => true; /// /// Slightly higher than the cutoff for . From 9e7912e66310fdfaec7bd9e2b27b1f3369eae81a Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 6 Feb 2024 21:56:26 +0900 Subject: [PATCH 23/26] Add test showing filled heatmap --- .../TestSceneAccuracyHeatmap.cs | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneAccuracyHeatmap.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneAccuracyHeatmap.cs index f99518997b..5524af2061 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneAccuracyHeatmap.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneAccuracyHeatmap.cs @@ -54,7 +54,7 @@ namespace osu.Game.Rulesets.Osu.Tests { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Size = new Vector2(130) + Size = new Vector2(300) } }; }); @@ -85,6 +85,30 @@ namespace osu.Game.Rulesets.Osu.Tests AddStep("return user input", () => InputManager.UseParentInput = true); } + [Test] + public void TestAllPoints() + { + AddStep("add points", () => + { + float minX = object1.DrawPosition.X - object1.DrawSize.X / 2; + float maxX = object1.DrawPosition.X + object1.DrawSize.X / 2; + + float minY = object1.DrawPosition.Y - object1.DrawSize.Y / 2; + float maxY = object1.DrawPosition.Y + object1.DrawSize.Y / 2; + + for (int i = 0; i < 10; i++) + { + for (float x = minX; x <= maxX; x += 0.5f) + { + for (float y = minY; y <= maxY; y += 0.5f) + { + accuracyHeatmap.AddPoint(object2.Position, object1.Position, new Vector2(x, y), RNG.NextSingle(10, 500)); + } + } + } + }); + } + protected override bool OnMouseDown(MouseDownEvent e) { accuracyHeatmap.AddPoint(object2.Position, object1.Position, background.ToLocalSpace(e.ScreenSpaceMouseDownPosition), 50); From 891346f7958b517d82022cf4ecaf2f980032c5fe Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 6 Feb 2024 21:56:52 +0900 Subject: [PATCH 24/26] Fix hit accuracy heatmap points being offset --- osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs index 83bab7dc01..f9d4a3b325 100644 --- a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs +++ b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs @@ -191,7 +191,7 @@ namespace osu.Game.Rulesets.Osu.Statistics for (int c = 0; c < points_per_dimension; c++) { - HitPointType pointType = Vector2.Distance(new Vector2(c, r), centre) <= innerRadius + HitPointType pointType = Vector2.Distance(new Vector2(c + 0.5f, r + 0.5f), centre) <= innerRadius ? HitPointType.Hit : HitPointType.Miss; From c21af1bf3d44f3c5b70c95c573748c2f41cd35f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 6 Feb 2024 14:53:01 +0100 Subject: [PATCH 25/26] Use more explicit match `taiko-bar-right-glow` is a prefix of `taiko-bar-right`... --- osu.Game/Skinning/LegacyTextureLoaderStore.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Skinning/LegacyTextureLoaderStore.cs b/osu.Game/Skinning/LegacyTextureLoaderStore.cs index 8c466e6aac..29206bbb85 100644 --- a/osu.Game/Skinning/LegacyTextureLoaderStore.cs +++ b/osu.Game/Skinning/LegacyTextureLoaderStore.cs @@ -61,8 +61,11 @@ namespace osu.Game.Skinning { // unfortunately at this level of lookup we can encounter `@2x` scale suffixes in the name, // so straight equality cannot be used. - if (name.StartsWith(grayscaleSprite, StringComparison.OrdinalIgnoreCase)) + if (name.Equals(grayscaleSprite, StringComparison.OrdinalIgnoreCase) + || name.Equals($@"{grayscaleSprite}@2x", StringComparison.OrdinalIgnoreCase)) + { return true; + } } return false; From ee05743921d8ab7a6b1041b4d47adadee9540b00 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 6 Feb 2024 22:58:11 +0900 Subject: [PATCH 26/26] Bump databased star rating versions --- osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs | 2 +- osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs index 6bb6879052..4190e74e51 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs @@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty private readonly bool isForCurrentRuleset; private readonly double originalOverallDifficulty; - public override int Version => 20220902; + public override int Version => 20230817; public ManiaDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap) : base(ruleset, beatmap) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index ab193caaa3..b84c2d25ee 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty { private const double difficulty_multiplier = 1.35; - public override int Version => 20220902; + public override int Version => 20221107; public TaikoDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap) : base(ruleset, beatmap)