From 78b30a076f8a0a8e8cb6caba99f34c8f5e0d3579 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 23 Dec 2019 15:40:14 +0900 Subject: [PATCH 01/54] Add mania player test scene --- osu.Game.Rulesets.Mania.Tests/TestScenePlayer.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 osu.Game.Rulesets.Mania.Tests/TestScenePlayer.cs diff --git a/osu.Game.Rulesets.Mania.Tests/TestScenePlayer.cs b/osu.Game.Rulesets.Mania.Tests/TestScenePlayer.cs new file mode 100644 index 0000000000..cd25d162d0 --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/TestScenePlayer.cs @@ -0,0 +1,15 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Mania.Tests +{ + public class TestScenePlayer : PlayerTestScene + { + public TestScenePlayer() + : base(new ManiaRuleset()) + { + } + } +} From 42853b5af645e4ead92c2d1ed7d9077c4d924189 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 23 Dec 2019 15:43:58 +0900 Subject: [PATCH 02/54] Separate head/tail notes from hold note class --- .../Objects/Drawables/DrawableHoldNote.cs | 129 +++--------------- .../Objects/Drawables/DrawableHoldNoteHead.cs | 37 +++++ .../Objects/Drawables/DrawableHoldNoteTail.cs | 74 ++++++++++ 3 files changed, 127 insertions(+), 113 deletions(-) create mode 100644 osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs create mode 100644 osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs index 87b9633c80..ca3b966d40 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Diagnostics; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces; @@ -24,8 +23,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables public DrawableNote Head => headContainer.Child; public DrawableNote Tail => tailContainer.Child; - private readonly Container headContainer; - private readonly Container tailContainer; + private readonly Container headContainer; + private readonly Container tailContainer; private readonly Container tickContainer; private readonly BodyPiece bodyPiece; @@ -33,12 +32,12 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables /// /// Time at which the user started holding this hold note. Null if the user is not holding this hold note. /// - private double? holdStartTime; + internal double? HoldStartTime; /// /// Whether the hold note has been released too early and shouldn't give full score for the release. /// - private bool hasBroken; + internal bool HasBroken; public DrawableHoldNote(HoldNote hitObject) : base(hitObject) @@ -49,8 +48,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables { bodyPiece = new BodyPiece { RelativeSizeAxes = Axes.X }, tickContainer = new Container { RelativeSizeAxes = Axes.Both }, - headContainer = new Container { RelativeSizeAxes = Axes.Both }, - tailContainer = new Container { RelativeSizeAxes = Axes.Both }, + headContainer = new Container { RelativeSizeAxes = Axes.Both }, + tailContainer = new Container { RelativeSizeAxes = Axes.Both }, }); AccentColour.BindValueChanged(colour => @@ -65,11 +64,11 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables switch (hitObject) { - case DrawableHeadNote head: + case DrawableHoldNoteHead head: headContainer.Child = head; break; - case DrawableTailNote tail: + case DrawableHoldNoteTail tail: tailContainer.Child = tail; break; @@ -92,7 +91,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables switch (hitObject) { case TailNote _: - return new DrawableTailNote(this) + return new DrawableHoldNoteTail(this) { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, @@ -100,7 +99,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables }; case Note _: - return new DrawableHeadNote(this) + return new DrawableHoldNoteHead(this) { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, @@ -110,7 +109,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables case HoldNoteTick tick: return new DrawableHoldNoteTick(tick) { - HoldStartTime = () => holdStartTime, + HoldStartTime = () => HoldStartTime, AccentColour = { BindTarget = AccentColour } }; } @@ -146,15 +145,15 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables base.UpdateStateTransforms(state); } - protected void BeginHold() + internal void BeginHold() { - holdStartTime = Time.Current; + HoldStartTime = Time.Current; bodyPiece.Hitting = true; } protected void EndHold() { - holdStartTime = null; + HoldStartTime = null; bodyPiece.Hitting = false; } @@ -177,7 +176,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables public bool OnReleased(ManiaAction action) { // Make sure that the user started holding the key during the hold note - if (!holdStartTime.HasValue) + if (!HoldStartTime.HasValue) return false; if (action != Action.Value) @@ -187,105 +186,9 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables // If the key has been released too early, the user should not receive full score for the release if (!Tail.IsHit) - hasBroken = true; + HasBroken = true; return true; } - - /// - /// The head note of a hold. - /// - private class DrawableHeadNote : DrawableNote - { - private readonly DrawableHoldNote holdNote; - - public DrawableHeadNote(DrawableHoldNote holdNote) - : base(holdNote.HitObject.Head) - { - this.holdNote = holdNote; - } - - public override bool OnPressed(ManiaAction action) - { - if (!base.OnPressed(action)) - return false; - - // If the key has been released too early, the user should not receive full score for the release - if (Result.Type == HitResult.Miss) - holdNote.hasBroken = true; - - // The head note also handles early hits before the body, but we want accurate early hits to count as the body being held - // The body doesn't handle these early early hits, so we have to explicitly set the holding state here - holdNote.BeginHold(); - - return true; - } - } - - /// - /// The tail note of a hold. - /// - private class DrawableTailNote : DrawableNote - { - /// - /// Lenience of release hit windows. This is to make cases where the hold note release - /// is timed alongside presses of other hit objects less awkward. - /// Todo: This shouldn't exist for non-LegacyBeatmapDecoder beatmaps - /// - private const double release_window_lenience = 1.5; - - private readonly DrawableHoldNote holdNote; - - public DrawableTailNote(DrawableHoldNote holdNote) - : base(holdNote.HitObject.Tail) - { - this.holdNote = holdNote; - } - - protected override void CheckForResult(bool userTriggered, double timeOffset) - { - Debug.Assert(HitObject.HitWindows != null); - - // Factor in the release lenience - timeOffset /= release_window_lenience; - - if (!userTriggered) - { - if (!HitObject.HitWindows.CanBeHit(timeOffset)) - ApplyResult(r => r.Type = HitResult.Miss); - - return; - } - - var result = HitObject.HitWindows.ResultFor(timeOffset); - if (result == HitResult.None) - return; - - ApplyResult(r => - { - if (holdNote.hasBroken && (result == HitResult.Perfect || result == HitResult.Perfect)) - result = HitResult.Good; - - r.Type = result; - }); - } - - public override bool OnPressed(ManiaAction action) => false; // Tail doesn't handle key down - - public override bool OnReleased(ManiaAction action) - { - // Make sure that the user started holding the key during the hold note - if (!holdNote.holdStartTime.HasValue) - return false; - - if (action != Action.Value) - return false; - - UpdateResult(true); - - // Handled by the hold note, which will set holding = false - return false; - } - } } } diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs new file mode 100644 index 0000000000..a1c9a09014 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs @@ -0,0 +1,37 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Rulesets.Mania.Objects.Drawables +{ + /// + /// The head of a . + /// + public class DrawableHoldNoteHead : DrawableNote + { + private readonly DrawableHoldNote holdNote; + + public DrawableHoldNoteHead(DrawableHoldNote holdNote) + : base(holdNote.HitObject.Head) + { + this.holdNote = holdNote; + } + + public override bool OnPressed(ManiaAction action) + { + if (!base.OnPressed(action)) + return false; + + // If the key has been released too early, the user should not receive full score for the release + if (Result.Type == HitResult.Miss) + holdNote.HasBroken = true; + + // The head note also handles early hits before the body, but we want accurate early hits to count as the body being held + // The body doesn't handle these early early hits, so we have to explicitly set the holding state here + holdNote.BeginHold(); + + return true; + } + } +} diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs new file mode 100644 index 0000000000..2b23fb1eef --- /dev/null +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs @@ -0,0 +1,74 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Diagnostics; +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Rulesets.Mania.Objects.Drawables +{ + /// + /// The tail of a . + /// + public class DrawableHoldNoteTail : DrawableNote + { + /// + /// Lenience of release hit windows. This is to make cases where the hold note release + /// is timed alongside presses of other hit objects less awkward. + /// Todo: This shouldn't exist for non-LegacyBeatmapDecoder beatmaps + /// + private const double release_window_lenience = 1.5; + + private readonly DrawableHoldNote holdNote; + + public DrawableHoldNoteTail(DrawableHoldNote holdNote) + : base(holdNote.HitObject.Tail) + { + this.holdNote = holdNote; + } + + protected override void CheckForResult(bool userTriggered, double timeOffset) + { + Debug.Assert(HitObject.HitWindows != null); + + // Factor in the release lenience + timeOffset /= release_window_lenience; + + if (!userTriggered) + { + if (!HitObject.HitWindows.CanBeHit(timeOffset)) + ApplyResult(r => r.Type = HitResult.Miss); + + return; + } + + var result = HitObject.HitWindows.ResultFor(timeOffset); + if (result == HitResult.None) + return; + + ApplyResult(r => + { + if (holdNote.HasBroken && (result == HitResult.Perfect || result == HitResult.Perfect)) + result = HitResult.Good; + + r.Type = result; + }); + } + + public override bool OnPressed(ManiaAction action) => false; // Tail doesn't handle key down + + public override bool OnReleased(ManiaAction action) + { + // Make sure that the user started holding the key during the hold note + if (!holdNote.HoldStartTime.HasValue) + return false; + + if (action != Action.Value) + return false; + + UpdateResult(true); + + // Handled by the hold note, which will set holding = false + return false; + } + } +} From 7ac6f68de8c4b7ce07d5aab461ff6cb9831558ca Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 23 Dec 2019 17:48:48 +0900 Subject: [PATCH 03/54] Rewrite hold note input handling --- .../HoldNoteNoteSelectionBlueprint.cs | 2 +- .../Objects/Drawables/DrawableHoldNote.cs | 58 +++++++++++-------- .../Objects/Drawables/DrawableHoldNoteHead.cs | 21 +------ .../Objects/Drawables/DrawableHoldNoteTail.cs | 19 ++---- 4 files changed, 41 insertions(+), 59 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteNoteSelectionBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteNoteSelectionBlueprint.cs index acce41db6f..4e73883de0 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteNoteSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteNoteSelectionBlueprint.cs @@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints // Todo: This shouldn't exist, mania should not reference the drawable hitobject directly. if (DrawableObject.IsLoaded) { - DrawableNote note = position == HoldNotePosition.Start ? DrawableObject.Head : DrawableObject.Tail; + DrawableNote note = position == HoldNotePosition.Start ? (DrawableNote)DrawableObject.Head : DrawableObject.Tail; Anchor = note.Anchor; Origin = note.Origin; diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs index ca3b966d40..c4a3e23cce 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs @@ -20,8 +20,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables { public override bool DisplayResult => false; - public DrawableNote Head => headContainer.Child; - public DrawableNote Tail => tailContainer.Child; + public DrawableHoldNoteHead Head => headContainer.Child; + public DrawableHoldNoteTail Tail => tailContainer.Child; private readonly Container headContainer; private readonly Container tailContainer; @@ -124,12 +124,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables bodyPiece.Anchor = bodyPiece.Origin = e.NewValue == ScrollingDirection.Up ? Anchor.TopLeft : Anchor.BottomLeft; } - protected override void CheckForResult(bool userTriggered, double timeOffset) - { - if (Tail.AllJudged) - ApplyResult(r => r.Type = HitResult.Perfect); - } - protected override void Update() { base.Update(); @@ -145,44 +139,52 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables base.UpdateStateTransforms(state); } - internal void BeginHold() + protected override void CheckForResult(bool userTriggered, double timeOffset) { - HoldStartTime = Time.Current; - bodyPiece.Hitting = true; - } + if (Tail.AllJudged) + ApplyResult(r => r.Type = HitResult.Perfect); - protected void EndHold() - { - HoldStartTime = null; - bodyPiece.Hitting = false; + if (Tail.Result.Type == HitResult.Miss) + HasBroken = true; } public bool OnPressed(ManiaAction action) { - // Make sure the action happened within the body of the hold note - if (Time.Current < HitObject.StartTime || Time.Current > HitObject.EndTime) + if (AllJudged) return false; if (action != Action.Value) return false; - // The user has pressed during the body of the hold note, after the head note and its hit windows have passed - // and within the limited range of the above if-statement. This state will be managed by the head note if the - // user has pressed during the hit windows of the head note. - BeginHold(); + beginHoldAt(Time.Current - Head.HitObject.StartTime); + Head.UpdateResult(); + return true; } + private void beginHoldAt(double timeOffset) + { + if (timeOffset < -Head.HitObject.HitWindows.WindowFor(HitResult.Miss)) + return; + + HoldStartTime = Time.Current; + bodyPiece.Hitting = true; + } + public bool OnReleased(ManiaAction action) { - // Make sure that the user started holding the key during the hold note - if (!HoldStartTime.HasValue) + if (AllJudged) return false; if (action != Action.Value) return false; - EndHold(); + // Make sure a hold was started + if (HoldStartTime == null) + return false; + + Tail.UpdateResult(); + endHold(); // If the key has been released too early, the user should not receive full score for the release if (!Tail.IsHit) @@ -190,5 +192,11 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables return true; } + + private void endHold() + { + HoldStartTime = null; + bodyPiece.Hitting = false; + } } } diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs index a1c9a09014..a5d03bf765 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Game.Rulesets.Scoring; - namespace osu.Game.Rulesets.Mania.Objects.Drawables { /// @@ -10,28 +8,15 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables /// public class DrawableHoldNoteHead : DrawableNote { - private readonly DrawableHoldNote holdNote; - public DrawableHoldNoteHead(DrawableHoldNote holdNote) : base(holdNote.HitObject.Head) { - this.holdNote = holdNote; } - public override bool OnPressed(ManiaAction action) - { - if (!base.OnPressed(action)) - return false; + public void UpdateResult() => base.UpdateResult(true); - // If the key has been released too early, the user should not receive full score for the release - if (Result.Type == HitResult.Miss) - holdNote.HasBroken = true; + public override bool OnPressed(ManiaAction action) => false; // Handled by the hold note - // The head note also handles early hits before the body, but we want accurate early hits to count as the body being held - // The body doesn't handle these early early hits, so we have to explicitly set the holding state here - holdNote.BeginHold(); - - return true; - } + public override bool OnReleased(ManiaAction action) => false; // Handled by the hold note } } diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs index 2b23fb1eef..a1f34a6db2 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs @@ -26,6 +26,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables this.holdNote = holdNote; } + public void UpdateResult() => base.UpdateResult(true); + protected override void CheckForResult(bool userTriggered, double timeOffset) { Debug.Assert(HitObject.HitWindows != null); @@ -54,21 +56,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables }); } - public override bool OnPressed(ManiaAction action) => false; // Tail doesn't handle key down + public override bool OnPressed(ManiaAction action) => false; // Handled by the hold note - public override bool OnReleased(ManiaAction action) - { - // Make sure that the user started holding the key during the hold note - if (!holdNote.HoldStartTime.HasValue) - return false; - - if (action != Action.Value) - return false; - - UpdateResult(true); - - // Handled by the hold note, which will set holding = false - return false; - } + public override bool OnReleased(ManiaAction action) => false; // Handled by the hold note } } From 7bb4a08f10a534246bd4f5ccce2c23e89044506e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 23 Dec 2019 18:35:40 +0900 Subject: [PATCH 04/54] Add failing tests --- .../TestSceneHoldNoteInput.cs | 314 ++++++++++++++++++ 1 file changed, 314 insertions(+) create mode 100644 osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs new file mode 100644 index 0000000000..8682604b13 --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs @@ -0,0 +1,314 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using NUnit.Framework; +using osu.Framework.Screens; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Replays; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Mania.Replays; +using osu.Game.Rulesets.Replays; +using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; +using osu.Game.Screens.Play; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Mania.Tests +{ + public class TestSceneHoldNoteInput : RateAdjustedBeatmapTestScene + { + private const double time_before_head = 250; + private const double time_head = 1500; + private const double time_during_hold_1 = 2500; + private const double time_tail = 4000; + private const double time_after_tail = 5250; + + private List judgementResults; + private bool allJudgedFired; + + /// + /// -----[ ]----- + /// o o + /// + [Test] + public void TestNoInput() + { + performTest(new List + { + new ManiaReplayFrame(time_before_head), + new ManiaReplayFrame(time_after_tail), + }); + + assertHeadJudgement(HitResult.Miss); + assertTickJudgement(HitResult.Miss); + assertTailJudgement(HitResult.Miss); + assertNoteJudgement(HitResult.Perfect); + } + + /// + /// -----[ ]----- + /// x o + /// + [Test] + public void TestPressTooEarlyAndReleaseAfterTail() + { + performTest(new List + { + new ManiaReplayFrame(time_before_head, ManiaAction.Key1), + new ManiaReplayFrame(time_after_tail, ManiaAction.Key1), + }); + + assertHeadJudgement(HitResult.Miss); + assertTickJudgement(HitResult.Miss); + assertTailJudgement(HitResult.Miss); + } + + /// + /// -----[ ]----- + /// x o + /// + [Test] + public void TestPressTooEarlyAndReleaseAtTail() + { + performTest(new List + { + new ManiaReplayFrame(time_before_head, ManiaAction.Key1), + new ManiaReplayFrame(time_tail), + }); + + assertHeadJudgement(HitResult.Miss); + assertTickJudgement(HitResult.Miss); + assertTailJudgement(HitResult.Miss); + } + + /// + /// -----[ ]----- + /// xo x o + /// + [Test] + public void TestPressTooEarlyThenPressAtStartAndReleaseAfterTail() + { + performTest(new List + { + new ManiaReplayFrame(time_before_head, ManiaAction.Key1), + new ManiaReplayFrame(time_before_head + 10), + new ManiaReplayFrame(time_head, ManiaAction.Key1), + new ManiaReplayFrame(time_after_tail), + }); + + assertHeadJudgement(HitResult.Perfect); + assertTickJudgement(HitResult.Perfect); + assertTailJudgement(HitResult.Miss); + } + + /// + /// -----[ ]----- + /// xo x o + /// + [Test] + public void TestPressTooEarlyThenPressAtStartAndReleaseAtTail() + { + performTest(new List + { + new ManiaReplayFrame(time_before_head, ManiaAction.Key1), + new ManiaReplayFrame(time_before_head + 10), + new ManiaReplayFrame(time_head, ManiaAction.Key1), + new ManiaReplayFrame(time_tail), + }); + + assertHeadJudgement(HitResult.Perfect); + assertTickJudgement(HitResult.Perfect); + assertTailJudgement(HitResult.Perfect); + } + + /// + /// -----[ ]----- + /// xo o + /// + [Test] + public void TestPressAtStartAndBreak() + { + performTest(new List + { + new ManiaReplayFrame(time_head, ManiaAction.Key1), + new ManiaReplayFrame(time_head + 10), + new ManiaReplayFrame(time_after_tail), + }); + + assertHeadJudgement(HitResult.Perfect); + assertTickJudgement(HitResult.Miss); + assertTailJudgement(HitResult.Miss); + } + + /// + /// -----[ ]----- + /// xo x o + /// + [Test] + public void TestPressAtStartThenBreakThenRepressAndReleaseAfterTail() + { + performTest(new List + { + new ManiaReplayFrame(time_head, ManiaAction.Key1), + new ManiaReplayFrame(time_head + 10), + new ManiaReplayFrame(time_during_hold_1, ManiaAction.Key1), + new ManiaReplayFrame(time_after_tail), + }); + + assertHeadJudgement(HitResult.Perfect); + assertTickJudgement(HitResult.Perfect); + assertTailJudgement(HitResult.Miss); + } + + /// + /// -----[ ]----- + /// xo x o o + /// + [Test] + public void TestPressAtStartThenBreakThenRepressAndReleaseAtTail() + { + performTest(new List + { + new ManiaReplayFrame(time_head, ManiaAction.Key1), + new ManiaReplayFrame(time_head + 10), + new ManiaReplayFrame(time_during_hold_1, ManiaAction.Key1), + new ManiaReplayFrame(time_tail), + }); + + assertHeadJudgement(HitResult.Perfect); + assertTickJudgement(HitResult.Perfect); + assertTailJudgement(HitResult.Meh); + } + + /// + /// -----[ ]----- + /// x o + /// + [Test] + public void TestPressDuringNoteAndReleaseAfterTail() + { + performTest(new List + { + new ManiaReplayFrame(time_during_hold_1, ManiaAction.Key1), + new ManiaReplayFrame(time_after_tail), + }); + + assertHeadJudgement(HitResult.Miss); + assertTickJudgement(HitResult.Perfect); + assertTailJudgement(HitResult.Miss); + } + + /// + /// -----[ ]----- + /// x o o + /// + [Test] + public void TestPressDuringNoteAndReleaseAtTail() + { + performTest(new List + { + new ManiaReplayFrame(time_during_hold_1, ManiaAction.Key1), + new ManiaReplayFrame(time_tail), + }); + + assertHeadJudgement(HitResult.Miss); + assertTickJudgement(HitResult.Perfect); + assertTailJudgement(HitResult.Meh); + } + + /// + /// -----[ ]----- + /// xo o + /// + [Test] + public void TestPressAndReleaseAtTail() + { + performTest(new List + { + new ManiaReplayFrame(time_tail, ManiaAction.Key1), + new ManiaReplayFrame(time_tail + 10), + }); + + assertHeadJudgement(HitResult.Miss); + assertTickJudgement(HitResult.Miss); + assertTailJudgement(HitResult.Meh); + } + + private void assertHeadJudgement(HitResult result) + => AddAssert($"head judged as{result}", () => judgementResults[0].Type == result); + + private void assertTailJudgement(HitResult result) + => AddAssert($"tail judged as {result}", () => judgementResults[^2].Type == result); + + private void assertNoteJudgement(HitResult result) + => AddAssert($"hold note judged as {result}", () => judgementResults[^1].Type == result); + + private void assertTickJudgement(HitResult result) + => AddAssert($"tick judged as {result}", () => judgementResults[6].Type == result); // arbitrary tick + + private ScoreAccessibleReplayPlayer currentPlayer; + + private void performTest(List frames) + { + AddStep("load player", () => + { + Beatmap.Value = CreateWorkingBeatmap(new Beatmap + { + HitObjects = + { + new HoldNote + { + StartTime = time_head, + Duration = time_tail - time_head, + Column = 0, + } + }, + BeatmapInfo = + { + BaseDifficulty = new BeatmapDifficulty { SliderTickRate = 4 }, + Ruleset = new ManiaRuleset().RulesetInfo + }, + }); + + Beatmap.Value.Beatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 0.1f }); + + var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } }); + + p.OnLoadComplete += _ => + { + p.ScoreProcessor.NewJudgement += result => + { + if (currentPlayer == p) judgementResults.Add(result); + }; + p.ScoreProcessor.AllJudged += () => + { + if (currentPlayer == p) allJudgedFired = true; + }; + }; + + LoadScreen(currentPlayer = p); + allJudgedFired = false; + judgementResults = new List(); + }); + + AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0); + AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen()); + AddUntilStep("Wait for all judged", () => allJudgedFired); + } + + private class ScoreAccessibleReplayPlayer : ReplayPlayer + { + public new ScoreProcessor ScoreProcessor => base.ScoreProcessor; + + protected override bool PauseOnFocusLost => false; + + public ScoreAccessibleReplayPlayer(Score score) + : base(score, false, false) + { + } + } + } +} From d6fd1007c46228c1d4df70c7855ad9bd3f3c3d0d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 23 Dec 2019 18:48:14 +0900 Subject: [PATCH 05/54] internal -> public --- osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs index c4a3e23cce..155adb958b 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs @@ -32,12 +32,12 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables /// /// Time at which the user started holding this hold note. Null if the user is not holding this hold note. /// - internal double? HoldStartTime; + public double? HoldStartTime { get; private set; } /// /// Whether the hold note has been released too early and shouldn't give full score for the release. /// - internal bool HasBroken; + public bool HasBroken { get; private set; } public DrawableHoldNote(HoldNote hitObject) : base(hitObject) From 63c96d5a83560e864d62585aa27e9443bd968236 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 23 Dec 2019 18:49:08 +0900 Subject: [PATCH 06/54] Fix tail note not properly capping result --- .../Objects/Drawables/DrawableHoldNoteTail.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs index a1f34a6db2..a660144dd1 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs @@ -49,8 +49,9 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables ApplyResult(r => { - if (holdNote.HasBroken && (result == HitResult.Perfect || result == HitResult.Perfect)) - result = HitResult.Good; + // If the head wasn't hit or the hold note was broken, cap the max score to Meh. + if (result > HitResult.Meh && (!holdNote.Head.IsHit || holdNote.HasBroken)) + result = HitResult.Meh; r.Type = result; }); From d5288760a7e85fd60320dbbc6b91c88737e8cf7d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 23 Dec 2019 18:50:18 +0900 Subject: [PATCH 07/54] Fix test text --- osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs index 8682604b13..7b0cf40d45 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs @@ -238,7 +238,7 @@ namespace osu.Game.Rulesets.Mania.Tests } private void assertHeadJudgement(HitResult result) - => AddAssert($"head judged as{result}", () => judgementResults[0].Type == result); + => AddAssert($"head judged as {result}", () => judgementResults[0].Type == result); private void assertTailJudgement(HitResult result) => AddAssert($"tail judged as {result}", () => judgementResults[^2].Type == result); From f603dd82bc87a59c6704ce30a74927b3a8ec7739 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 26 Dec 2019 14:52:08 +0900 Subject: [PATCH 08/54] Fix difficulty adjust mod not correctly reading settings while leaderboard visible --- osu.Game/OsuGame.cs | 22 +++++++++++++++++++ .../Rulesets/Mods/IApplicableToDifficulty.cs | 11 ++++++++++ osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs | 6 ++--- osu.Game/Rulesets/Mods/ModEasy.cs | 2 ++ osu.Game/Rulesets/Mods/ModHardRock.cs | 2 ++ 5 files changed, 40 insertions(+), 3 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index c7c746bed3..84aba4af52 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -39,6 +39,7 @@ using osu.Game.Online.Chat; using osu.Game.Skinning; using osuTK.Graphics; using osu.Game.Overlays.Volume; +using osu.Game.Rulesets.Mods; using osu.Game.Scoring; using osu.Game.Screens.Select; using osu.Game.Utils; @@ -204,6 +205,7 @@ namespace osu.Game Audio.AddAdjustment(AdjustableProperty.Volume, inactiveVolumeFade); + SelectedMods.BindValueChanged(modsChanged); Beatmap.BindValueChanged(beatmapChanged, true); } @@ -403,9 +405,29 @@ namespace osu.Game oldBeatmap.Track.Completed -= currentTrackCompleted; } + updateModDefaults(); + nextBeatmap?.LoadBeatmapAsync(); } + private void modsChanged(ValueChangedEvent> mods) + { + updateModDefaults(); + } + + private void updateModDefaults() + { + BeatmapDifficulty baseDifficulty = Beatmap.Value.BeatmapInfo.BaseDifficulty; + + if (baseDifficulty != null && SelectedMods.Value.Any(m => m is IApplicableToDifficulty)) + { + var adjustedDifficulty = baseDifficulty.Clone(); + + foreach (var mod in SelectedMods.Value.OfType()) + mod.ReadFromDifficulty(adjustedDifficulty); + } + } + private void currentTrackCompleted() => Schedule(() => { if (!Beatmap.Value.Track.Looping && !Beatmap.Disabled) diff --git a/osu.Game/Rulesets/Mods/IApplicableToDifficulty.cs b/osu.Game/Rulesets/Mods/IApplicableToDifficulty.cs index 4d4cd75434..d1d85f5aca 100644 --- a/osu.Game/Rulesets/Mods/IApplicableToDifficulty.cs +++ b/osu.Game/Rulesets/Mods/IApplicableToDifficulty.cs @@ -10,6 +10,17 @@ namespace osu.Game.Rulesets.Mods /// public interface IApplicableToDifficulty : IApplicableMod { + /// + /// Called when a beatmap is changed. Can be used to read default values. + /// Any changes made will not be preserved. + /// + /// + void ReadFromDifficulty(BeatmapDifficulty difficulty); + + /// + /// Called post beatmap conversion. Can be used to apply changes to difficulty attributes. + /// + /// void ApplyToDifficulty(BeatmapDifficulty difficulty); } } diff --git a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs index 224fc78508..7b8d4bb3df 100644 --- a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs @@ -47,17 +47,17 @@ namespace osu.Game.Rulesets.Mods private BeatmapDifficulty difficulty; - public void ApplyToDifficulty(BeatmapDifficulty difficulty) + public void ReadFromDifficulty(BeatmapDifficulty difficulty) { if (this.difficulty == null || this.difficulty.ID != difficulty.ID) { this.difficulty = difficulty; TransferSettings(difficulty); } - else - ApplySettings(difficulty); } + public void ApplyToDifficulty(BeatmapDifficulty difficulty) => ApplySettings(difficulty); + /// /// Transfer initial settings from the beatmap to settings. /// diff --git a/osu.Game/Rulesets/Mods/ModEasy.cs b/osu.Game/Rulesets/Mods/ModEasy.cs index a91e4dfd5c..a8058961ee 100644 --- a/osu.Game/Rulesets/Mods/ModEasy.cs +++ b/osu.Game/Rulesets/Mods/ModEasy.cs @@ -33,6 +33,8 @@ namespace osu.Game.Rulesets.Mods private BindableNumber health; + public void ReadFromDifficulty(BeatmapDifficulty difficulty) { } + public void ApplyToDifficulty(BeatmapDifficulty difficulty) { const float ratio = 0.5f; diff --git a/osu.Game/Rulesets/Mods/ModHardRock.cs b/osu.Game/Rulesets/Mods/ModHardRock.cs index 2bcac3e4a9..a613d41cf4 100644 --- a/osu.Game/Rulesets/Mods/ModHardRock.cs +++ b/osu.Game/Rulesets/Mods/ModHardRock.cs @@ -17,6 +17,8 @@ namespace osu.Game.Rulesets.Mods public override string Description => "Everything just got a bit harder..."; public override Type[] IncompatibleMods => new[] { typeof(ModEasy), typeof(ModDifficultyAdjust) }; + public void ReadFromDifficulty(BeatmapDifficulty difficulty) { } + public void ApplyToDifficulty(BeatmapDifficulty difficulty) { const float ratio = 1.4f; From 90cb9d9162cc11909a230b0b4fa9c058235a4e73 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 24 Dec 2019 17:01:17 +0900 Subject: [PATCH 09/54] Simplify scoreprocesor/healthprocessor implementations --- osu.Game.Rulesets.Catch/CatchRuleset.cs | 4 +- .../Scoring/CatchHealthProcessor.cs | 27 --------- .../Scoring/CatchScoreProcessor.cs | 6 -- osu.Game.Rulesets.Mania/ManiaRuleset.cs | 4 +- .../Scoring/ManiaHealthProcessor.cs | 58 ------------------- .../Scoring/ManiaScoreProcessor.cs | 6 -- osu.Game.Rulesets.Osu/OsuRuleset.cs | 4 +- .../Scoring/OsuHealthProcessor.cs | 39 ------------- .../Scoring/OsuScoreProcessor.cs | 6 -- .../Scoring/TaikoHealthProcessor.cs | 36 ------------ .../Scoring/TaikoScoreProcessor.cs | 6 -- osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 4 +- osu.Game/Rulesets/Ruleset.cs | 8 +-- osu.Game/Rulesets/Scoring/HealthProcessor.cs | 6 -- .../Rulesets/Scoring/JudgementProcessor.cs | 16 ++--- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 21 +++---- osu.Game/Screens/Play/Player.cs | 6 +- 17 files changed, 29 insertions(+), 228 deletions(-) diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs index f0e50c5ba5..b86fa09484 100644 --- a/osu.Game.Rulesets.Catch/CatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs @@ -28,9 +28,9 @@ namespace osu.Game.Rulesets.Catch { public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => new DrawableCatchRuleset(this, beatmap, mods); - public override ScoreProcessor CreateScoreProcessor(IBeatmap beatmap) => new CatchScoreProcessor(beatmap); + public override ScoreProcessor CreateScoreProcessor() => new CatchScoreProcessor(); - public override HealthProcessor CreateHealthProcessor(IBeatmap beatmap) => new CatchHealthProcessor(beatmap); + public override HealthProcessor CreateHealthProcessor() => new CatchHealthProcessor(); public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new CatchBeatmapConverter(beatmap, this); diff --git a/osu.Game.Rulesets.Catch/Scoring/CatchHealthProcessor.cs b/osu.Game.Rulesets.Catch/Scoring/CatchHealthProcessor.cs index 49ba0f6122..77a6c665bd 100644 --- a/osu.Game.Rulesets.Catch/Scoring/CatchHealthProcessor.cs +++ b/osu.Game.Rulesets.Catch/Scoring/CatchHealthProcessor.cs @@ -1,38 +1,11 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Game.Beatmaps; -using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Catch.Scoring { public class CatchHealthProcessor : HealthProcessor { - public CatchHealthProcessor(IBeatmap beatmap) - : base(beatmap) - { - } - - private float hpDrainRate; - - protected override void ApplyBeatmap(IBeatmap beatmap) - { - base.ApplyBeatmap(beatmap); - - hpDrainRate = beatmap.BeatmapInfo.BaseDifficulty.DrainRate; - } - - protected override double HealthAdjustmentFactorFor(JudgementResult result) - { - switch (result.Type) - { - case HitResult.Miss: - return hpDrainRate; - - default: - return 10.2 - hpDrainRate; // Award less HP as drain rate is increased - } - } } } diff --git a/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs b/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs index ad7520d57d..4c7bc4ab73 100644 --- a/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs +++ b/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs @@ -1,18 +1,12 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Game.Beatmaps; using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Catch.Scoring { public class CatchScoreProcessor : ScoreProcessor { - public CatchScoreProcessor(IBeatmap beatmap) - : base(beatmap) - { - } - public override HitWindows CreateHitWindows() => new CatchHitWindows(); } } diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index 5bca8588b6..4cc439f909 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -35,9 +35,9 @@ namespace osu.Game.Rulesets.Mania { public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => new DrawableManiaRuleset(this, beatmap, mods); - public override ScoreProcessor CreateScoreProcessor(IBeatmap beatmap) => new ManiaScoreProcessor(beatmap); + public override ScoreProcessor CreateScoreProcessor() => new ManiaScoreProcessor(); - public override HealthProcessor CreateHealthProcessor(IBeatmap beatmap) => new ManiaHealthProcessor(beatmap); + public override HealthProcessor CreateHealthProcessor() => new ManiaHealthProcessor(); public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new ManiaBeatmapConverter(beatmap, this); diff --git a/osu.Game.Rulesets.Mania/Scoring/ManiaHealthProcessor.cs b/osu.Game.Rulesets.Mania/Scoring/ManiaHealthProcessor.cs index c362c906a4..391fe0ace7 100644 --- a/osu.Game.Rulesets.Mania/Scoring/ManiaHealthProcessor.cs +++ b/osu.Game.Rulesets.Mania/Scoring/ManiaHealthProcessor.cs @@ -1,69 +1,11 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Game.Beatmaps; -using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Mania.Scoring { public class ManiaHealthProcessor : HealthProcessor { - /// - /// The hit HP multiplier at OD = 0. - /// - private const double hp_multiplier_min = 0.75; - - /// - /// The hit HP multiplier at OD = 0. - /// - private const double hp_multiplier_mid = 0.85; - - /// - /// The hit HP multiplier at OD = 0. - /// - private const double hp_multiplier_max = 1; - - /// - /// The MISS HP multiplier at OD = 0. - /// - private const double hp_multiplier_miss_min = 0.5; - - /// - /// The MISS HP multiplier at OD = 5. - /// - private const double hp_multiplier_miss_mid = 0.75; - - /// - /// The MISS HP multiplier at OD = 10. - /// - private const double hp_multiplier_miss_max = 1; - - /// - /// The MISS HP multiplier. This is multiplied to the miss hp increase. - /// - private double hpMissMultiplier = 1; - - /// - /// The HIT HP multiplier. This is multiplied to hit hp increases. - /// - private double hpMultiplier = 1; - - public ManiaHealthProcessor(IBeatmap beatmap) - : base(beatmap) - { - } - - protected override void ApplyBeatmap(IBeatmap beatmap) - { - base.ApplyBeatmap(beatmap); - - BeatmapDifficulty difficulty = beatmap.BeatmapInfo.BaseDifficulty; - hpMultiplier = BeatmapDifficulty.DifficultyRange(difficulty.DrainRate, hp_multiplier_min, hp_multiplier_mid, hp_multiplier_max); - hpMissMultiplier = BeatmapDifficulty.DifficultyRange(difficulty.DrainRate, hp_multiplier_miss_min, hp_multiplier_miss_mid, hp_multiplier_miss_max); - } - - protected override double HealthAdjustmentFactorFor(JudgementResult result) - => result.Type == HitResult.Miss ? hpMissMultiplier : hpMultiplier; } } diff --git a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs index 97f1ea721c..9b54b48de3 100644 --- a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs +++ b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs @@ -1,18 +1,12 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Game.Beatmaps; using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Mania.Scoring { internal class ManiaScoreProcessor : ScoreProcessor { - public ManiaScoreProcessor(IBeatmap beatmap) - : base(beatmap) - { - } - public override HitWindows CreateHitWindows() => new ManiaHitWindows(); } } diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index c8a156dc57..f7b4f90209 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -36,7 +36,9 @@ namespace osu.Game.Rulesets.Osu { public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => new DrawableOsuRuleset(this, beatmap, mods); - public override ScoreProcessor CreateScoreProcessor(IBeatmap beatmap) => new OsuScoreProcessor(beatmap); + public override ScoreProcessor CreateScoreProcessor() => new OsuScoreProcessor(); + + public override HealthProcessor CreateHealthProcessor() => new OsuHealthProcessor(); public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new OsuBeatmapConverter(beatmap, this); diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs index 36ccc80af6..d6d83425ff 100644 --- a/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs +++ b/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Game.Beatmaps; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Judgements; @@ -11,44 +10,6 @@ namespace osu.Game.Rulesets.Osu.Scoring { public class OsuHealthProcessor : HealthProcessor { - public OsuHealthProcessor(IBeatmap beatmap) - : base(beatmap) - { - } - - private float hpDrainRate; - - protected override void ApplyBeatmap(IBeatmap beatmap) - { - base.ApplyBeatmap(beatmap); - - hpDrainRate = beatmap.BeatmapInfo.BaseDifficulty.DrainRate; - } - - protected override double HealthAdjustmentFactorFor(JudgementResult result) - { - switch (result.Type) - { - case HitResult.Great: - return 10.2 - hpDrainRate; - - case HitResult.Good: - return 8 - hpDrainRate; - - case HitResult.Meh: - return 4 - hpDrainRate; - - // case HitResult.SliderTick: - // return Math.Max(7 - hpDrainRate, 0) * 0.01; - - case HitResult.Miss: - return hpDrainRate; - - default: - return 0; - } - } - protected override JudgementResult CreateResult(HitObject hitObject, Judgement judgement) => new OsuJudgementResult(hitObject, judgement); } } diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs index 4593364e42..1de7d488f3 100644 --- a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs +++ b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Game.Beatmaps; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Judgements; @@ -11,11 +10,6 @@ namespace osu.Game.Rulesets.Osu.Scoring { internal class OsuScoreProcessor : ScoreProcessor { - public OsuScoreProcessor(IBeatmap beatmap) - : base(beatmap) - { - } - protected override JudgementResult CreateResult(HitObject hitObject, Judgement judgement) => new OsuJudgementResult(hitObject, judgement); public override HitWindows CreateHitWindows() => new OsuHitWindows(); diff --git a/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs b/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs index c8aa32a678..df5dd19de3 100644 --- a/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs +++ b/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs @@ -1,53 +1,17 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Linq; -using osu.Game.Beatmaps; -using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; -using osu.Game.Rulesets.Taiko.Objects; namespace osu.Game.Rulesets.Taiko.Scoring { public class TaikoHealthProcessor : HealthProcessor { - /// - /// A value used for calculating . - /// - private const double object_count_factor = 3; - /// /// Taiko fails at the end of the map if the player has not half-filled their HP bar. /// protected override bool DefaultFailCondition => JudgedHits == MaxHits && Health.Value <= 0.5; - /// - /// HP multiplier for a successful . - /// - private double hpMultiplier; - - /// - /// HP multiplier for a . - /// - private double hpMissMultiplier; - - public TaikoHealthProcessor(IBeatmap beatmap) - : base(beatmap) - { - } - - protected override void ApplyBeatmap(IBeatmap beatmap) - { - base.ApplyBeatmap(beatmap); - - hpMultiplier = 1 / (object_count_factor * beatmap.HitObjects.OfType().Count() * BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.DrainRate, 0.5, 0.75, 0.98)); - - hpMissMultiplier = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.DrainRate, 0.0018, 0.0075, 0.0120); - } - - protected override double HealthAdjustmentFactorFor(JudgementResult result) - => result.Type == HitResult.Miss ? hpMissMultiplier : hpMultiplier; - protected override void Reset(bool storeResults) { base.Reset(storeResults); diff --git a/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs b/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs index 10011d2669..003d40af56 100644 --- a/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs +++ b/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs @@ -1,18 +1,12 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Game.Beatmaps; using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Taiko.Scoring { internal class TaikoScoreProcessor : ScoreProcessor { - public TaikoScoreProcessor(IBeatmap beatmap) - : base(beatmap) - { - } - public override HitWindows CreateHitWindows() => new TaikoHitWindows(); } } diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index c5c2027af1..6763a1d340 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -28,9 +28,9 @@ namespace osu.Game.Rulesets.Taiko { public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => new DrawableTaikoRuleset(this, beatmap, mods); - public override ScoreProcessor CreateScoreProcessor(IBeatmap beatmap) => new TaikoScoreProcessor(beatmap); + public override ScoreProcessor CreateScoreProcessor() => new TaikoScoreProcessor(); - public override HealthProcessor CreateHealthProcessor(IBeatmap beatmap) => new TaikoHealthProcessor(beatmap); + public override HealthProcessor CreateHealthProcessor() => new TaikoHealthProcessor(); public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new TaikoBeatmapConverter(beatmap, this); diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index bfd6a16729..72973cc677 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -71,16 +71,16 @@ namespace osu.Game.Rulesets public abstract DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null); /// - /// Creates a for a beatmap converted to this ruleset. + /// Creates a for this . /// /// The score processor. - public virtual ScoreProcessor CreateScoreProcessor(IBeatmap beatmap) => new ScoreProcessor(beatmap); + public virtual ScoreProcessor CreateScoreProcessor() => new ScoreProcessor(); /// - /// Creates a for a beatmap converted to this ruleset. + /// Creates a for this . /// /// The health processor. - public virtual HealthProcessor CreateHealthProcessor(IBeatmap beatmap) => new HealthProcessor(beatmap); + public virtual HealthProcessor CreateHealthProcessor() => new HealthProcessor(); /// /// Creates a to convert a to one that is applicable for this . diff --git a/osu.Game/Rulesets/Scoring/HealthProcessor.cs b/osu.Game/Rulesets/Scoring/HealthProcessor.cs index d05e2d7b6b..f027cac8bd 100644 --- a/osu.Game/Rulesets/Scoring/HealthProcessor.cs +++ b/osu.Game/Rulesets/Scoring/HealthProcessor.cs @@ -4,7 +4,6 @@ using System; using osu.Framework.Bindables; using osu.Framework.MathUtils; -using osu.Game.Beatmaps; using osu.Game.Rulesets.Judgements; namespace osu.Game.Rulesets.Scoring @@ -32,11 +31,6 @@ namespace osu.Game.Rulesets.Scoring /// public bool HasFailed { get; private set; } - public HealthProcessor(IBeatmap beatmap) - : base(beatmap) - { - } - protected override void ApplyResultInternal(JudgementResult result) { result.HealthAtJudgement = Health.Value; diff --git a/osu.Game/Rulesets/Scoring/JudgementProcessor.cs b/osu.Game/Rulesets/Scoring/JudgementProcessor.cs index c7ac466eb0..54debebde9 100644 --- a/osu.Game/Rulesets/Scoring/JudgementProcessor.cs +++ b/osu.Game/Rulesets/Scoring/JudgementProcessor.cs @@ -36,23 +36,17 @@ namespace osu.Game.Rulesets.Scoring /// public bool HasCompleted => JudgedHits == MaxHits; - protected JudgementProcessor(IBeatmap beatmap) + /// + /// Applies a to this . + /// + /// The to read properties from. + public void ApplyBeatmap(IBeatmap beatmap) { - ApplyBeatmap(beatmap); - Reset(false); SimulateAutoplay(beatmap); Reset(true); } - /// - /// Applies any properties of the which affect scoring to this . - /// - /// The to read properties from. - protected virtual void ApplyBeatmap(IBeatmap beatmap) - { - } - /// /// Applies the score change of a to this . /// diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index acd394d955..8ccc2af93b 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -7,7 +7,6 @@ using System.Diagnostics; using System.Linq; using osu.Framework.Bindables; using osu.Framework.Extensions; -using osu.Game.Beatmaps; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mods; using osu.Game.Scoring; @@ -64,15 +63,9 @@ namespace osu.Game.Rulesets.Scoring private double scoreMultiplier = 1; - public ScoreProcessor(IBeatmap beatmap) - : base(beatmap) + public ScoreProcessor() { Debug.Assert(base_portion + combo_portion == 1.0); - } - - protected override void ApplyBeatmap(IBeatmap beatmap) - { - base.ApplyBeatmap(beatmap); Combo.ValueChanged += combo => HighestCombo.Value = Math.Max(HighestCombo.Value, combo.NewValue); Accuracy.ValueChanged += accuracy => @@ -82,12 +75,6 @@ namespace osu.Game.Rulesets.Scoring Rank.Value = mod.AdjustRank(Rank.Value, accuracy.NewValue); }; - if (maxBaseScore == 0 || maxHighestCombo == 0) - { - Mode.Value = ScoringMode.Classic; - Mode.Disabled = true; - } - Mode.ValueChanged += _ => updateScore(); Mods.ValueChanged += mods => { @@ -225,6 +212,12 @@ namespace osu.Game.Rulesets.Scoring { maxHighestCombo = HighestCombo.Value; maxBaseScore = baseScore; + + if (maxBaseScore == 0 || maxHighestCombo == 0) + { + Mode.Value = ScoringMode.Classic; + Mode.Disabled = true; + } } baseScore = 0; diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index f57bfb0bde..3699763e14 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -131,10 +131,12 @@ namespace osu.Game.Screens.Play DrawableRuleset = ruleset.CreateDrawableRulesetWith(playableBeatmap, Mods.Value); - ScoreProcessor = ruleset.CreateScoreProcessor(playableBeatmap); + ScoreProcessor = ruleset.CreateScoreProcessor(); + ScoreProcessor.ApplyBeatmap(playableBeatmap); ScoreProcessor.Mods.BindTo(Mods); - HealthProcessor = ruleset.CreateHealthProcessor(playableBeatmap); + HealthProcessor = ruleset.CreateHealthProcessor(); + HealthProcessor.ApplyBeatmap(playableBeatmap); if (!ScoreProcessor.Mode.Disabled) config.BindWith(OsuSetting.ScoreDisplayMode, ScoreProcessor.Mode); From 985277da797e4985d4291f86aa4af2aa5c5b34fc Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 24 Dec 2019 17:12:58 +0900 Subject: [PATCH 10/54] Add time elapsation to judgement simulation --- osu.Game/Rulesets/Scoring/JudgementProcessor.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Scoring/JudgementProcessor.cs b/osu.Game/Rulesets/Scoring/JudgementProcessor.cs index 54debebde9..b0bc205ea9 100644 --- a/osu.Game/Rulesets/Scoring/JudgementProcessor.cs +++ b/osu.Game/Rulesets/Scoring/JudgementProcessor.cs @@ -47,6 +47,10 @@ namespace osu.Game.Rulesets.Scoring Reset(true); } + public virtual void ApplyElapsedTime(double elapsedTime) + { + } + /// /// Applies the score change of a to this . /// @@ -115,6 +119,8 @@ namespace osu.Game.Rulesets.Scoring /// The to simulate. protected virtual void SimulateAutoplay(IBeatmap beatmap) { + HitObject lastObject = null; + foreach (var obj in beatmap.HitObjects) simulate(obj); @@ -123,6 +129,9 @@ namespace osu.Game.Rulesets.Scoring foreach (var nested in obj.NestedHitObjects) simulate(nested); + if (lastObject != null) + ApplyElapsedTime(lastObject.GetEndTime() - obj.StartTime); + var judgement = obj.CreateJudgement(); if (judgement == null) return; @@ -132,8 +141,9 @@ namespace osu.Game.Rulesets.Scoring throw new InvalidOperationException($"{GetType().ReadableName()} must provide a {nameof(JudgementResult)} through {nameof(CreateResult)}."); result.Type = judgement.MaxResult; - ApplyResult(result); + + lastObject = obj; } } } From a4b1c79e18bb9656e3c1392b42de2431e15827d6 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 25 Dec 2019 13:09:04 +0900 Subject: [PATCH 11/54] Initial implementation of HP drain --- osu.Game/Rulesets/Scoring/HealthProcessor.cs | 48 +++++++++++++++++++- osu.Game/Screens/Play/Player.cs | 8 ++++ 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Scoring/HealthProcessor.cs b/osu.Game/Rulesets/Scoring/HealthProcessor.cs index f027cac8bd..270128b60e 100644 --- a/osu.Game/Rulesets/Scoring/HealthProcessor.cs +++ b/osu.Game/Rulesets/Scoring/HealthProcessor.cs @@ -2,9 +2,11 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using osu.Framework.Bindables; using osu.Framework.MathUtils; using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects; namespace osu.Game.Rulesets.Scoring { @@ -31,15 +33,23 @@ namespace osu.Game.Rulesets.Scoring /// public bool HasFailed { get; private set; } + private readonly List<(double time, double health)> healthIncreases = new List<(double time, double health)>(); + private double drainRate = 1; + + public override void ApplyElapsedTime(double elapsedTime) => Health.Value -= drainRate * elapsedTime; + protected override void ApplyResultInternal(JudgementResult result) { result.HealthAtJudgement = Health.Value; result.FailedAtJudgement = HasFailed; + double healthIncrease = HealthAdjustmentFactorFor(result) * result.Judgement.HealthIncreaseFor(result); + healthIncreases.Add((result.HitObject.GetEndTime() + result.TimeOffset, healthIncrease)); + if (HasFailed) return; - Health.Value += HealthAdjustmentFactorFor(result) * result.Judgement.HealthIncreaseFor(result); + Health.Value += healthIncrease; if (!DefaultFailCondition && FailConditions?.Invoke(this, result) != true) return; @@ -71,6 +81,42 @@ namespace osu.Game.Rulesets.Scoring { base.Reset(storeResults); + drainRate = 1; + + if (storeResults) + { + const double percentage_target = 0.5; + + int count = 1; + + while (true) + { + double currentHealth = 1; + double lowestHealth = 1; + + for (int i = 0; i < healthIncreases.Count; i++) + { + var lastTime = i > 0 ? healthIncreases[i - 1].time : 0; + + currentHealth -= (healthIncreases[i].time - lastTime) * drainRate; + lowestHealth = Math.Min(lowestHealth, currentHealth); + currentHealth = Math.Min(1, currentHealth + healthIncreases[i].health); + + // Common scenario for when the drain rate is definitely too harsh + if (lowestHealth < 0) + break; + } + + if (Math.Abs(lowestHealth - percentage_target) <= 0.01) + break; + + count *= 2; + drainRate += 1.0 / count * Math.Sign(lowestHealth - percentage_target); + } + } + + healthIncreases.Clear(); + Health.Value = 1; HasFailed = false; } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 3699763e14..bc6510440a 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -342,6 +342,14 @@ namespace osu.Game.Screens.Play this.Exit(); } + protected override void Update() + { + base.Update(); + + if (!GameplayClockContainer.IsPaused.Value) + HealthProcessor.ApplyElapsedTime(GameplayClockContainer.GameplayClock.ElapsedFrameTime); + } + /// /// Restart gameplay via a parent . /// This can be called from a child screen in order to trigger the restart process. From 70d2d8a2fabdea21d4328077fa8a827552cd8f3a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 25 Dec 2019 13:32:58 +0900 Subject: [PATCH 12/54] Add adjustable target percentage --- osu.Game/Rulesets/Scoring/HealthProcessor.cs | 14 ++++++++++---- osu.Game/Rulesets/Scoring/JudgementProcessor.cs | 2 +- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/HealthProcessor.cs b/osu.Game/Rulesets/Scoring/HealthProcessor.cs index 270128b60e..bb8f377f84 100644 --- a/osu.Game/Rulesets/Scoring/HealthProcessor.cs +++ b/osu.Game/Rulesets/Scoring/HealthProcessor.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using osu.Framework.Bindables; using osu.Framework.MathUtils; +using osu.Game.Beatmaps; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; @@ -34,8 +35,15 @@ namespace osu.Game.Rulesets.Scoring public bool HasFailed { get; private set; } private readonly List<(double time, double health)> healthIncreases = new List<(double time, double health)>(); + private double targetMinimumHealth; private double drainRate = 1; + public override void ApplyBeatmap(IBeatmap beatmap) + { + targetMinimumHealth = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.DrainRate, 0.95, 0.85, 0.65); + base.ApplyBeatmap(beatmap); + } + public override void ApplyElapsedTime(double elapsedTime) => Health.Value -= drainRate * elapsedTime; protected override void ApplyResultInternal(JudgementResult result) @@ -85,8 +93,6 @@ namespace osu.Game.Rulesets.Scoring if (storeResults) { - const double percentage_target = 0.5; - int count = 1; while (true) @@ -107,11 +113,11 @@ namespace osu.Game.Rulesets.Scoring break; } - if (Math.Abs(lowestHealth - percentage_target) <= 0.01) + if (Math.Abs(lowestHealth - targetMinimumHealth) <= 0.01) break; count *= 2; - drainRate += 1.0 / count * Math.Sign(lowestHealth - percentage_target); + drainRate += 1.0 / count * Math.Sign(lowestHealth - targetMinimumHealth); } } diff --git a/osu.Game/Rulesets/Scoring/JudgementProcessor.cs b/osu.Game/Rulesets/Scoring/JudgementProcessor.cs index b0bc205ea9..33dc4b9a22 100644 --- a/osu.Game/Rulesets/Scoring/JudgementProcessor.cs +++ b/osu.Game/Rulesets/Scoring/JudgementProcessor.cs @@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Scoring /// Applies a to this . /// /// The to read properties from. - public void ApplyBeatmap(IBeatmap beatmap) + public virtual void ApplyBeatmap(IBeatmap beatmap) { Reset(false); SimulateAutoplay(beatmap); From bd74d086fb412e32cb9249e41287c47dbb037f5c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 25 Dec 2019 13:33:50 +0900 Subject: [PATCH 13/54] Remove adjustment factor --- osu.Game/Rulesets/Scoring/HealthProcessor.cs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/HealthProcessor.cs b/osu.Game/Rulesets/Scoring/HealthProcessor.cs index bb8f377f84..8c2e2b66ca 100644 --- a/osu.Game/Rulesets/Scoring/HealthProcessor.cs +++ b/osu.Game/Rulesets/Scoring/HealthProcessor.cs @@ -51,7 +51,7 @@ namespace osu.Game.Rulesets.Scoring result.HealthAtJudgement = Health.Value; result.FailedAtJudgement = HasFailed; - double healthIncrease = HealthAdjustmentFactorFor(result) * result.Judgement.HealthIncreaseFor(result); + double healthIncrease = result.Judgement.HealthIncreaseFor(result); healthIncreases.Add((result.HitObject.GetEndTime() + result.TimeOffset, healthIncrease)); if (HasFailed) @@ -73,13 +73,6 @@ namespace osu.Game.Rulesets.Scoring // Todo: Revert HasFailed state with proper player support } - /// - /// An adjustment factor which is multiplied into the health increase provided by a . - /// - /// The for which the adjustment should apply. - /// The adjustment factor. - protected virtual double HealthAdjustmentFactorFor(JudgementResult result) => 1; - /// /// The default conditions for failing. /// From 85c44b5a5a015217931c16651f5389ab47d52fab Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 25 Dec 2019 13:34:13 +0900 Subject: [PATCH 14/54] Disable list additions during gameplay --- osu.Game/Rulesets/Scoring/HealthProcessor.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/HealthProcessor.cs b/osu.Game/Rulesets/Scoring/HealthProcessor.cs index 8c2e2b66ca..2cb51fa853 100644 --- a/osu.Game/Rulesets/Scoring/HealthProcessor.cs +++ b/osu.Game/Rulesets/Scoring/HealthProcessor.cs @@ -34,14 +34,19 @@ namespace osu.Game.Rulesets.Scoring /// public bool HasFailed { get; private set; } - private readonly List<(double time, double health)> healthIncreases = new List<(double time, double health)>(); + private List<(double time, double health)> healthIncreases; private double targetMinimumHealth; private double drainRate = 1; public override void ApplyBeatmap(IBeatmap beatmap) { + healthIncreases = new List<(double time, double health)>(); targetMinimumHealth = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.DrainRate, 0.95, 0.85, 0.65); + base.ApplyBeatmap(beatmap); + + // Only required during the simulation stage + healthIncreases = null; } public override void ApplyElapsedTime(double elapsedTime) => Health.Value -= drainRate * elapsedTime; @@ -52,7 +57,7 @@ namespace osu.Game.Rulesets.Scoring result.FailedAtJudgement = HasFailed; double healthIncrease = result.Judgement.HealthIncreaseFor(result); - healthIncreases.Add((result.HitObject.GetEndTime() + result.TimeOffset, healthIncrease)); + healthIncreases?.Add((result.HitObject.GetEndTime() + result.TimeOffset, healthIncrease)); if (HasFailed) return; From 977fb3d1bfe862f9fc38b37b110894a12d0379bd Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 25 Dec 2019 14:35:32 +0900 Subject: [PATCH 15/54] Make processors and break overlay frame-stable --- osu.Game/Rulesets/Scoring/HealthProcessor.cs | 7 +++++- .../Rulesets/Scoring/JudgementProcessor.cs | 14 ++--------- osu.Game/Rulesets/UI/DrawableRuleset.cs | 25 ++++++++++++------- osu.Game/Screens/Play/Player.cs | 24 ++++++++---------- 4 files changed, 34 insertions(+), 36 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/HealthProcessor.cs b/osu.Game/Rulesets/Scoring/HealthProcessor.cs index 2cb51fa853..b9638fed15 100644 --- a/osu.Game/Rulesets/Scoring/HealthProcessor.cs +++ b/osu.Game/Rulesets/Scoring/HealthProcessor.cs @@ -49,7 +49,12 @@ namespace osu.Game.Rulesets.Scoring healthIncreases = null; } - public override void ApplyElapsedTime(double elapsedTime) => Health.Value -= drainRate * elapsedTime; + protected override void Update() + { + base.Update(); + + Health.Value -= drainRate * Time.Elapsed; + } protected override void ApplyResultInternal(JudgementResult result) { diff --git a/osu.Game/Rulesets/Scoring/JudgementProcessor.cs b/osu.Game/Rulesets/Scoring/JudgementProcessor.cs index 33dc4b9a22..3016007f98 100644 --- a/osu.Game/Rulesets/Scoring/JudgementProcessor.cs +++ b/osu.Game/Rulesets/Scoring/JudgementProcessor.cs @@ -3,13 +3,14 @@ using System; using osu.Framework.Extensions.TypeExtensions; +using osu.Framework.Graphics; using osu.Game.Beatmaps; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; namespace osu.Game.Rulesets.Scoring { - public abstract class JudgementProcessor + public abstract class JudgementProcessor : Component { /// /// Invoked when all s have been judged by this . @@ -47,10 +48,6 @@ namespace osu.Game.Rulesets.Scoring Reset(true); } - public virtual void ApplyElapsedTime(double elapsedTime) - { - } - /// /// Applies the score change of a to this . /// @@ -119,8 +116,6 @@ namespace osu.Game.Rulesets.Scoring /// The to simulate. protected virtual void SimulateAutoplay(IBeatmap beatmap) { - HitObject lastObject = null; - foreach (var obj in beatmap.HitObjects) simulate(obj); @@ -129,9 +124,6 @@ namespace osu.Game.Rulesets.Scoring foreach (var nested in obj.NestedHitObjects) simulate(nested); - if (lastObject != null) - ApplyElapsedTime(lastObject.GetEndTime() - obj.StartTime); - var judgement = obj.CreateJudgement(); if (judgement == null) return; @@ -142,8 +134,6 @@ namespace osu.Game.Rulesets.Scoring result.Type = judgement.MaxResult; ApplyResult(result); - - lastObject = obj; } } } diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index f318539bb7..e624fb80fa 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -72,10 +72,9 @@ namespace osu.Game.Rulesets.UI /// public override Playfield Playfield => playfield.Value; - /// - /// Place to put drawables above hit objects but below UI. - /// - public Container Overlays { get; private set; } + private Container overlays; + + public override Container Overlays => overlays; public override GameplayClock FrameStableClock => frameStabilityContainer.GameplayClock; @@ -185,12 +184,15 @@ namespace osu.Game.Rulesets.UI frameStabilityContainer = new FrameStabilityContainer(GameplayStartTime) { FrameStablePlayback = FrameStablePlayback, - Child = KeyBindingInputManager - .WithChild(CreatePlayfieldAdjustmentContainer() - .WithChild(Playfield) - ) + Children = new Drawable[] + { + KeyBindingInputManager + .WithChild(CreatePlayfieldAdjustmentContainer() + .WithChild(Playfield) + ), + overlays = new Container { RelativeSizeAxes = Axes.Both } + } }, - Overlays = new Container { RelativeSizeAxes = Axes.Both } }; if ((ResumeOverlay = CreateResumeOverlay()) != null) @@ -385,6 +387,11 @@ namespace osu.Game.Rulesets.UI /// public abstract Playfield Playfield { get; } + /// + /// Place to put drawables above hit objects but below UI. + /// + public abstract Container Overlays { get; } + /// /// The frame-stable clock which is being used for playfield display. /// diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index bc6510440a..620a9195f7 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -208,12 +208,6 @@ namespace osu.Game.Screens.Play { target.AddRange(new[] { - BreakOverlay = new BreakOverlay(working.Beatmap.BeatmapInfo.LetterboxInBreaks, DrawableRuleset.GameplayStartTime, ScoreProcessor) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Breaks = working.Beatmap.Breaks - }, // display the cursor above some HUD elements. DrawableRuleset.Cursor?.CreateProxy() ?? new Container(), DrawableRuleset.ResumeOverlay?.CreateProxy() ?? new Container(), @@ -268,6 +262,16 @@ namespace osu.Game.Screens.Play }, failAnimation = new FailAnimation(DrawableRuleset) { OnComplete = onFailComplete, } }); + + DrawableRuleset.Overlays.Add(BreakOverlay = new BreakOverlay(working.Beatmap.BeatmapInfo.LetterboxInBreaks, DrawableRuleset.GameplayStartTime, ScoreProcessor) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Breaks = working.Beatmap.Breaks + }); + + DrawableRuleset.Overlays.Add(ScoreProcessor); + DrawableRuleset.Overlays.Add(HealthProcessor); } private void updatePauseOnFocusLostState() => @@ -342,14 +346,6 @@ namespace osu.Game.Screens.Play this.Exit(); } - protected override void Update() - { - base.Update(); - - if (!GameplayClockContainer.IsPaused.Value) - HealthProcessor.ApplyElapsedTime(GameplayClockContainer.GameplayClock.ElapsedFrameTime); - } - /// /// Restart gameplay via a parent . /// This can be called from a child screen in order to trigger the restart process. From b348abcd0779fe203e9c10dc91018ca7d54d1fc6 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 25 Dec 2019 14:49:17 +0900 Subject: [PATCH 16/54] Only drain health in non-break times --- osu.Game/Rulesets/Scoring/HealthProcessor.cs | 8 +++++++- osu.Game/Screens/Play/Player.cs | 2 ++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Scoring/HealthProcessor.cs b/osu.Game/Rulesets/Scoring/HealthProcessor.cs index b9638fed15..af81f3b8b5 100644 --- a/osu.Game/Rulesets/Scoring/HealthProcessor.cs +++ b/osu.Game/Rulesets/Scoring/HealthProcessor.cs @@ -29,6 +29,11 @@ namespace osu.Game.Rulesets.Scoring /// public readonly BindableDouble Health = new BindableDouble(1) { MinValue = 0, MaxValue = 1 }; + /// + /// Whether gameplay is currently in a break. + /// + public readonly IBindable IsBreakTime = new Bindable(); + /// /// Whether this ScoreProcessor has already triggered the failed state. /// @@ -53,7 +58,8 @@ namespace osu.Game.Rulesets.Scoring { base.Update(); - Health.Value -= drainRate * Time.Elapsed; + if (!IsBreakTime.Value) + Health.Value -= drainRate * Time.Elapsed; } protected override void ApplyResultInternal(JudgementResult result) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 620a9195f7..dc0dd86c51 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -272,6 +272,8 @@ namespace osu.Game.Screens.Play DrawableRuleset.Overlays.Add(ScoreProcessor); DrawableRuleset.Overlays.Add(HealthProcessor); + + HealthProcessor.IsBreakTime.BindTo(BreakOverlay.IsBreakTime); } private void updatePauseOnFocusLostState() => From 522847987bd4c16b6d24b83955c02a9e3455c0fb Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 25 Dec 2019 15:33:13 +0900 Subject: [PATCH 17/54] Implement break/gameplay start times --- osu.Game.Rulesets.Catch/CatchRuleset.cs | 2 -- .../Scoring/CatchHealthProcessor.cs | 11 -------- osu.Game.Rulesets.Mania/ManiaRuleset.cs | 2 -- .../Scoring/ManiaHealthProcessor.cs | 11 -------- osu.Game.Rulesets.Osu/OsuRuleset.cs | 2 -- .../Scoring/OsuHealthProcessor.cs | 15 ---------- .../Scoring/TaikoHealthProcessor.cs | 22 --------------- osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 2 -- osu.Game/Rulesets/Ruleset.cs | 2 +- osu.Game/Rulesets/Scoring/HealthProcessor.cs | 28 +++++++++++++++++-- osu.Game/Screens/Play/Player.cs | 2 +- 11 files changed, 28 insertions(+), 71 deletions(-) delete mode 100644 osu.Game.Rulesets.Catch/Scoring/CatchHealthProcessor.cs delete mode 100644 osu.Game.Rulesets.Mania/Scoring/ManiaHealthProcessor.cs delete mode 100644 osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs delete mode 100644 osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs index b86fa09484..dc8df28e6a 100644 --- a/osu.Game.Rulesets.Catch/CatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs @@ -30,8 +30,6 @@ namespace osu.Game.Rulesets.Catch public override ScoreProcessor CreateScoreProcessor() => new CatchScoreProcessor(); - public override HealthProcessor CreateHealthProcessor() => new CatchHealthProcessor(); - public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new CatchBeatmapConverter(beatmap, this); public override IBeatmapProcessor CreateBeatmapProcessor(IBeatmap beatmap) => new CatchBeatmapProcessor(beatmap); diff --git a/osu.Game.Rulesets.Catch/Scoring/CatchHealthProcessor.cs b/osu.Game.Rulesets.Catch/Scoring/CatchHealthProcessor.cs deleted file mode 100644 index 77a6c665bd..0000000000 --- a/osu.Game.Rulesets.Catch/Scoring/CatchHealthProcessor.cs +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Game.Rulesets.Scoring; - -namespace osu.Game.Rulesets.Catch.Scoring -{ - public class CatchHealthProcessor : HealthProcessor - { - } -} diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index 4cc439f909..c693aa05de 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -37,8 +37,6 @@ namespace osu.Game.Rulesets.Mania public override ScoreProcessor CreateScoreProcessor() => new ManiaScoreProcessor(); - public override HealthProcessor CreateHealthProcessor() => new ManiaHealthProcessor(); - public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new ManiaBeatmapConverter(beatmap, this); diff --git a/osu.Game.Rulesets.Mania/Scoring/ManiaHealthProcessor.cs b/osu.Game.Rulesets.Mania/Scoring/ManiaHealthProcessor.cs deleted file mode 100644 index 391fe0ace7..0000000000 --- a/osu.Game.Rulesets.Mania/Scoring/ManiaHealthProcessor.cs +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Game.Rulesets.Scoring; - -namespace osu.Game.Rulesets.Mania.Scoring -{ - public class ManiaHealthProcessor : HealthProcessor - { - } -} diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index f7b4f90209..36346eb78a 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -38,8 +38,6 @@ namespace osu.Game.Rulesets.Osu public override ScoreProcessor CreateScoreProcessor() => new OsuScoreProcessor(); - public override HealthProcessor CreateHealthProcessor() => new OsuHealthProcessor(); - public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new OsuBeatmapConverter(beatmap, this); public override IBeatmapProcessor CreateBeatmapProcessor(IBeatmap beatmap) => new OsuBeatmapProcessor(beatmap); diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs deleted file mode 100644 index d6d83425ff..0000000000 --- a/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Game.Rulesets.Judgements; -using osu.Game.Rulesets.Objects; -using osu.Game.Rulesets.Osu.Judgements; -using osu.Game.Rulesets.Scoring; - -namespace osu.Game.Rulesets.Osu.Scoring -{ - public class OsuHealthProcessor : HealthProcessor - { - protected override JudgementResult CreateResult(HitObject hitObject, Judgement judgement) => new OsuJudgementResult(hitObject, judgement); - } -} diff --git a/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs b/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs deleted file mode 100644 index df5dd19de3..0000000000 --- a/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Game.Rulesets.Scoring; - -namespace osu.Game.Rulesets.Taiko.Scoring -{ - public class TaikoHealthProcessor : HealthProcessor - { - /// - /// Taiko fails at the end of the map if the player has not half-filled their HP bar. - /// - protected override bool DefaultFailCondition => JudgedHits == MaxHits && Health.Value <= 0.5; - - protected override void Reset(bool storeResults) - { - base.Reset(storeResults); - - Health.Value = 0; - } - } -} diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index 6763a1d340..79de2d0334 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -30,8 +30,6 @@ namespace osu.Game.Rulesets.Taiko public override ScoreProcessor CreateScoreProcessor() => new TaikoScoreProcessor(); - public override HealthProcessor CreateHealthProcessor() => new TaikoHealthProcessor(); - public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new TaikoBeatmapConverter(beatmap, this); diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index 72973cc677..6310b19cd3 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -80,7 +80,7 @@ namespace osu.Game.Rulesets /// Creates a for this . /// /// The health processor. - public virtual HealthProcessor CreateHealthProcessor() => new HealthProcessor(); + public virtual HealthProcessor CreateHealthProcessor(double gameplayStartTime) => new HealthProcessor(gameplayStartTime); /// /// Creates a to convert a to one that is applicable for this . diff --git a/osu.Game/Rulesets/Scoring/HealthProcessor.cs b/osu.Game/Rulesets/Scoring/HealthProcessor.cs index af81f3b8b5..772e317955 100644 --- a/osu.Game/Rulesets/Scoring/HealthProcessor.cs +++ b/osu.Game/Rulesets/Scoring/HealthProcessor.cs @@ -39,14 +39,25 @@ namespace osu.Game.Rulesets.Scoring /// public bool HasFailed { get; private set; } + private readonly double gameplayStartTime; + + private IBeatmap beatmap; + private List<(double time, double health)> healthIncreases; private double targetMinimumHealth; private double drainRate = 1; + public HealthProcessor(double gameplayStartTime) + { + this.gameplayStartTime = gameplayStartTime; + } + public override void ApplyBeatmap(IBeatmap beatmap) { + this.beatmap = beatmap; + healthIncreases = new List<(double time, double health)>(); - targetMinimumHealth = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.DrainRate, 0.95, 0.85, 0.65); + targetMinimumHealth = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.DrainRate, 0.95, 0.6, 0.2); base.ApplyBeatmap(beatmap); @@ -108,11 +119,24 @@ namespace osu.Game.Rulesets.Scoring { double currentHealth = 1; double lowestHealth = 1; + int currentBreak = -1; for (int i = 0; i < healthIncreases.Count; i++) { - var lastTime = i > 0 ? healthIncreases[i - 1].time : 0; + double currentTime = healthIncreases[i].time; + double lastTime = i > 0 ? healthIncreases[i - 1].time : gameplayStartTime; + // Subtract any break time from the duration since the last object + if (beatmap.Breaks.Count > 0) + { + while (currentBreak + 1 < beatmap.Breaks.Count && beatmap.Breaks[currentBreak + 1].EndTime < currentTime) + currentBreak++; + + if (currentBreak >= 0) + lastTime = Math.Max(lastTime, beatmap.Breaks[currentBreak].EndTime); + } + + // Apply health adjustments currentHealth -= (healthIncreases[i].time - lastTime) * drainRate; lowestHealth = Math.Min(lowestHealth, currentHealth); currentHealth = Math.Min(1, currentHealth + healthIncreases[i].health); diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index dc0dd86c51..c56fbaf847 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -135,7 +135,7 @@ namespace osu.Game.Screens.Play ScoreProcessor.ApplyBeatmap(playableBeatmap); ScoreProcessor.Mods.BindTo(Mods); - HealthProcessor = ruleset.CreateHealthProcessor(); + HealthProcessor = ruleset.CreateHealthProcessor(DrawableRuleset.GameplayStartTime); HealthProcessor.ApplyBeatmap(playableBeatmap); if (!ScoreProcessor.Mode.Disabled) From 90a0569660a593e53e27cc1853155a73df09bbf6 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 25 Dec 2019 19:19:57 +0900 Subject: [PATCH 18/54] Tweak drain values --- osu.Game.Rulesets.Osu/Judgements/OsuJudgement.cs | 13 +++++++++++-- osu.Game/Rulesets/Scoring/HealthProcessor.cs | 2 +- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Judgements/OsuJudgement.cs b/osu.Game.Rulesets.Osu/Judgements/OsuJudgement.cs index 7a5b98864c..845ff76ce6 100644 --- a/osu.Game.Rulesets.Osu/Judgements/OsuJudgement.cs +++ b/osu.Game.Rulesets.Osu/Judgements/OsuJudgement.cs @@ -8,6 +8,11 @@ namespace osu.Game.Rulesets.Osu.Judgements { public class OsuJudgement : Judgement { + /// + /// The health increase for a maximum judgement result. + /// + protected const double MAX_HEALTH_INCREASE = 0.05; + public override HitResult MaxResult => HitResult.Great; protected override int NumericResultFor(HitResult result) @@ -33,12 +38,16 @@ namespace osu.Game.Rulesets.Osu.Judgements switch (result) { case HitResult.Miss: - return -0.02; + return -MAX_HEALTH_INCREASE; case HitResult.Meh: + return -MAX_HEALTH_INCREASE * 0.05; + case HitResult.Good: + return MAX_HEALTH_INCREASE * 0.3; + case HitResult.Great: - return 0.01; + return MAX_HEALTH_INCREASE; default: return 0; diff --git a/osu.Game/Rulesets/Scoring/HealthProcessor.cs b/osu.Game/Rulesets/Scoring/HealthProcessor.cs index 772e317955..a1389d7ee9 100644 --- a/osu.Game/Rulesets/Scoring/HealthProcessor.cs +++ b/osu.Game/Rulesets/Scoring/HealthProcessor.cs @@ -57,7 +57,7 @@ namespace osu.Game.Rulesets.Scoring this.beatmap = beatmap; healthIncreases = new List<(double time, double health)>(); - targetMinimumHealth = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.DrainRate, 0.95, 0.6, 0.2); + targetMinimumHealth = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.DrainRate, 0.95, 0.70, 0.30); base.ApplyBeatmap(beatmap); From 0454c5022d997b22835f94f446f5ba9bcf345dfa Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 25 Dec 2019 19:39:59 +0900 Subject: [PATCH 19/54] Fix some maps potentially starting with 0 health --- osu.Game/Rulesets/Scoring/HealthProcessor.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Scoring/HealthProcessor.cs b/osu.Game/Rulesets/Scoring/HealthProcessor.cs index a1389d7ee9..8dd6cc1ed9 100644 --- a/osu.Game/Rulesets/Scoring/HealthProcessor.cs +++ b/osu.Game/Rulesets/Scoring/HealthProcessor.cs @@ -70,7 +70,11 @@ namespace osu.Game.Rulesets.Scoring base.Update(); if (!IsBreakTime.Value) - Health.Value -= drainRate * Time.Elapsed; + { + // When jumping from before the gameplay start to after it or vice-versa, we only want to consider any drain since the gameplay start time + double lastTime = Math.Max(gameplayStartTime, Time.Current - Time.Elapsed); + Health.Value -= drainRate * (Time.Current - lastTime); + } } protected override void ApplyResultInternal(JudgementResult result) From 3b07c3913dd9209be68f7f928cb704ac6c6ba168 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 25 Dec 2019 19:58:23 +0900 Subject: [PATCH 20/54] Add perfect and ok values, move to base Judgement --- .../Judgements/OsuJudgement.cs | 26 --------------- osu.Game/Rulesets/Judgements/Judgement.cs | 33 ++++++++++++++++++- 2 files changed, 32 insertions(+), 27 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Judgements/OsuJudgement.cs b/osu.Game.Rulesets.Osu/Judgements/OsuJudgement.cs index 845ff76ce6..bf30fbc351 100644 --- a/osu.Game.Rulesets.Osu/Judgements/OsuJudgement.cs +++ b/osu.Game.Rulesets.Osu/Judgements/OsuJudgement.cs @@ -8,11 +8,6 @@ namespace osu.Game.Rulesets.Osu.Judgements { public class OsuJudgement : Judgement { - /// - /// The health increase for a maximum judgement result. - /// - protected const double MAX_HEALTH_INCREASE = 0.05; - public override HitResult MaxResult => HitResult.Great; protected override int NumericResultFor(HitResult result) @@ -32,26 +27,5 @@ namespace osu.Game.Rulesets.Osu.Judgements return 300; } } - - protected override double HealthIncreaseFor(HitResult result) - { - switch (result) - { - case HitResult.Miss: - return -MAX_HEALTH_INCREASE; - - case HitResult.Meh: - return -MAX_HEALTH_INCREASE * 0.05; - - case HitResult.Good: - return MAX_HEALTH_INCREASE * 0.3; - - case HitResult.Great: - return MAX_HEALTH_INCREASE; - - default: - return 0; - } - } } } diff --git a/osu.Game/Rulesets/Judgements/Judgement.cs b/osu.Game/Rulesets/Judgements/Judgement.cs index f07f76a2b8..599135ba54 100644 --- a/osu.Game/Rulesets/Judgements/Judgement.cs +++ b/osu.Game/Rulesets/Judgements/Judgement.cs @@ -11,6 +11,12 @@ namespace osu.Game.Rulesets.Judgements /// public class Judgement { + /// + /// The default health increase for a maximum judgement, as a proportion of total health. + /// By default, each maximum judgement restores 5% of total health. + /// + protected const double DEFAULT_MAX_HEALTH_INCREASE = 0.05; + /// /// The maximum that can be achieved. /// @@ -55,7 +61,32 @@ namespace osu.Game.Rulesets.Judgements /// /// The to find the numeric health increase for. /// The numeric health increase of . - protected virtual double HealthIncreaseFor(HitResult result) => 0; + protected virtual double HealthIncreaseFor(HitResult result) + { + switch (result) + { + case HitResult.Miss: + return -DEFAULT_MAX_HEALTH_INCREASE; + + case HitResult.Meh: + return -DEFAULT_MAX_HEALTH_INCREASE * 0.05; + + case HitResult.Ok: + return -DEFAULT_MAX_HEALTH_INCREASE * 0.01; + + case HitResult.Good: + return DEFAULT_MAX_HEALTH_INCREASE * 0.3; + + case HitResult.Great: + return DEFAULT_MAX_HEALTH_INCREASE; + + case HitResult.Perfect: + return DEFAULT_MAX_HEALTH_INCREASE * 1.05; + + default: + return 0; + } + } /// /// Retrieves the numeric health increase of a . From eba6371526095c2e39f850f734c33671a496866c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 25 Dec 2019 21:16:40 +0900 Subject: [PATCH 21/54] Re-implement taiko's accumulating health processor --- .../Scoring/TaikoHealthProcessor.cs | 47 ++++++++ osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 2 + osu.Game/Rulesets/Ruleset.cs | 2 +- .../Scoring/AccumulatingHealthProcessor.cs | 20 ++++ .../Scoring/DrainingHealthProcessor.cs | 108 ++++++++++++++++++ osu.Game/Rulesets/Scoring/HealthProcessor.cs | 99 ++-------------- 6 files changed, 188 insertions(+), 90 deletions(-) create mode 100644 osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs create mode 100644 osu.Game/Rulesets/Scoring/AccumulatingHealthProcessor.cs create mode 100644 osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs diff --git a/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs b/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs new file mode 100644 index 0000000000..679addc32d --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs @@ -0,0 +1,47 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Taiko.Objects; + +namespace osu.Game.Rulesets.Taiko.Scoring +{ + public class TaikoHealthProcessor : AccumulatingHealthProcessor + { + /// + /// A value used for calculating . + /// + private const double object_count_factor = 3; + + /// + /// HP multiplier for a successful . + /// + private double hpMultiplier; + + /// + /// HP multiplier for a . + /// + private double hpMissMultiplier; + + public TaikoHealthProcessor(double gameplayStartTime) + : base(gameplayStartTime) + { + } + + public override void ApplyBeatmap(IBeatmap beatmap) + { + base.ApplyBeatmap(beatmap); + + hpMultiplier = 1 / (object_count_factor * beatmap.HitObjects.OfType().Count() * BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.DrainRate, 0.5, 0.75, 0.98)); + hpMissMultiplier = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.DrainRate, 0.0018, 0.0075, 0.0120); + } + + protected override double GetHealthIncreaseFor(JudgementResult result) + => base.GetHealthIncreaseFor(result) * (result.Type == HitResult.Miss ? hpMissMultiplier : hpMultiplier); + + protected override bool DefaultFailCondition => JudgedHits == MaxHits && Health.Value <= 0.5; + } +} diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index 79de2d0334..de589f81b6 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -30,6 +30,8 @@ namespace osu.Game.Rulesets.Taiko public override ScoreProcessor CreateScoreProcessor() => new TaikoScoreProcessor(); + public override HealthProcessor CreateHealthProcessor(double gameplayStartTime) => new TaikoHealthProcessor(gameplayStartTime); + public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new TaikoBeatmapConverter(beatmap, this); diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index 6310b19cd3..ff58d3c8dc 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -80,7 +80,7 @@ namespace osu.Game.Rulesets /// Creates a for this . /// /// The health processor. - public virtual HealthProcessor CreateHealthProcessor(double gameplayStartTime) => new HealthProcessor(gameplayStartTime); + public virtual HealthProcessor CreateHealthProcessor(double gameplayStartTime) => new DrainingHealthProcessor(gameplayStartTime); /// /// Creates a to convert a to one that is applicable for this . diff --git a/osu.Game/Rulesets/Scoring/AccumulatingHealthProcessor.cs b/osu.Game/Rulesets/Scoring/AccumulatingHealthProcessor.cs new file mode 100644 index 0000000000..edc8d18804 --- /dev/null +++ b/osu.Game/Rulesets/Scoring/AccumulatingHealthProcessor.cs @@ -0,0 +1,20 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Rulesets.Scoring +{ + public class AccumulatingHealthProcessor : HealthProcessor + { + public AccumulatingHealthProcessor(double gameplayStartTime) + : base(gameplayStartTime) + { + } + + protected override void Reset(bool storeResults) + { + base.Reset(storeResults); + + Health.Value = 0; + } + } +} diff --git a/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs b/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs new file mode 100644 index 0000000000..8b30728caf --- /dev/null +++ b/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs @@ -0,0 +1,108 @@ +// 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 osu.Game.Beatmaps; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects; + +namespace osu.Game.Rulesets.Scoring +{ + public class DrainingHealthProcessor : HealthProcessor + { + private IBeatmap beatmap; + + private List<(double time, double health)> healthIncreases; + private double targetMinimumHealth; + private double drainRate = 1; + + public DrainingHealthProcessor(double gameplayStartTime) + : base(gameplayStartTime) + { + } + + protected override void Update() + { + base.Update(); + + if (!IsBreakTime.Value) + { + // When jumping from before the gameplay start to after it or vice-versa, we only want to consider any drain since the gameplay start time + double lastTime = Math.Max(GameplayStartTime, Time.Current - Time.Elapsed); + Health.Value -= drainRate * (Time.Current - lastTime); + } + } + + public override void ApplyBeatmap(IBeatmap beatmap) + { + this.beatmap = beatmap; + + healthIncreases = new List<(double time, double health)>(); + targetMinimumHealth = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.DrainRate, 0.95, 0.70, 0.30); + + base.ApplyBeatmap(beatmap); + + // Only required during the simulation stage + healthIncreases = null; + } + + protected override void ApplyResultInternal(JudgementResult result) + { + base.ApplyResultInternal(result); + healthIncreases?.Add((result.HitObject.GetEndTime() + result.TimeOffset, GetHealthIncreaseFor(result))); + } + + protected override void Reset(bool storeResults) + { + base.Reset(storeResults); + + drainRate = 1; + + if (storeResults) + { + int count = 1; + + while (true) + { + double currentHealth = 1; + double lowestHealth = 1; + int currentBreak = -1; + + for (int i = 0; i < healthIncreases.Count; i++) + { + double currentTime = healthIncreases[i].time; + double lastTime = i > 0 ? healthIncreases[i - 1].time : GameplayStartTime; + + // Subtract any break time from the duration since the last object + if (beatmap.Breaks.Count > 0) + { + while (currentBreak + 1 < beatmap.Breaks.Count && beatmap.Breaks[currentBreak + 1].EndTime < currentTime) + currentBreak++; + + if (currentBreak >= 0) + lastTime = Math.Max(lastTime, beatmap.Breaks[currentBreak].EndTime); + } + + // Apply health adjustments + currentHealth -= (healthIncreases[i].time - lastTime) * drainRate; + lowestHealth = Math.Min(lowestHealth, currentHealth); + currentHealth = Math.Min(1, currentHealth + healthIncreases[i].health); + + // Common scenario for when the drain rate is definitely too harsh + if (lowestHealth < 0) + break; + } + + if (Math.Abs(lowestHealth - targetMinimumHealth) <= 0.01) + break; + + count *= 2; + drainRate += 1.0 / count * Math.Sign(lowestHealth - targetMinimumHealth); + } + } + + healthIncreases.Clear(); + } + } +} diff --git a/osu.Game/Rulesets/Scoring/HealthProcessor.cs b/osu.Game/Rulesets/Scoring/HealthProcessor.cs index 8dd6cc1ed9..597989701d 100644 --- a/osu.Game/Rulesets/Scoring/HealthProcessor.cs +++ b/osu.Game/Rulesets/Scoring/HealthProcessor.cs @@ -2,16 +2,13 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using osu.Framework.Bindables; using osu.Framework.MathUtils; -using osu.Game.Beatmaps; using osu.Game.Rulesets.Judgements; -using osu.Game.Rulesets.Objects; namespace osu.Game.Rulesets.Scoring { - public class HealthProcessor : JudgementProcessor + public abstract class HealthProcessor : JudgementProcessor { /// /// Invoked when the is in a failed state. @@ -39,42 +36,14 @@ namespace osu.Game.Rulesets.Scoring /// public bool HasFailed { get; private set; } - private readonly double gameplayStartTime; + /// + /// The gameplay start time. + /// + protected readonly double GameplayStartTime; - private IBeatmap beatmap; - - private List<(double time, double health)> healthIncreases; - private double targetMinimumHealth; - private double drainRate = 1; - - public HealthProcessor(double gameplayStartTime) + protected HealthProcessor(double gameplayStartTime) { - this.gameplayStartTime = gameplayStartTime; - } - - public override void ApplyBeatmap(IBeatmap beatmap) - { - this.beatmap = beatmap; - - healthIncreases = new List<(double time, double health)>(); - targetMinimumHealth = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.DrainRate, 0.95, 0.70, 0.30); - - base.ApplyBeatmap(beatmap); - - // Only required during the simulation stage - healthIncreases = null; - } - - protected override void Update() - { - base.Update(); - - if (!IsBreakTime.Value) - { - // When jumping from before the gameplay start to after it or vice-versa, we only want to consider any drain since the gameplay start time - double lastTime = Math.Max(gameplayStartTime, Time.Current - Time.Elapsed); - Health.Value -= drainRate * (Time.Current - lastTime); - } + GameplayStartTime = gameplayStartTime; } protected override void ApplyResultInternal(JudgementResult result) @@ -82,13 +51,10 @@ namespace osu.Game.Rulesets.Scoring result.HealthAtJudgement = Health.Value; result.FailedAtJudgement = HasFailed; - double healthIncrease = result.Judgement.HealthIncreaseFor(result); - healthIncreases?.Add((result.HitObject.GetEndTime() + result.TimeOffset, healthIncrease)); - if (HasFailed) return; - Health.Value += healthIncrease; + Health.Value += GetHealthIncreaseFor(result); if (!DefaultFailCondition && FailConditions?.Invoke(this, result) != true) return; @@ -104,6 +70,8 @@ namespace osu.Game.Rulesets.Scoring // Todo: Revert HasFailed state with proper player support } + protected virtual double GetHealthIncreaseFor(JudgementResult result) => result.Judgement.HealthIncreaseFor(result); + /// /// The default conditions for failing. /// @@ -113,53 +81,6 @@ namespace osu.Game.Rulesets.Scoring { base.Reset(storeResults); - drainRate = 1; - - if (storeResults) - { - int count = 1; - - while (true) - { - double currentHealth = 1; - double lowestHealth = 1; - int currentBreak = -1; - - for (int i = 0; i < healthIncreases.Count; i++) - { - double currentTime = healthIncreases[i].time; - double lastTime = i > 0 ? healthIncreases[i - 1].time : gameplayStartTime; - - // Subtract any break time from the duration since the last object - if (beatmap.Breaks.Count > 0) - { - while (currentBreak + 1 < beatmap.Breaks.Count && beatmap.Breaks[currentBreak + 1].EndTime < currentTime) - currentBreak++; - - if (currentBreak >= 0) - lastTime = Math.Max(lastTime, beatmap.Breaks[currentBreak].EndTime); - } - - // Apply health adjustments - currentHealth -= (healthIncreases[i].time - lastTime) * drainRate; - lowestHealth = Math.Min(lowestHealth, currentHealth); - currentHealth = Math.Min(1, currentHealth + healthIncreases[i].health); - - // Common scenario for when the drain rate is definitely too harsh - if (lowestHealth < 0) - break; - } - - if (Math.Abs(lowestHealth - targetMinimumHealth) <= 0.01) - break; - - count *= 2; - drainRate += 1.0 / count * Math.Sign(lowestHealth - targetMinimumHealth); - } - } - - healthIncreases.Clear(); - Health.Value = 1; HasFailed = false; } From e0c1072ab22cd78f002fca32c512236c6b786feb Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 25 Dec 2019 21:22:46 +0900 Subject: [PATCH 22/54] Add xmldocs --- .../Scoring/TaikoHealthProcessor.cs | 3 +-- .../Scoring/AccumulatingHealthProcessor.cs | 16 +++++++++++++++- .../Rulesets/Scoring/DrainingHealthProcessor.cs | 7 +++++++ osu.Game/Rulesets/Scoring/HealthProcessor.cs | 9 +++++++++ 4 files changed, 32 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs b/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs index 679addc32d..85f072fa54 100644 --- a/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs +++ b/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs @@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Taiko.Scoring private double hpMissMultiplier; public TaikoHealthProcessor(double gameplayStartTime) - : base(gameplayStartTime) + : base(gameplayStartTime, 0.5) { } @@ -42,6 +42,5 @@ namespace osu.Game.Rulesets.Taiko.Scoring protected override double GetHealthIncreaseFor(JudgementResult result) => base.GetHealthIncreaseFor(result) * (result.Type == HitResult.Miss ? hpMissMultiplier : hpMultiplier); - protected override bool DefaultFailCondition => JudgedHits == MaxHits && Health.Value <= 0.5; } } diff --git a/osu.Game/Rulesets/Scoring/AccumulatingHealthProcessor.cs b/osu.Game/Rulesets/Scoring/AccumulatingHealthProcessor.cs index edc8d18804..540b4b8a1d 100644 --- a/osu.Game/Rulesets/Scoring/AccumulatingHealthProcessor.cs +++ b/osu.Game/Rulesets/Scoring/AccumulatingHealthProcessor.cs @@ -3,11 +3,25 @@ namespace osu.Game.Rulesets.Scoring { + /// + /// A that accumulates health and causes a fail if the final health + /// is less than a value required to pass the beatmap. + /// public class AccumulatingHealthProcessor : HealthProcessor { - public AccumulatingHealthProcessor(double gameplayStartTime) + protected override bool DefaultFailCondition => JudgedHits == MaxHits && Health.Value < requiredHealth; + + private readonly double requiredHealth; + + /// + /// Creates a new . + /// + /// The gameplay start time. + /// The minimum amount of health required to beatmap. + public AccumulatingHealthProcessor(double gameplayStartTime, double requiredHealth) : base(gameplayStartTime) { + this.requiredHealth = requiredHealth; } protected override void Reset(bool storeResults) diff --git a/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs b/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs index 8b30728caf..df9fba0cca 100644 --- a/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs +++ b/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs @@ -9,6 +9,9 @@ using osu.Game.Rulesets.Objects; namespace osu.Game.Rulesets.Scoring { + /// + /// A which continuously drains health. + /// public class DrainingHealthProcessor : HealthProcessor { private IBeatmap beatmap; @@ -17,6 +20,10 @@ namespace osu.Game.Rulesets.Scoring private double targetMinimumHealth; private double drainRate = 1; + /// + /// Creates a new . + /// + /// The gameplay start time. public DrainingHealthProcessor(double gameplayStartTime) : base(gameplayStartTime) { diff --git a/osu.Game/Rulesets/Scoring/HealthProcessor.cs b/osu.Game/Rulesets/Scoring/HealthProcessor.cs index 597989701d..d8addc2b4d 100644 --- a/osu.Game/Rulesets/Scoring/HealthProcessor.cs +++ b/osu.Game/Rulesets/Scoring/HealthProcessor.cs @@ -41,6 +41,10 @@ namespace osu.Game.Rulesets.Scoring /// protected readonly double GameplayStartTime; + /// + /// Creates a new . + /// + /// The gameplay start time. protected HealthProcessor(double gameplayStartTime) { GameplayStartTime = gameplayStartTime; @@ -70,6 +74,11 @@ namespace osu.Game.Rulesets.Scoring // Todo: Revert HasFailed state with proper player support } + /// + /// Retrieves the health increase for a . + /// + /// The . + /// The health increase. protected virtual double GetHealthIncreaseFor(JudgementResult result) => result.Judgement.HealthIncreaseFor(result); /// From 429272b864b022547c21c488961931b3095c5a57 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 26 Dec 2019 15:28:30 +0900 Subject: [PATCH 23/54] Clean up + document taiko health processor --- osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs b/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs index 85f072fa54..279d119626 100644 --- a/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs +++ b/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs @@ -9,6 +9,10 @@ using osu.Game.Rulesets.Taiko.Objects; namespace osu.Game.Rulesets.Taiko.Scoring { + /// + /// A for the taiko ruleset. + /// Taiko fails if the player has not half-filled their health by the end of the map. + /// public class TaikoHealthProcessor : AccumulatingHealthProcessor { /// @@ -41,6 +45,5 @@ namespace osu.Game.Rulesets.Taiko.Scoring protected override double GetHealthIncreaseFor(JudgementResult result) => base.GetHealthIncreaseFor(result) * (result.Type == HitResult.Miss ? hpMissMultiplier : hpMultiplier); - } } From 662ec2d8122448f30138a18b40b3eb4b88295758 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 26 Dec 2019 15:46:07 +0900 Subject: [PATCH 24/54] Refactor gameplay-time drain limitation --- .../Rulesets/Scoring/DrainingHealthProcessor.cs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs b/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs index df9fba0cca..3735dc75c1 100644 --- a/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs +++ b/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs @@ -16,6 +16,8 @@ namespace osu.Game.Rulesets.Scoring { private IBeatmap beatmap; + private double gameplayEndTime; + private List<(double time, double health)> healthIncreases; private double targetMinimumHealth; private double drainRate = 1; @@ -35,9 +37,11 @@ namespace osu.Game.Rulesets.Scoring if (!IsBreakTime.Value) { - // When jumping from before the gameplay start to after it or vice-versa, we only want to consider any drain since the gameplay start time - double lastTime = Math.Max(GameplayStartTime, Time.Current - Time.Elapsed); - Health.Value -= drainRate * (Time.Current - lastTime); + // When jumping in and out of gameplay time within a single frame, health should only be drained for the period within the gameplay time + double lastGameplayTime = Math.Clamp(Time.Current - Time.Elapsed, GameplayStartTime, gameplayEndTime); + double currentGameplayTime = Math.Clamp(Time.Current, GameplayStartTime, gameplayEndTime); + + Health.Value -= drainRate * (currentGameplayTime - lastGameplayTime); } } @@ -45,6 +49,9 @@ namespace osu.Game.Rulesets.Scoring { this.beatmap = beatmap; + if (beatmap.HitObjects.Count > 0) + gameplayEndTime = beatmap.HitObjects[^1].GetEndTime(); + healthIncreases = new List<(double time, double health)>(); targetMinimumHealth = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.DrainRate, 0.95, 0.70, 0.30); From f5dbd57d55cb285b04bf367b27476dce54411157 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 26 Dec 2019 17:36:40 +0900 Subject: [PATCH 25/54] Refactor drain calculation for resiliency --- .../Scoring/DrainingHealthProcessor.cs | 79 +++++++++++-------- 1 file changed, 47 insertions(+), 32 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs b/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs index 3735dc75c1..e7e8656efb 100644 --- a/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs +++ b/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs @@ -14,8 +14,12 @@ namespace osu.Game.Rulesets.Scoring /// public class DrainingHealthProcessor : HealthProcessor { - private IBeatmap beatmap; + /// + /// A reasonable allowable error for the minimum health offset from . A 1% error is unnoticeable. + /// + private const double minimum_health_error = 0.01; + private IBeatmap beatmap; private double gameplayEndTime; private List<(double time, double health)> healthIncreases; @@ -74,49 +78,60 @@ namespace osu.Game.Rulesets.Scoring drainRate = 1; if (storeResults) + drainRate = computeDrainRate(); + } + + private double computeDrainRate() + { + if (healthIncreases == null || healthIncreases.Count == 0) + return 0; + + int adjustment = 1; + double result = 1; + + // Although we expect the following loop to converge within 30 iterations (health within 1/2^31 accuracy of the target), + // we'll still keep a safety measure to avoid infinite loops by detecting overflows. + while (adjustment > 0) { - int count = 1; + double currentHealth = 1; + double lowestHealth = 1; + int currentBreak = -1; - while (true) + for (int i = 0; i < healthIncreases.Count; i++) { - double currentHealth = 1; - double lowestHealth = 1; - int currentBreak = -1; + double currentTime = healthIncreases[i].time; + double lastTime = i > 0 ? healthIncreases[i - 1].time : GameplayStartTime; - for (int i = 0; i < healthIncreases.Count; i++) + // Subtract any break time from the duration since the last object + if (beatmap.Breaks.Count > 0) { - double currentTime = healthIncreases[i].time; - double lastTime = i > 0 ? healthIncreases[i - 1].time : GameplayStartTime; + while (currentBreak + 1 < beatmap.Breaks.Count && beatmap.Breaks[currentBreak + 1].EndTime < currentTime) + currentBreak++; - // Subtract any break time from the duration since the last object - if (beatmap.Breaks.Count > 0) - { - while (currentBreak + 1 < beatmap.Breaks.Count && beatmap.Breaks[currentBreak + 1].EndTime < currentTime) - currentBreak++; - - if (currentBreak >= 0) - lastTime = Math.Max(lastTime, beatmap.Breaks[currentBreak].EndTime); - } - - // Apply health adjustments - currentHealth -= (healthIncreases[i].time - lastTime) * drainRate; - lowestHealth = Math.Min(lowestHealth, currentHealth); - currentHealth = Math.Min(1, currentHealth + healthIncreases[i].health); - - // Common scenario for when the drain rate is definitely too harsh - if (lowestHealth < 0) - break; + if (currentBreak >= 0) + lastTime = Math.Max(lastTime, beatmap.Breaks[currentBreak].EndTime); } - if (Math.Abs(lowestHealth - targetMinimumHealth) <= 0.01) - break; + // Apply health adjustments + currentHealth -= (healthIncreases[i].time - lastTime) * result; + lowestHealth = Math.Min(lowestHealth, currentHealth); + currentHealth = Math.Min(1, currentHealth + healthIncreases[i].health); - count *= 2; - drainRate += 1.0 / count * Math.Sign(lowestHealth - targetMinimumHealth); + // Common scenario for when the drain rate is definitely too harsh + if (lowestHealth < 0) + break; } + + // Stop if the resulting health is within a reasonable offset from the target + if (Math.Abs(lowestHealth - targetMinimumHealth) <= minimum_health_error) + break; + + // This effectively works like a binary search - each iteration the search space moves closer to the the target, but may exceed it. + adjustment *= 2; + result += 1.0 / adjustment * Math.Sign(lowestHealth - targetMinimumHealth); } - healthIncreases.Clear(); + return result; } } } From 7ee0370d237a25f145ce224c202900e566589e44 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 26 Dec 2019 17:48:46 +0900 Subject: [PATCH 26/54] Add some tests --- .../TestSceneDrainingHealthProcessor.cs | 149 ++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs diff --git a/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs b/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs new file mode 100644 index 0000000000..4996c86e5d --- /dev/null +++ b/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs @@ -0,0 +1,149 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.MathUtils; +using osu.Framework.Testing; +using osu.Framework.Timing; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Scoring; +using osu.Game.Tests.Visual; + +namespace osu.Game.Tests.Gameplay +{ + [HeadlessTest] + public class TestSceneDrainingHealthProcessor : OsuTestScene + { + private Bindable breakTime; + private HealthProcessor processor; + private ManualClock clock; + + [Test] + public void TestInitialHealthStartsAtOne() + { + createProcessor(createBeatmap(1000, 2000)); + + assertHealthEqualTo(1); + } + + [Test] + public void TestHealthNotDrainedBeforeGameplayStart() + { + createProcessor(createBeatmap(1000, 2000)); + + setTime(100); + assertHealthEqualTo(1); + setTime(900); + assertHealthEqualTo(1); + } + + [Test] + public void TestHealthNotDrainedAfterGameplayEnd() + { + createProcessor(createBeatmap(1000, 2000)); + setTime(2001); // After the hitobjects + setHealth(1); // Reset the current health for assertions to take place + + setTime(2100); + assertHealthEqualTo(1); + setTime(3000); + assertHealthEqualTo(1); + } + + [Test] + public void TestHealthNotDrainedDuringBreak() + { + createProcessor(createBeatmap(0, 2000)); + setBreak(true); + + setTime(700); + assertHealthEqualTo(1); + setTime(900); + assertHealthEqualTo(1); + } + + [Test] + public void TestHealthDrainedDuringGameplay() + { + createProcessor(createBeatmap(0, 1000)); + + setTime(500); + assertHealthNotEqualTo(1); + } + + [Test] + public void TestHealthGainedOnHit() + { + Beatmap beatmap = createBeatmap(0, 1000); + + createProcessor(beatmap); + setTime(10); // Decrease health slightly + assertHealthNotEqualTo(1); + + AddStep("apply hit result", () => processor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], new Judgement()) { Type = HitResult.Perfect })); + assertHealthEqualTo(1); + } + + [Test] + public void TestHealthRemovedOnRevert() + { + var beatmap = createBeatmap(0, 1000); + JudgementResult result = null; + + createProcessor(beatmap); + setTime(10); // Decrease health slightly + AddStep("apply hit result", () => processor.ApplyResult(result = new JudgementResult(beatmap.HitObjects[0], new Judgement()) { Type = HitResult.Perfect })); + + AddStep("revert hit result", () => processor.RevertResult(result)); + assertHealthNotEqualTo(1); + } + + private Beatmap createBeatmap(double startTime, double endTime) + { + var beatmap = new Beatmap + { + BeatmapInfo = { BaseDifficulty = { DrainRate = 5 } }, + }; + + for (double time = startTime; time <= endTime; time += 100) + beatmap.HitObjects.Add(new JudgeableHitObject { StartTime = time }); + + return beatmap; + } + + private void createProcessor(Beatmap beatmap) => AddStep("create processor", () => + { + breakTime = new Bindable(); + + Child = processor = new DrainingHealthProcessor(beatmap.HitObjects[0].StartTime).With(d => + { + d.RelativeSizeAxes = Axes.Both; + d.Clock = new FramedClock(clock = new ManualClock()); + }); + + processor.IsBreakTime.BindTo(breakTime); + processor.ApplyBeatmap(beatmap); + }); + + private void setTime(double time) => AddStep($"set time = {time}", () => clock.CurrentTime = time); + + private void setHealth(double health) => AddStep($"set health = {health}", () => processor.Health.Value = health); + + private void setBreak(bool enabled) => AddStep($"{(enabled ? "enable" : "disable")} break", () => breakTime.Value = enabled); + + private void assertHealthEqualTo(double value) + => AddAssert($"health = {value}", () => Precision.AlmostEquals(value, processor.Health.Value, 0.0001f)); + + private void assertHealthNotEqualTo(double value) + => AddAssert($"health != {value}", () => !Precision.AlmostEquals(value, processor.Health.Value, 0.0001f)); + + private class JudgeableHitObject : HitObject + { + public override Judgement CreateJudgement() => new Judgement(); + } + } +} From 35f64d9aaea7729829fc0233792b93f3484b7c9c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 26 Dec 2019 17:54:31 +0900 Subject: [PATCH 27/54] More comments --- osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs b/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs index e7e8656efb..96242205d4 100644 --- a/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs +++ b/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs @@ -105,6 +105,7 @@ namespace osu.Game.Rulesets.Scoring // Subtract any break time from the duration since the last object if (beatmap.Breaks.Count > 0) { + // Advance the last break occuring before the current time while (currentBreak + 1 < beatmap.Breaks.Count && beatmap.Breaks[currentBreak + 1].EndTime < currentTime) currentBreak++; @@ -126,7 +127,7 @@ namespace osu.Game.Rulesets.Scoring if (Math.Abs(lowestHealth - targetMinimumHealth) <= minimum_health_error) break; - // This effectively works like a binary search - each iteration the search space moves closer to the the target, but may exceed it. + // This effectively works like a binary search - each iteration the search space moves closer to the target, but may exceed it. adjustment *= 2; result += 1.0 / adjustment * Math.Sign(lowestHealth - targetMinimumHealth); } From a4d4efc312317f2a51e5a5624ee4901b582dc403 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 26 Dec 2019 19:33:59 +0900 Subject: [PATCH 28/54] Fix missing comments --- osu.Game/Rulesets/Mods/IApplicableToDifficulty.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Mods/IApplicableToDifficulty.cs b/osu.Game/Rulesets/Mods/IApplicableToDifficulty.cs index d1d85f5aca..34198da722 100644 --- a/osu.Game/Rulesets/Mods/IApplicableToDifficulty.cs +++ b/osu.Game/Rulesets/Mods/IApplicableToDifficulty.cs @@ -14,13 +14,13 @@ namespace osu.Game.Rulesets.Mods /// Called when a beatmap is changed. Can be used to read default values. /// Any changes made will not be preserved. /// - /// + /// The difficulty to read from. void ReadFromDifficulty(BeatmapDifficulty difficulty); /// /// Called post beatmap conversion. Can be used to apply changes to difficulty attributes. /// - /// + /// The difficulty to mutate. void ApplyToDifficulty(BeatmapDifficulty difficulty); } } From 005ec4b3733cdb3a8197efa9e45dc9f4129c9a08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 26 Dec 2019 20:17:51 +0100 Subject: [PATCH 29/54] Demonstrate bug in scrolling container scene Modify TestSceneScrollingHitObjects to showcase the effect of origin choice on object lifetime for all four scrolling directions. --- .../Visual/Gameplay/TestSceneScrollingHitObjects.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs index aa80819694..7ab53b2d65 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs @@ -45,7 +45,7 @@ namespace osu.Game.Tests.Visual.Gameplay RelativeSizeAxes = Axes.Both, Child = playfields[0] = new TestPlayfield() }, - scrollContainers[1] = new ScrollingTestContainer(ScrollingDirection.Up) + scrollContainers[1] = new ScrollingTestContainer(ScrollingDirection.Down) { RelativeSizeAxes = Axes.Both, Child = playfields[1] = new TestPlayfield() @@ -53,12 +53,12 @@ namespace osu.Game.Tests.Visual.Gameplay }, new Drawable[] { - scrollContainers[2] = new ScrollingTestContainer(ScrollingDirection.Up) + scrollContainers[2] = new ScrollingTestContainer(ScrollingDirection.Left) { RelativeSizeAxes = Axes.Both, Child = playfields[2] = new TestPlayfield() }, - scrollContainers[3] = new ScrollingTestContainer(ScrollingDirection.Up) + scrollContainers[3] = new ScrollingTestContainer(ScrollingDirection.Right) { RelativeSizeAxes = Axes.Both, Child = playfields[3] = new TestPlayfield() @@ -207,7 +207,9 @@ namespace osu.Game.Tests.Visual.Gameplay public TestDrawableHitObject(double time) : base(new HitObject { StartTime = time }) { - Origin = Anchor.Centre; + Origin = Anchor.Custom; + OriginPosition = new Vector2(75 / 4.0f); + AutoSizeAxes = Axes.Both; AddInternal(new Box { Size = new Vector2(75) }); From 193e41f8789c5f1da4acc038569fca9f1a858327 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 26 Dec 2019 20:23:16 +0100 Subject: [PATCH 30/54] Add origin adjustment for hitobject lifetime Visual inspection of taiko gameplay has shown that hitobjects appeared on screen only when the origin of the hitobject came into the bounds of the screen, instead of appearing when any visible part of the hitobject came into the screen bounds. This behaviour was due to lifetime calculation being based on the origin of the hitobject and not taking into account the actual object dimensions. Adjust the lifetime start of the hitobject by subtracting the time needed to show the part of the hitobject that should already be visible on screen when the origin comes into frame. --- .../Scrolling/ScrollingHitObjectContainer.cs | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs index 857929ff9e..04b4374fc4 100644 --- a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs @@ -100,7 +100,7 @@ namespace osu.Game.Rulesets.UI.Scrolling private void computeLifetimeStartRecursive(DrawableHitObject hitObject) { - hitObject.LifetimeStart = scrollingInfo.Algorithm.GetDisplayStartTime(hitObject.HitObject.StartTime, timeRange.Value); + hitObject.LifetimeStart = computeOriginAdjustedLifetimeStart(hitObject); foreach (var obj in hitObject.NestedHitObjects) computeLifetimeStartRecursive(obj); @@ -108,6 +108,35 @@ namespace osu.Game.Rulesets.UI.Scrolling private readonly Dictionary hitObjectInitialStateCache = new Dictionary(); + private double computeOriginAdjustedLifetimeStart(DrawableHitObject hitObject) + { + float originAdjustment = 0.0f; + + // calculate the dimension of the part of the hitobject that should already be visible + // when the hitobject origin first appears inside the scrolling container + switch (direction.Value) + { + case ScrollingDirection.Up: + originAdjustment = hitObject.OriginPosition.Y; + break; + + case ScrollingDirection.Down: + originAdjustment = hitObject.DrawHeight - hitObject.OriginPosition.Y; + break; + + case ScrollingDirection.Left: + originAdjustment = hitObject.OriginPosition.X; + break; + + case ScrollingDirection.Right: + originAdjustment = hitObject.DrawWidth - hitObject.OriginPosition.X; + break; + } + + var adjustedStartTime = scrollingInfo.Algorithm.TimeAt(-originAdjustment, hitObject.HitObject.StartTime, timeRange.Value, scrollLength); + return scrollingInfo.Algorithm.GetDisplayStartTime(adjustedStartTime, timeRange.Value); + } + // Cant use AddOnce() since the delegate is re-constructed every invocation private void computeInitialStateRecursive(DrawableHitObject hitObject) => hitObject.Schedule(() => { From fbbd16b4f02e9a4fbda6ee9d511e6d846c0d95fc Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 27 Dec 2019 12:39:25 +0900 Subject: [PATCH 31/54] Remove healthIncreases nullability --- osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs b/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs index 96242205d4..ba29a9a0ce 100644 --- a/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs +++ b/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs @@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Scoring private IBeatmap beatmap; private double gameplayEndTime; - private List<(double time, double health)> healthIncreases; + private readonly List<(double time, double health)> healthIncreases = new List<(double, double)>(); private double targetMinimumHealth; private double drainRate = 1; @@ -56,19 +56,15 @@ namespace osu.Game.Rulesets.Scoring if (beatmap.HitObjects.Count > 0) gameplayEndTime = beatmap.HitObjects[^1].GetEndTime(); - healthIncreases = new List<(double time, double health)>(); targetMinimumHealth = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.DrainRate, 0.95, 0.70, 0.30); base.ApplyBeatmap(beatmap); - - // Only required during the simulation stage - healthIncreases = null; } protected override void ApplyResultInternal(JudgementResult result) { base.ApplyResultInternal(result); - healthIncreases?.Add((result.HitObject.GetEndTime() + result.TimeOffset, GetHealthIncreaseFor(result))); + healthIncreases.Add((result.HitObject.GetEndTime() + result.TimeOffset, GetHealthIncreaseFor(result))); } protected override void Reset(bool storeResults) @@ -79,11 +75,13 @@ namespace osu.Game.Rulesets.Scoring if (storeResults) drainRate = computeDrainRate(); + + healthIncreases.Clear(); } private double computeDrainRate() { - if (healthIncreases == null || healthIncreases.Count == 0) + if (healthIncreases.Count == 0) return 0; int adjustment = 1; From c22744de2cc8df0ef116cfc4ca32f63ed25506b7 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 27 Dec 2019 12:46:35 +0900 Subject: [PATCH 32/54] Better define minimum health targets --- .../Scoring/DrainingHealthProcessor.cs | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs b/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs index ba29a9a0ce..34ede153dd 100644 --- a/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs +++ b/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs @@ -10,7 +10,10 @@ using osu.Game.Rulesets.Objects; namespace osu.Game.Rulesets.Scoring { /// - /// A which continuously drains health. + /// A which continuously drains health.
+ /// At HP=0, the minimum health reached for a perfect play is 95%.
+ /// At HP=5, the minimum health reached for a perfect play is 70%.
+ /// At HP=10, the minimum health reached for a perfect play is 30%. ///
public class DrainingHealthProcessor : HealthProcessor { @@ -19,6 +22,21 @@ namespace osu.Game.Rulesets.Scoring /// private const double minimum_health_error = 0.01; + /// + /// The minimum health target at an HP drain rate of 0. + /// + private const double min_health_target = 0.95; + + /// + /// The minimum health target at an HP drain rate of 5. + /// + private const double mid_health_target = 0.70; + + /// + /// The minimum health target at an HP drain rate of 10. + /// + private const double max_health_target = 0.30; + private IBeatmap beatmap; private double gameplayEndTime; @@ -56,7 +74,7 @@ namespace osu.Game.Rulesets.Scoring if (beatmap.HitObjects.Count > 0) gameplayEndTime = beatmap.HitObjects[^1].GetEndTime(); - targetMinimumHealth = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.DrainRate, 0.95, 0.70, 0.30); + targetMinimumHealth = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.DrainRate, min_health_target, mid_health_target, max_health_target); base.ApplyBeatmap(beatmap); } From 35cdd67c4a184b1284e41e71205c54ac81a82c1c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 27 Dec 2019 12:47:23 +0900 Subject: [PATCH 33/54] Remove instantiation of non-existing processor --- osu.Game.Rulesets.Osu/OsuRuleset.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 203e37f91a..36346eb78a 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -38,8 +38,6 @@ namespace osu.Game.Rulesets.Osu public override ScoreProcessor CreateScoreProcessor() => new OsuScoreProcessor(); - public override HealthProcessor CreateHealthProcessor(IBeatmap beatmap) => new OsuHealthProcessor(beatmap); - public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new OsuBeatmapConverter(beatmap, this); public override IBeatmapProcessor CreateBeatmapProcessor(IBeatmap beatmap) => new OsuBeatmapProcessor(beatmap); From b330aec03e54447b0788c2029d13d55b1db70e6d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 27 Dec 2019 16:14:49 +0900 Subject: [PATCH 34/54] Drain starting at the first hitobject, not gameplay start --- .../Scoring/TaikoHealthProcessor.cs | 4 ++-- osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 2 +- osu.Game/Rulesets/Ruleset.cs | 2 +- .../Scoring/AccumulatingHealthProcessor.cs | 4 +--- .../Rulesets/Scoring/DrainingHealthProcessor.cs | 15 +++++++++------ osu.Game/Rulesets/Scoring/HealthProcessor.cs | 14 -------------- osu.Game/Screens/Play/Player.cs | 2 +- 7 files changed, 15 insertions(+), 28 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs b/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs index 279d119626..edb089dbac 100644 --- a/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs +++ b/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs @@ -30,8 +30,8 @@ namespace osu.Game.Rulesets.Taiko.Scoring /// private double hpMissMultiplier; - public TaikoHealthProcessor(double gameplayStartTime) - : base(gameplayStartTime, 0.5) + public TaikoHealthProcessor() + : base(0.5) { } diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index 371236e47a..777b68a993 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Taiko public override ScoreProcessor CreateScoreProcessor() => new TaikoScoreProcessor(); - public override HealthProcessor CreateHealthProcessor(double gameplayStartTime) => new TaikoHealthProcessor(gameplayStartTime); + public override HealthProcessor CreateHealthProcessor(double drainStartTime) => new TaikoHealthProcessor(); public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new TaikoBeatmapConverter(beatmap, this); diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index ff58d3c8dc..67ec6d15ea 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -80,7 +80,7 @@ namespace osu.Game.Rulesets /// Creates a for this . /// /// The health processor. - public virtual HealthProcessor CreateHealthProcessor(double gameplayStartTime) => new DrainingHealthProcessor(gameplayStartTime); + public virtual HealthProcessor CreateHealthProcessor(double drainStartTime) => new DrainingHealthProcessor(drainStartTime); /// /// Creates a to convert a to one that is applicable for this . diff --git a/osu.Game/Rulesets/Scoring/AccumulatingHealthProcessor.cs b/osu.Game/Rulesets/Scoring/AccumulatingHealthProcessor.cs index 540b4b8a1d..5dfb5167f4 100644 --- a/osu.Game/Rulesets/Scoring/AccumulatingHealthProcessor.cs +++ b/osu.Game/Rulesets/Scoring/AccumulatingHealthProcessor.cs @@ -16,10 +16,8 @@ namespace osu.Game.Rulesets.Scoring /// /// Creates a new . /// - /// The gameplay start time. /// The minimum amount of health required to beatmap. - public AccumulatingHealthProcessor(double gameplayStartTime, double requiredHealth) - : base(gameplayStartTime) + public AccumulatingHealthProcessor(double requiredHealth) { this.requiredHealth = requiredHealth; } diff --git a/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs b/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs index 34ede153dd..fffcbb3c9f 100644 --- a/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs +++ b/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs @@ -38,8 +38,11 @@ namespace osu.Game.Rulesets.Scoring private const double max_health_target = 0.30; private IBeatmap beatmap; + private double gameplayEndTime; + private readonly double drainStartTime; + private readonly List<(double time, double health)> healthIncreases = new List<(double, double)>(); private double targetMinimumHealth; private double drainRate = 1; @@ -47,10 +50,10 @@ namespace osu.Game.Rulesets.Scoring /// /// Creates a new . /// - /// The gameplay start time. - public DrainingHealthProcessor(double gameplayStartTime) - : base(gameplayStartTime) + /// The time after which draining should begin. + public DrainingHealthProcessor(double drainStartTime) { + this.drainStartTime = drainStartTime; } protected override void Update() @@ -60,8 +63,8 @@ namespace osu.Game.Rulesets.Scoring if (!IsBreakTime.Value) { // When jumping in and out of gameplay time within a single frame, health should only be drained for the period within the gameplay time - double lastGameplayTime = Math.Clamp(Time.Current - Time.Elapsed, GameplayStartTime, gameplayEndTime); - double currentGameplayTime = Math.Clamp(Time.Current, GameplayStartTime, gameplayEndTime); + double lastGameplayTime = Math.Clamp(Time.Current - Time.Elapsed, drainStartTime, gameplayEndTime); + double currentGameplayTime = Math.Clamp(Time.Current, drainStartTime, gameplayEndTime); Health.Value -= drainRate * (currentGameplayTime - lastGameplayTime); } @@ -116,7 +119,7 @@ namespace osu.Game.Rulesets.Scoring for (int i = 0; i < healthIncreases.Count; i++) { double currentTime = healthIncreases[i].time; - double lastTime = i > 0 ? healthIncreases[i - 1].time : GameplayStartTime; + double lastTime = i > 0 ? healthIncreases[i - 1].time : drainStartTime; // Subtract any break time from the duration since the last object if (beatmap.Breaks.Count > 0) diff --git a/osu.Game/Rulesets/Scoring/HealthProcessor.cs b/osu.Game/Rulesets/Scoring/HealthProcessor.cs index d8addc2b4d..0c6b3f67b4 100644 --- a/osu.Game/Rulesets/Scoring/HealthProcessor.cs +++ b/osu.Game/Rulesets/Scoring/HealthProcessor.cs @@ -36,20 +36,6 @@ namespace osu.Game.Rulesets.Scoring /// public bool HasFailed { get; private set; } - /// - /// The gameplay start time. - /// - protected readonly double GameplayStartTime; - - /// - /// Creates a new . - /// - /// The gameplay start time. - protected HealthProcessor(double gameplayStartTime) - { - GameplayStartTime = gameplayStartTime; - } - protected override void ApplyResultInternal(JudgementResult result) { result.HealthAtJudgement = Health.Value; diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index a3d512cdaa..7228e22382 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -135,7 +135,7 @@ namespace osu.Game.Screens.Play ScoreProcessor.ApplyBeatmap(playableBeatmap); ScoreProcessor.Mods.BindTo(Mods); - HealthProcessor = ruleset.CreateHealthProcessor(DrawableRuleset.GameplayStartTime); + HealthProcessor = ruleset.CreateHealthProcessor(playableBeatmap.HitObjects[0].StartTime); HealthProcessor.ApplyBeatmap(playableBeatmap); if (!ScoreProcessor.Mode.Disabled) From 7507fee9f9657479602104aa17de5926aaa373ad Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 27 Dec 2019 16:55:46 +0900 Subject: [PATCH 35/54] Add test for HP rewind --- .../Gameplay/TestSceneDrainingHealthProcessor.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs b/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs index 4996c86e5d..eec52669ff 100644 --- a/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs +++ b/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs @@ -75,6 +75,16 @@ namespace osu.Game.Tests.Gameplay assertHealthNotEqualTo(1); } + [Test] + public void TestHealthGainedAfterRewind() + { + createProcessor(createBeatmap(0, 1000)); + setTime(500); + + setTime(0); + assertHealthEqualTo(1); + } + [Test] public void TestHealthGainedOnHit() { From 2c8879f0fb7f2f754725274821a707ff95c77af1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 27 Dec 2019 19:05:17 +0900 Subject: [PATCH 36/54] Lock user adjusted difficulty settings when changing beatmap --- .../Mods/CatchModDifficultyAdjust.cs | 4 +-- .../Mods/OsuModDifficultyAdjust.cs | 4 +-- osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs | 30 +++++++++++++++++-- 3 files changed, 31 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs index 4c0f5d510e..8377b3786a 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs @@ -34,8 +34,8 @@ namespace osu.Game.Rulesets.Catch.Mods { base.TransferSettings(difficulty); - CircleSize.Value = CircleSize.Default = difficulty.CircleSize; - ApproachRate.Value = ApproachRate.Default = difficulty.ApproachRate; + TransferSetting(CircleSize, difficulty.CircleSize); + TransferSetting(ApproachRate, difficulty.ApproachRate); } protected override void ApplySettings(BeatmapDifficulty difficulty) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs index 0514e2ab34..7eee71be81 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs @@ -34,8 +34,8 @@ namespace osu.Game.Rulesets.Osu.Mods { base.TransferSettings(difficulty); - CircleSize.Value = CircleSize.Default = difficulty.CircleSize; - ApproachRate.Value = ApproachRate.Default = difficulty.ApproachRate; + TransferSetting(CircleSize, difficulty.CircleSize); + TransferSetting(ApproachRate, difficulty.ApproachRate); } protected override void ApplySettings(BeatmapDifficulty difficulty) diff --git a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs index 7b8d4bb3df..1c26e5a720 100644 --- a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs @@ -5,6 +5,7 @@ using osu.Game.Beatmaps; using osu.Framework.Bindables; using osu.Framework.Graphics.Sprites; using System; +using System.Collections.Generic; using osu.Game.Configuration; namespace osu.Game.Rulesets.Mods @@ -51,8 +52,8 @@ namespace osu.Game.Rulesets.Mods { if (this.difficulty == null || this.difficulty.ID != difficulty.ID) { - this.difficulty = difficulty; TransferSettings(difficulty); + this.difficulty = difficulty; } } @@ -64,8 +65,31 @@ namespace osu.Game.Rulesets.Mods /// The beatmap's initial values. protected virtual void TransferSettings(BeatmapDifficulty difficulty) { - DrainRate.Value = DrainRate.Default = difficulty.DrainRate; - OverallDifficulty.Value = OverallDifficulty.Default = difficulty.OverallDifficulty; + TransferSetting(DrainRate, difficulty.DrainRate); + TransferSetting(OverallDifficulty, difficulty.OverallDifficulty); + } + + private readonly Dictionary userChangedSettings = new Dictionary(); + + /// + /// Transfer a setting from to a configuration bindable. + /// Only performs the transfer if the user it not currently overriding.. + /// + protected void TransferSetting(BindableNumber bindable, T beatmapDefault) + where T : struct, IComparable, IConvertible, IEquatable + { + bindable.UnbindEvents(); + + userChangedSettings.TryAdd(bindable, false); + + // users generally choose a difficulty setting and want it to stick across multiple beatmap changes. + // we only want to value transfer if the user hasn't changed the value previously. + if (!userChangedSettings[bindable]) + { + bindable.Value = bindable.Default = beatmapDefault; + } + + bindable.ValueChanged += _ => userChangedSettings[bindable] = !bindable.IsDefault; } /// From ca862124a5b7cae97ddba65176e92003e77400c5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 27 Dec 2019 19:36:48 +0900 Subject: [PATCH 37/54] Fix replay import from main menu causing a hard crash --- osu.Game/Screens/Play/PlayerLoader.cs | 30 ++++++++++----------- osu.Game/Screens/Play/ReplayPlayerLoader.cs | 8 +++--- 2 files changed, 18 insertions(+), 20 deletions(-) diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 57021dfc68..64fcc48004 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -118,8 +118,6 @@ namespace osu.Game.Screens.Play }, idleTracker = new IdleTracker(750) }); - - loadNewPlayer(); } protected override void LoadComplete() @@ -127,6 +125,21 @@ namespace osu.Game.Screens.Play base.LoadComplete(); inputManager = GetContainingInputManager(); + } + + public override void OnEntering(IScreen last) + { + base.OnEntering(last); + + loadNewPlayer(); + + content.ScaleTo(0.7f); + Background?.FadeColour(Color4.White, 800, Easing.OutQuint); + + contentIn(); + + info.Delay(750).FadeIn(500); + this.Delay(1800).Schedule(pushWhenLoaded); if (!muteWarningShownOnce.Value) { @@ -179,19 +192,6 @@ namespace osu.Game.Screens.Play content.FadeOut(250); } - public override void OnEntering(IScreen last) - { - base.OnEntering(last); - - content.ScaleTo(0.7f); - Background?.FadeColour(Color4.White, 800, Easing.OutQuint); - - contentIn(); - - info.Delay(750).FadeIn(500); - this.Delay(1800).Schedule(pushWhenLoaded); - } - protected override void LogoArriving(OsuLogo logo, bool resuming) { base.LogoArriving(logo, resuming); diff --git a/osu.Game/Screens/Play/ReplayPlayerLoader.cs b/osu.Game/Screens/Play/ReplayPlayerLoader.cs index c8ca604902..4572570437 100644 --- a/osu.Game/Screens/Play/ReplayPlayerLoader.cs +++ b/osu.Game/Screens/Play/ReplayPlayerLoader.cs @@ -2,7 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; -using osu.Framework.Allocation; +using osu.Framework.Screens; using osu.Game.Scoring; namespace osu.Game.Screens.Play @@ -20,15 +20,13 @@ namespace osu.Game.Screens.Play scoreInfo = score.ScoreInfo; } - protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) + public override void OnEntering(IScreen last) { - var dependencies = base.CreateChildDependencies(parent); - // these will be reverted thanks to PlayerLoader's lease. Mods.Value = scoreInfo.Mods; Ruleset.Value = scoreInfo.Ruleset; - return dependencies; + base.OnEntering(last); } } } From 651abd2e119520b9ff7d46f277040722342e8a33 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 27 Dec 2019 21:51:18 +0900 Subject: [PATCH 38/54] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index a43c834406..6ff9416e47 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -54,6 +54,6 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 3f64ce8dfd..806aadde84 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -23,7 +23,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index c2d4e3ad8e..230ff01cce 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -74,7 +74,7 @@ - + @@ -82,7 +82,7 @@ - + From 5efb7e801552d4a61bb407ad54728d05f9ab2c0d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 27 Dec 2019 23:01:52 +0900 Subject: [PATCH 39/54] Always update default value --- osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs index 1c26e5a720..c5b8a1bc73 100644 --- a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs @@ -82,12 +82,12 @@ namespace osu.Game.Rulesets.Mods userChangedSettings.TryAdd(bindable, false); + bindable.Default = beatmapDefault; + // users generally choose a difficulty setting and want it to stick across multiple beatmap changes. // we only want to value transfer if the user hasn't changed the value previously. if (!userChangedSettings[bindable]) - { - bindable.Value = bindable.Default = beatmapDefault; - } + bindable.Value = beatmapDefault; bindable.ValueChanged += _ => userChangedSettings[bindable] = !bindable.IsDefault; } From e2a55b79cac9c4bc9c5a95dbf2122baef3fd3f3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 27 Dec 2019 16:47:46 +0100 Subject: [PATCH 40/54] Refactor scrolling hit object scene To better demonstrate the desired effect of the fix introduced in 193e41f, refactor TestSceneScrollingHitObjects to contain two tests, one of which contains the pre-existing controls to test scroll algorithms, and the other aims to showcase the fix by setting scroll parameters appropriately. --- .../Gameplay/TestSceneScrollingHitObjects.cs | 60 +++++++++++++------ 1 file changed, 42 insertions(+), 18 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs index 7ab53b2d65..922001f3dd 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs @@ -3,12 +3,14 @@ using System; using System.Collections.Generic; +using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Threading; using osu.Game.Configuration; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; @@ -28,12 +30,16 @@ namespace osu.Game.Tests.Visual.Gameplay [Cached(typeof(IReadOnlyList))] private IReadOnlyList mods { get; set; } = Array.Empty(); + private const int spawn_interval = 5000; + private readonly ScrollingTestContainer[] scrollContainers = new ScrollingTestContainer[4]; private readonly TestPlayfield[] playfields = new TestPlayfield[4]; + private ScheduledDelegate hitObjectSpawnDelegate; - public TestSceneScrollingHitObjects() + [SetUp] + public void Setup() { - Add(new GridContainer + Child = new GridContainer { RelativeSizeAxes = Axes.Both, Content = new[] @@ -43,12 +49,14 @@ namespace osu.Game.Tests.Visual.Gameplay scrollContainers[0] = new ScrollingTestContainer(ScrollingDirection.Up) { RelativeSizeAxes = Axes.Both, - Child = playfields[0] = new TestPlayfield() + Child = playfields[0] = new TestPlayfield(), + TimeRange = spawn_interval }, scrollContainers[1] = new ScrollingTestContainer(ScrollingDirection.Down) { RelativeSizeAxes = Axes.Both, - Child = playfields[1] = new TestPlayfield() + Child = playfields[1] = new TestPlayfield(), + TimeRange = spawn_interval }, }, new Drawable[] @@ -56,35 +64,51 @@ namespace osu.Game.Tests.Visual.Gameplay scrollContainers[2] = new ScrollingTestContainer(ScrollingDirection.Left) { RelativeSizeAxes = Axes.Both, - Child = playfields[2] = new TestPlayfield() + Child = playfields[2] = new TestPlayfield(), + TimeRange = spawn_interval }, scrollContainers[3] = new ScrollingTestContainer(ScrollingDirection.Right) { RelativeSizeAxes = Axes.Both, - Child = playfields[3] = new TestPlayfield() + Child = playfields[3] = new TestPlayfield(), + TimeRange = spawn_interval } } } - }); + }; + setUpHitObjects(); + } + + private void setUpHitObjects() + { + scrollContainers.ForEach(c => c.ControlPoints.Add(new MultiplierControlPoint(0))); + + for (int i = 0; i <= spawn_interval; i += 1000) + addHitObject(Time.Current + i); + + hitObjectSpawnDelegate?.Cancel(); + hitObjectSpawnDelegate = Scheduler.AddDelayed(() => addHitObject(Time.Current + spawn_interval), 1000, true); + } + + [Test] + public void TestScrollAlgorithms() + { AddStep("Constant scroll", () => setScrollAlgorithm(ScrollVisualisationMethod.Constant)); AddStep("Overlapping scroll", () => setScrollAlgorithm(ScrollVisualisationMethod.Overlapping)); AddStep("Sequential scroll", () => setScrollAlgorithm(ScrollVisualisationMethod.Sequential)); - AddSliderStep("Time range", 100, 10000, 5000, v => scrollContainers.ForEach(c => c.TimeRange = v)); - AddStep("Add control point", () => addControlPoint(Time.Current + 5000)); + AddSliderStep("Time range", 100, 10000, spawn_interval, v => scrollContainers.Where(c => c != null).ForEach(c => c.TimeRange = v)); + AddStep("Add control point", () => addControlPoint(Time.Current + spawn_interval)); } - protected override void LoadComplete() + [Test] + public void TestScrollLifetime() { - base.LoadComplete(); - - scrollContainers.ForEach(c => c.ControlPoints.Add(new MultiplierControlPoint(0))); - - for (int i = 0; i <= 5000; i += 1000) - addHitObject(Time.Current + i); - - Scheduler.AddDelayed(() => addHitObject(Time.Current + 5000), 1000, true); + AddStep("Set constant scroll", () => setScrollAlgorithm(ScrollVisualisationMethod.Constant)); + // scroll container time range must be less than the rate of spawning hitobjects + // otherwise the hitobjects will spawn already partly visible on screen and look wrong + AddStep("Set time range", () => scrollContainers.ForEach(c => c.TimeRange = spawn_interval / 2.0)); } private void addHitObject(double time) From d828b31ae4b1582ef901f06499a83bd2eec3d3b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 27 Dec 2019 17:16:43 +0100 Subject: [PATCH 41/54] Schedule child mutation in test setup --- .../Visual/Gameplay/TestSceneScrollingHitObjects.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs index 922001f3dd..8629522dc2 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs @@ -37,7 +37,7 @@ namespace osu.Game.Tests.Visual.Gameplay private ScheduledDelegate hitObjectSpawnDelegate; [SetUp] - public void Setup() + public void Setup() => Schedule(() => { Child = new GridContainer { @@ -78,7 +78,7 @@ namespace osu.Game.Tests.Visual.Gameplay }; setUpHitObjects(); - } + }); private void setUpHitObjects() { From bf162f148e320f93cc85456ee780383c030878a2 Mon Sep 17 00:00:00 2001 From: mcendu Date: Sat, 28 Dec 2019 13:48:10 +0800 Subject: [PATCH 42/54] Move mania stage hint to its own class --- .../UI/Components/ColumnHitObjectArea.cs | 108 ++++++++++++------ .../UI/Components/StageHint.cs | 14 +++ 2 files changed, 90 insertions(+), 32 deletions(-) create mode 100644 osu.Game.Rulesets.Mania/UI/Components/StageHint.cs diff --git a/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs b/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs index 386bcbb724..dde718d5ba 100644 --- a/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs +++ b/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs @@ -22,26 +22,15 @@ namespace osu.Game.Rulesets.Mania.UI.Components private readonly IBindable direction = new Bindable(); - private readonly Container hitTargetLine; - private readonly Drawable hitTargetBar; + private readonly Drawable stageHint; public ColumnHitObjectArea(HitObjectContainer hitObjectContainer) { - InternalChildren = new[] + InternalChildren = new Drawable[] { - hitTargetBar = new Box + stageHint = new DefaultStageHint { RelativeSizeAxes = Axes.X, - Height = NotePiece.NOTE_HEIGHT, - Alpha = 0.6f, - Colour = Color4.Black - }, - hitTargetLine = new Container - { - RelativeSizeAxes = Axes.X, - Height = hit_target_bar_height, - Masking = true, - Child = new Box { RelativeSizeAxes = Axes.Both } }, hitObjectContainer }; @@ -55,17 +44,10 @@ namespace osu.Game.Rulesets.Mania.UI.Components { Anchor anchor = dir.NewValue == ScrollingDirection.Up ? Anchor.TopLeft : Anchor.BottomLeft; - hitTargetBar.Anchor = hitTargetBar.Origin = anchor; - hitTargetLine.Anchor = hitTargetLine.Origin = anchor; + stageHint.Anchor = stageHint.Origin = anchor; }, true); } - protected override void LoadComplete() - { - base.LoadComplete(); - updateColours(); - } - private Color4 accentColour; public Color4 AccentColour @@ -77,22 +59,84 @@ namespace osu.Game.Rulesets.Mania.UI.Components return; accentColour = value; - - updateColours(); } } - private void updateColours() + private class DefaultStageHint : StageHint { - if (!IsLoaded) - return; + private readonly IBindable direction = new Bindable(); - hitTargetLine.EdgeEffect = new EdgeEffectParameters + private readonly Container hitTargetLine; + private readonly Drawable hitTargetBar; + + public DefaultStageHint() { - Type = EdgeEffectType.Glow, - Radius = 5, - Colour = accentColour.Opacity(0.5f), - }; + InternalChildren = new[] + { + hitTargetBar = new Box + { + RelativeSizeAxes = Axes.X, + Height = NotePiece.NOTE_HEIGHT, + Alpha = 0.6f, + Colour = Color4.Black + }, + hitTargetLine = new Container + { + RelativeSizeAxes = Axes.X, + Height = hit_target_bar_height, + Masking = true, + Child = new Box { RelativeSizeAxes = Axes.Both } + }, + }; + } + + [BackgroundDependencyLoader] + private void load(IScrollingInfo scrollingInfo) + { + direction.BindTo(scrollingInfo.Direction); + direction.BindValueChanged(dir => + { + Anchor anchor = dir.NewValue == ScrollingDirection.Up ? Anchor.TopLeft : Anchor.BottomLeft; + + hitTargetBar.Anchor = hitTargetBar.Origin = anchor; + hitTargetLine.Anchor = hitTargetLine.Origin = anchor; + }, true); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + updateColours(); + } + + private Color4 accentColour; + + public override Color4 AccentColour + { + get => accentColour; + set + { + if (accentColour == value) + return; + + accentColour = value; + + updateColours(); + } + } + + private void updateColours() + { + if (!IsLoaded) + return; + + hitTargetLine.EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Glow, + Radius = 5, + Colour = accentColour.Opacity(0.5f), + }; + } } } } diff --git a/osu.Game.Rulesets.Mania/UI/Components/StageHint.cs b/osu.Game.Rulesets.Mania/UI/Components/StageHint.cs new file mode 100644 index 0000000000..9574f77eb5 --- /dev/null +++ b/osu.Game.Rulesets.Mania/UI/Components/StageHint.cs @@ -0,0 +1,14 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Mania.UI.Components +{ + public abstract class StageHint : CompositeDrawable, IHasAccentColour + { + public abstract Color4 AccentColour { get; set; } + } +} From 5d2b5cc950b5608cdf369c1d3e226733a7648689 Mon Sep 17 00:00:00 2001 From: mcendu Date: Sat, 28 Dec 2019 14:05:46 +0800 Subject: [PATCH 43/54] correct type of field stageHint --- osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs b/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs index dde718d5ba..6bfb2b89aa 100644 --- a/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs +++ b/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs @@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Mania.UI.Components private readonly IBindable direction = new Bindable(); - private readonly Drawable stageHint; + private readonly StageHint stageHint; public ColumnHitObjectArea(HitObjectContainer hitObjectContainer) { @@ -59,6 +59,8 @@ namespace osu.Game.Rulesets.Mania.UI.Components return; accentColour = value; + + stageHint.AccentColour = accentColour; } } From cdfbe96e9bdceecc7f04ff2262e23b4e207720e7 Mon Sep 17 00:00:00 2001 From: mcendu Date: Sun, 29 Dec 2019 14:52:51 +0800 Subject: [PATCH 44/54] Make AccentColour of StageHint virtual --- osu.Game.Rulesets.Mania/UI/Components/StageHint.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/UI/Components/StageHint.cs b/osu.Game.Rulesets.Mania/UI/Components/StageHint.cs index 9574f77eb5..813a6928c9 100644 --- a/osu.Game.Rulesets.Mania/UI/Components/StageHint.cs +++ b/osu.Game.Rulesets.Mania/UI/Components/StageHint.cs @@ -9,6 +9,6 @@ namespace osu.Game.Rulesets.Mania.UI.Components { public abstract class StageHint : CompositeDrawable, IHasAccentColour { - public abstract Color4 AccentColour { get; set; } + public virtual Color4 AccentColour { get; set; } } } From 61fb9f5613dc47c07a96b88eba4f40f23efeb0c2 Mon Sep 17 00:00:00 2001 From: mcendu Date: Sun, 29 Dec 2019 23:18:50 +0800 Subject: [PATCH 45/54] Remove class StageHint and usage --- .../UI/Components/ColumnHitObjectArea.cs | 9 +++++---- osu.Game.Rulesets.Mania/UI/Components/StageHint.cs | 14 -------------- 2 files changed, 5 insertions(+), 18 deletions(-) delete mode 100644 osu.Game.Rulesets.Mania/UI/Components/StageHint.cs diff --git a/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs b/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs index 6bfb2b89aa..494da6c61b 100644 --- a/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs +++ b/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs @@ -22,11 +22,11 @@ namespace osu.Game.Rulesets.Mania.UI.Components private readonly IBindable direction = new Bindable(); - private readonly StageHint stageHint; + private readonly Drawable stageHint; public ColumnHitObjectArea(HitObjectContainer hitObjectContainer) { - InternalChildren = new Drawable[] + InternalChildren = new[] { stageHint = new DefaultStageHint { @@ -60,11 +60,12 @@ namespace osu.Game.Rulesets.Mania.UI.Components accentColour = value; - stageHint.AccentColour = accentColour; + if (stageHint is IHasAccentColour colouredHitTarget) + colouredHitTarget.AccentColour = accentColour; } } - private class DefaultStageHint : StageHint + private class DefaultStageHint : CompositeDrawable, IHasAccentColour { private readonly IBindable direction = new Bindable(); diff --git a/osu.Game.Rulesets.Mania/UI/Components/StageHint.cs b/osu.Game.Rulesets.Mania/UI/Components/StageHint.cs deleted file mode 100644 index 813a6928c9..0000000000 --- a/osu.Game.Rulesets.Mania/UI/Components/StageHint.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Graphics.Containers; -using osu.Game.Graphics; -using osuTK.Graphics; - -namespace osu.Game.Rulesets.Mania.UI.Components -{ - public abstract class StageHint : CompositeDrawable, IHasAccentColour - { - public virtual Color4 AccentColour { get; set; } - } -} From 51000765ddca9c9f4b4a60605e10bd554e74c111 Mon Sep 17 00:00:00 2001 From: mcendu Date: Sun, 29 Dec 2019 23:29:00 +0800 Subject: [PATCH 46/54] remove override --- osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs b/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs index 494da6c61b..c78023a58b 100644 --- a/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs +++ b/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs @@ -114,7 +114,7 @@ namespace osu.Game.Rulesets.Mania.UI.Components private Color4 accentColour; - public override Color4 AccentColour + public Color4 AccentColour { get => accentColour; set From 20c5748342efacb4d36c6d971d9e9bcdf689d650 Mon Sep 17 00:00:00 2001 From: mcendu Date: Sun, 29 Dec 2019 23:37:28 +0800 Subject: [PATCH 47/54] Use hitTarget in place of stageHint --- .../UI/Components/ColumnHitObjectArea.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs b/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs index c78023a58b..ee2cec1bbd 100644 --- a/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs +++ b/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs @@ -22,13 +22,13 @@ namespace osu.Game.Rulesets.Mania.UI.Components private readonly IBindable direction = new Bindable(); - private readonly Drawable stageHint; + private readonly Drawable hitTarget; public ColumnHitObjectArea(HitObjectContainer hitObjectContainer) { InternalChildren = new[] { - stageHint = new DefaultStageHint + hitTarget = new DefaultHitTarget { RelativeSizeAxes = Axes.X, }, @@ -44,7 +44,7 @@ namespace osu.Game.Rulesets.Mania.UI.Components { Anchor anchor = dir.NewValue == ScrollingDirection.Up ? Anchor.TopLeft : Anchor.BottomLeft; - stageHint.Anchor = stageHint.Origin = anchor; + hitTarget.Anchor = hitTarget.Origin = anchor; }, true); } @@ -60,19 +60,19 @@ namespace osu.Game.Rulesets.Mania.UI.Components accentColour = value; - if (stageHint is IHasAccentColour colouredHitTarget) + if (hitTarget is IHasAccentColour colouredHitTarget) colouredHitTarget.AccentColour = accentColour; } } - private class DefaultStageHint : CompositeDrawable, IHasAccentColour + private class DefaultHitTarget : CompositeDrawable, IHasAccentColour { private readonly IBindable direction = new Bindable(); private readonly Container hitTargetLine; private readonly Drawable hitTargetBar; - public DefaultStageHint() + public DefaultHitTarget() { InternalChildren = new[] { From 39d77386a809c4900600e97db86fe8558e18b52a Mon Sep 17 00:00:00 2001 From: Joehu Date: Sun, 29 Dec 2019 10:49:28 -0800 Subject: [PATCH 48/54] Fix context menus not showing on social panels --- .../SearchableList/SearchableListOverlay.cs | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/osu.Game/Overlays/SearchableList/SearchableListOverlay.cs b/osu.Game/Overlays/SearchableList/SearchableListOverlay.cs index 37478d902b..5975e94ffc 100644 --- a/osu.Game/Overlays/SearchableList/SearchableListOverlay.cs +++ b/osu.Game/Overlays/SearchableList/SearchableListOverlay.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Cursor; namespace osu.Game.Overlays.SearchableList { @@ -61,21 +62,20 @@ namespace osu.Game.Overlays.SearchableList scrollContainer = new Container { RelativeSizeAxes = Axes.Both, - Children = new[] + Child = new OsuContextMenuContainer { - new OsuScrollContainer + RelativeSizeAxes = Axes.Both, + Masking = true, + Child = new OsuScrollContainer { RelativeSizeAxes = Axes.Both, ScrollbarVisible = false, - Children = new[] + Child = ScrollFlow = new FillFlowContainer { - ScrollFlow = new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Horizontal = WIDTH_PADDING, Bottom = 50 }, - Direction = FillDirection.Vertical, - }, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Horizontal = WIDTH_PADDING, Bottom = 50 }, + Direction = FillDirection.Vertical, }, }, }, From bcf7156882aeb0dd86fbd67a1feba7166e76d218 Mon Sep 17 00:00:00 2001 From: Joehu Date: Sun, 29 Dec 2019 11:19:46 -0800 Subject: [PATCH 49/54] Add context menu on direct panels --- osu.Game/Overlays/Direct/DirectPanel.cs | 27 +++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/osu.Game/Overlays/Direct/DirectPanel.cs b/osu.Game/Overlays/Direct/DirectPanel.cs index c1c5113c5e..0ec0466580 100644 --- a/osu.Game/Overlays/Direct/DirectPanel.cs +++ b/osu.Game/Overlays/Direct/DirectPanel.cs @@ -1,6 +1,7 @@ // 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.Diagnostics; using System.Linq; @@ -9,21 +10,25 @@ using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics; +using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; using osuTK; using osuTK.Graphics; namespace osu.Game.Overlays.Direct { - public abstract class DirectPanel : Container + public abstract class DirectPanel : OsuClickableContainer, IHasContextMenu { public readonly BeatmapSetInfo SetInfo; @@ -44,6 +49,8 @@ namespace osu.Game.Overlays.Direct protected override Container Content => content; + protected Action ViewBeatmap; + protected DirectPanel(BeatmapSetInfo setInfo) { Debug.Assert(setInfo.OnlineBeatmapSetID != null); @@ -88,6 +95,12 @@ namespace osu.Game.Overlays.Direct }, } }); + + Action = ViewBeatmap = () => + { + Debug.Assert(SetInfo.OnlineBeatmapSetID != null); + beatmapSetOverlay?.FetchAndShowBeatmapSet(SetInfo.OnlineBeatmapSetID.Value); + }; } protected override void Update() @@ -120,13 +133,6 @@ namespace osu.Game.Overlays.Direct base.OnHoverLost(e); } - protected override bool OnClick(ClickEvent e) - { - Debug.Assert(SetInfo.OnlineBeatmapSetID != null); - beatmapSetOverlay?.FetchAndShowBeatmapSet(SetInfo.OnlineBeatmapSetID.Value); - return true; - } - protected override void LoadComplete() { base.LoadComplete(); @@ -203,5 +209,10 @@ namespace osu.Game.Overlays.Direct Value = value; } } + + public MenuItem[] ContextMenuItems => new MenuItem[] + { + new OsuMenuItem("View Beatmap", MenuItemType.Highlighted, ViewBeatmap), + }; } } From 0eccfc79cc84403609b080665bf70465992a3eae Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 30 Dec 2019 11:51:33 +0900 Subject: [PATCH 50/54] Remove unused field --- osu.Game/Overlays/Direct/DirectPanel.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/osu.Game/Overlays/Direct/DirectPanel.cs b/osu.Game/Overlays/Direct/DirectPanel.cs index 0ec0466580..4ad8e95512 100644 --- a/osu.Game/Overlays/Direct/DirectPanel.cs +++ b/osu.Game/Overlays/Direct/DirectPanel.cs @@ -37,8 +37,6 @@ namespace osu.Game.Overlays.Direct private Container content; - private BeatmapSetOverlay beatmapSetOverlay; - public PreviewTrack Preview => PlayButton.Preview; public Bindable PreviewPlaying => PlayButton?.Playing; @@ -77,8 +75,6 @@ namespace osu.Game.Overlays.Direct [BackgroundDependencyLoader(permitNulls: true)] private void load(BeatmapManager beatmaps, OsuColour colours, BeatmapSetOverlay beatmapSetOverlay) { - this.beatmapSetOverlay = beatmapSetOverlay; - AddInternal(content = new Container { RelativeSizeAxes = Axes.Both, From 8ae4cfaa5205111b5e2afe06aba07fe66e9c5f8c Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 30 Dec 2019 07:33:49 +0000 Subject: [PATCH 51/54] Bump ppy.osu.Game.Resources from 2019.1227.0 to 2019.1230.0 Bumps [ppy.osu.Game.Resources](https://github.com/ppy/osu-resources) from 2019.1227.0 to 2019.1230.0. - [Release notes](https://github.com/ppy/osu-resources/releases) - [Commits](https://github.com/ppy/osu-resources/compare/2019.1227.0...2019.1230.0) Signed-off-by: dependabot-preview[bot] --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 6ff9416e47..0b41d5cda4 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -53,7 +53,7 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 806aadde84..565608b40f 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -22,7 +22,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 230ff01cce..60355b8592 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -73,7 +73,7 @@ - + From 32e4d4a5633e907130da26f9e5e82fa991ade859 Mon Sep 17 00:00:00 2001 From: Joehu Date: Mon, 30 Dec 2019 09:29:46 -0800 Subject: [PATCH 52/54] Fix search textboxes absorbing home/end input on scroll containers --- osu.Game/Graphics/UserInterface/SearchTextBox.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game/Graphics/UserInterface/SearchTextBox.cs b/osu.Game/Graphics/UserInterface/SearchTextBox.cs index ff3618b263..cdae36e700 100644 --- a/osu.Game/Graphics/UserInterface/SearchTextBox.cs +++ b/osu.Game/Graphics/UserInterface/SearchTextBox.cs @@ -40,6 +40,13 @@ namespace osu.Game.Graphics.UserInterface if (action.ActionType == PlatformActionType.CharNext && action.ActionMethod == PlatformActionMethod.Delete) return false; + switch (action.ActionType) + { + case PlatformActionType.LineEnd: + case PlatformActionType.LineStart: + return false; + } + return base.OnPressed(action); } From 25be3fd799855d4a234c05d80ff2e1cb431c97d4 Mon Sep 17 00:00:00 2001 From: Joehu Date: Mon, 30 Dec 2019 09:38:22 -0800 Subject: [PATCH 53/54] Move shift-delete handling to switch --- osu.Game/Graphics/UserInterface/SearchTextBox.cs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/SearchTextBox.cs b/osu.Game/Graphics/UserInterface/SearchTextBox.cs index cdae36e700..dbaf2cd073 100644 --- a/osu.Game/Graphics/UserInterface/SearchTextBox.cs +++ b/osu.Game/Graphics/UserInterface/SearchTextBox.cs @@ -34,17 +34,19 @@ namespace osu.Game.Graphics.UserInterface public override bool OnPressed(PlatformAction action) { - // Shift+delete is handled via PlatformAction on macOS. this is not so useful in the context of a SearchTextBox - // as we do not allow arrow key navigation in the first place (ie. the caret should always be at the end of text) - // Avoid handling it here to allow other components to potentially consume the shortcut. - if (action.ActionType == PlatformActionType.CharNext && action.ActionMethod == PlatformActionMethod.Delete) - return false; - switch (action.ActionType) { case PlatformActionType.LineEnd: case PlatformActionType.LineStart: return false; + + // Shift+delete is handled via PlatformAction on macOS. this is not so useful in the context of a SearchTextBox + // as we do not allow arrow key navigation in the first place (ie. the caret should always be at the end of text) + // Avoid handling it here to allow other components to potentially consume the shortcut. + case PlatformActionType.CharNext: + if (action.ActionMethod == PlatformActionMethod.Delete) + return false; + break; } return base.OnPressed(action); From a292d235d6f6cf7f78a28ad195c40f857b498193 Mon Sep 17 00:00:00 2001 From: Joehu Date: Mon, 30 Dec 2019 09:49:34 -0800 Subject: [PATCH 54/54] Separate statement with newline --- osu.Game/Graphics/UserInterface/SearchTextBox.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Graphics/UserInterface/SearchTextBox.cs b/osu.Game/Graphics/UserInterface/SearchTextBox.cs index dbaf2cd073..fe8756a4d2 100644 --- a/osu.Game/Graphics/UserInterface/SearchTextBox.cs +++ b/osu.Game/Graphics/UserInterface/SearchTextBox.cs @@ -46,6 +46,7 @@ namespace osu.Game.Graphics.UserInterface case PlatformActionType.CharNext: if (action.ActionMethod == PlatformActionMethod.Delete) return false; + break; }