From 961bd1177c9bfbeb3203aab22a4602fee3d3f6b2 Mon Sep 17 00:00:00 2001 From: Pasi4K5 Date: Sun, 25 Apr 2021 00:39:36 +0200 Subject: [PATCH 01/33] Add mod "Random" for ruleset "osu!" --- osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs | 77 ++++++++++++++++++++++ osu.Game.Rulesets.Osu/OsuRuleset.cs | 1 + osu.Game/Rulesets/Mods/ModRandomOsu.cs | 47 +++++++++++++ 3 files changed, 125 insertions(+) create mode 100644 osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs create mode 100644 osu.Game/Rulesets/Mods/ModRandomOsu.cs diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs new file mode 100644 index 0000000000..f3c9040b1c --- /dev/null +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs @@ -0,0 +1,77 @@ +// 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.Linq; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.UI; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Mods +{ + public class OsuModRandom : ModRandomOsu + { + protected override void RandomiseHitObjectPositions(IBeatmap beatmap) + { + var rng = new Random(); + + foreach (var hitObject in beatmap.HitObjects) + { + if (RandomiseCirclePositions.Value && hitObject is HitCircle circle) + { + circle.Position = new Vector2( + (float)rng.NextDouble() * OsuPlayfield.BASE_SIZE.X, + (float)rng.NextDouble() * OsuPlayfield.BASE_SIZE.Y + ); + } + else if (RandomiseSpinnerPositions.Value && hitObject is Spinner spinner) + { + spinner.Position = new Vector2( + (float)rng.NextDouble() * OsuPlayfield.BASE_SIZE.X, + (float)rng.NextDouble() * OsuPlayfield.BASE_SIZE.Y + ); + } + else if (RandomiseSliderPositions.Value && hitObject is Slider slider) + { + // Min. distances from the slider's position to the border to prevent the slider from being partially out of the screen + float minLeft = 0, minRight = 0, minTop = 0, minBottom = 0; + + var controlPointPositions = (from position + in slider.Path.ControlPoints + select position.Position.Value).ToList(); + + controlPointPositions.Add(slider.EndPosition); + controlPointPositions.RemoveAt(controlPointPositions.Count - 1); + + foreach (var position in controlPointPositions) + { + if (position.X > minRight) + { + minRight = position.X; + } + else if (-position.X > minLeft) + { + minLeft = -position.X; + } + + if (position.Y > minBottom) + { + minBottom = position.Y; + } + else if (-position.Y > minTop) + { + minTop = -position.Y; + } + } + + slider.Position = new Vector2( + (float)rng.NextDouble() * (OsuPlayfield.BASE_SIZE.X - minLeft - minRight) + minLeft, + (float)rng.NextDouble() * (OsuPlayfield.BASE_SIZE.Y - minTop - minBottom) + minTop + ); + } + } + } + } +} diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 465d6d7155..6a04c4ca5c 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -186,6 +186,7 @@ namespace osu.Game.Rulesets.Osu new MultiMod(new ModWindUp(), new ModWindDown()), new OsuModTraceable(), new OsuModBarrelRoll(), + new OsuModRandom(), }; case ModType.System: diff --git a/osu.Game/Rulesets/Mods/ModRandomOsu.cs b/osu.Game/Rulesets/Mods/ModRandomOsu.cs new file mode 100644 index 0000000000..9fb2c07d82 --- /dev/null +++ b/osu.Game/Rulesets/Mods/ModRandomOsu.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 osu.Framework.Bindables; +using osu.Framework.Graphics.Sprites; +using osu.Game.Beatmaps; +using osu.Game.Configuration; +using osu.Game.Graphics; + +namespace osu.Game.Rulesets.Mods +{ + public abstract class ModRandomOsu : Mod, IApplicableToBeatmap + { + public override string Name => "Random"; + public override string Acronym => "RD"; + public override IconUsage? Icon => OsuIcon.Dice; + public override ModType Type => ModType.Fun; + public override string Description => "Hit objects appear at random positions"; + public override double ScoreMultiplier => 1; + public override bool Ranked => false; + + [SettingSource("Randomise circle positions", "Hit circles appear at random positions")] + public Bindable RandomiseCirclePositions { get; } = new BindableBool + { + Default = true, + Value = true, + }; + + [SettingSource("Randomise slider positions", "Sliders appear at random positions")] + public Bindable RandomiseSliderPositions { get; } = new BindableBool + { + Default = true, + Value = true, + }; + + [SettingSource("Randomise spinner positions", "Spinners appear at random positions")] + public Bindable RandomiseSpinnerPositions { get; } = new BindableBool + { + Default = true, + Value = true, + }; + + public void ApplyToBeatmap(IBeatmap beatmap) => RandomiseHitObjectPositions(beatmap); + + protected abstract void RandomiseHitObjectPositions(IBeatmap beatmap); + } +} From 817bb5213c48d0b4cf5f94a375c74e04c1a16ff5 Mon Sep 17 00:00:00 2001 From: Pasi4K5 Date: Sun, 25 Apr 2021 00:46:35 +0200 Subject: [PATCH 02/33] Make OsuAutoGenerator spin the cursor around the position of the spinner instead of a set value This is to make Autoplay work with randomised spinner positions --- .../Replays/OsuAutoGenerator.cs | 18 +++++++++--------- .../Replays/OsuAutoGeneratorBase.cs | 3 --- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs index 7b0cf651c8..609799dc54 100644 --- a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs +++ b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs @@ -155,9 +155,9 @@ namespace osu.Game.Rulesets.Osu.Replays if (spinner.SpinsRequired == 0) return; - calcSpinnerStartPosAndDirection(((OsuReplayFrame)Frames[^1]).Position, out startPosition, out spinnerDirection); + calcSpinnerStartPosAndDirection(spinner, ((OsuReplayFrame)Frames[^1]).Position, out startPosition, out spinnerDirection); - Vector2 spinCentreOffset = SPINNER_CENTRE - ((OsuReplayFrame)Frames[^1]).Position; + Vector2 spinCentreOffset = spinner.Position - ((OsuReplayFrame)Frames[^1]).Position; if (spinCentreOffset.Length > SPIN_RADIUS) { @@ -180,9 +180,9 @@ namespace osu.Game.Rulesets.Osu.Replays #region Helper subroutines - private static void calcSpinnerStartPosAndDirection(Vector2 prevPos, out Vector2 startPosition, out float spinnerDirection) + private static void calcSpinnerStartPosAndDirection(Spinner spinner, Vector2 prevPos, out Vector2 startPosition, out float spinnerDirection) { - Vector2 spinCentreOffset = SPINNER_CENTRE - prevPos; + Vector2 spinCentreOffset = spinner.Position - prevPos; float distFromCentre = spinCentreOffset.Length; float distToTangentPoint = MathF.Sqrt(distFromCentre * distFromCentre - SPIN_RADIUS * SPIN_RADIUS); @@ -216,13 +216,13 @@ namespace osu.Game.Rulesets.Osu.Replays else if (spinCentreOffset.Length > 0) { // Previous cursor position was inside spin circle, set startPosition to the nearest point on spin circle. - startPosition = SPINNER_CENTRE - spinCentreOffset * (SPIN_RADIUS / spinCentreOffset.Length); + startPosition = spinner.Position - spinCentreOffset * (SPIN_RADIUS / spinCentreOffset.Length); spinnerDirection = 1; } else { // Degenerate case where cursor position is exactly at the centre of the spin circle. - startPosition = SPINNER_CENTRE + new Vector2(0, -SPIN_RADIUS); + startPosition = spinner.Position + new Vector2(0, -SPIN_RADIUS); spinnerDirection = 1; } } @@ -335,7 +335,7 @@ namespace osu.Game.Rulesets.Osu.Replays { // We add intermediate frames for spinning / following a slider here. case Spinner spinner: - Vector2 difference = startPosition - SPINNER_CENTRE; + Vector2 difference = startPosition - spinner.Position; float radius = difference.Length; float angle = radius == 0 ? 0 : MathF.Atan2(difference.Y, difference.X); @@ -348,7 +348,7 @@ namespace osu.Game.Rulesets.Osu.Replays t = ApplyModsToTimeDelta(previousFrame, nextFrame) * spinnerDirection; angle += (float)t / 20; - Vector2 pos = SPINNER_CENTRE + CirclePosition(angle, SPIN_RADIUS); + Vector2 pos = spinner.Position + CirclePosition(angle, SPIN_RADIUS); AddFrameToReplay(new OsuReplayFrame((int)nextFrame, new Vector2(pos.X, pos.Y), action)); previousFrame = nextFrame; @@ -357,7 +357,7 @@ namespace osu.Game.Rulesets.Osu.Replays t = ApplyModsToTimeDelta(previousFrame, spinner.EndTime) * spinnerDirection; angle += (float)t / 20; - Vector2 endPosition = SPINNER_CENTRE + CirclePosition(angle, SPIN_RADIUS); + Vector2 endPosition = spinner.Position + CirclePosition(angle, SPIN_RADIUS); AddFrameToReplay(new OsuReplayFrame(spinner.EndTime, new Vector2(endPosition.X, endPosition.Y), action)); diff --git a/osu.Game.Rulesets.Osu/Replays/OsuAutoGeneratorBase.cs b/osu.Game.Rulesets.Osu/Replays/OsuAutoGeneratorBase.cs index 1cb3208c30..69eb669a8e 100644 --- a/osu.Game.Rulesets.Osu/Replays/OsuAutoGeneratorBase.cs +++ b/osu.Game.Rulesets.Osu/Replays/OsuAutoGeneratorBase.cs @@ -8,7 +8,6 @@ using System.Collections.Generic; using System.Linq; using osu.Game.Replays; using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Replays; namespace osu.Game.Rulesets.Osu.Replays @@ -20,8 +19,6 @@ namespace osu.Game.Rulesets.Osu.Replays /// /// Constants (for spinners). /// - protected static readonly Vector2 SPINNER_CENTRE = OsuPlayfield.BASE_SIZE / 2; - public const float SPIN_RADIUS = 50; #endregion From 8a3fa53c2661662272a62699eed7d7a1f04407ea Mon Sep 17 00:00:00 2001 From: Pasi4K5 Date: Sun, 25 Apr 2021 01:02:03 +0200 Subject: [PATCH 03/33] Change mod description and settings labels --- osu.Game/Rulesets/Mods/ModRandomOsu.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModRandomOsu.cs b/osu.Game/Rulesets/Mods/ModRandomOsu.cs index 9fb2c07d82..6b86da357a 100644 --- a/osu.Game/Rulesets/Mods/ModRandomOsu.cs +++ b/osu.Game/Rulesets/Mods/ModRandomOsu.cs @@ -15,25 +15,25 @@ namespace osu.Game.Rulesets.Mods public override string Acronym => "RD"; public override IconUsage? Icon => OsuIcon.Dice; public override ModType Type => ModType.Fun; - public override string Description => "Hit objects appear at random positions"; + public override string Description => "Practice your reaction time!"; public override double ScoreMultiplier => 1; public override bool Ranked => false; - [SettingSource("Randomise circle positions", "Hit circles appear at random positions")] + [SettingSource("Circles", "Hit circles appear at random positions")] public Bindable RandomiseCirclePositions { get; } = new BindableBool { Default = true, Value = true, }; - [SettingSource("Randomise slider positions", "Sliders appear at random positions")] + [SettingSource("Sliders", "Sliders appear at random positions")] public Bindable RandomiseSliderPositions { get; } = new BindableBool { Default = true, Value = true, }; - [SettingSource("Randomise spinner positions", "Spinners appear at random positions")] + [SettingSource("Spinners", "Spinners appear at random positions")] public Bindable RandomiseSpinnerPositions { get; } = new BindableBool { Default = true, From 92f765b9588e6de084b4d57d7e00a75a210c07ec Mon Sep 17 00:00:00 2001 From: Pasi4K5 Date: Sun, 25 Apr 2021 01:19:06 +0200 Subject: [PATCH 04/33] Change ModType from Fun to Conversion --- osu.Game.Rulesets.Osu/OsuRuleset.cs | 4 ++-- osu.Game/Rulesets/Mods/ModRandomOsu.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 6a04c4ca5c..b50d3ad2b4 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -164,7 +164,8 @@ namespace osu.Game.Rulesets.Osu { new OsuModTarget(), new OsuModDifficultyAdjust(), - new OsuModClassic() + new OsuModClassic(), + new OsuModRandom(), }; case ModType.Automation: @@ -186,7 +187,6 @@ namespace osu.Game.Rulesets.Osu new MultiMod(new ModWindUp(), new ModWindDown()), new OsuModTraceable(), new OsuModBarrelRoll(), - new OsuModRandom(), }; case ModType.System: diff --git a/osu.Game/Rulesets/Mods/ModRandomOsu.cs b/osu.Game/Rulesets/Mods/ModRandomOsu.cs index 6b86da357a..1581065c01 100644 --- a/osu.Game/Rulesets/Mods/ModRandomOsu.cs +++ b/osu.Game/Rulesets/Mods/ModRandomOsu.cs @@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Mods public override string Name => "Random"; public override string Acronym => "RD"; public override IconUsage? Icon => OsuIcon.Dice; - public override ModType Type => ModType.Fun; + public override ModType Type => ModType.Conversion; public override string Description => "Practice your reaction time!"; public override double ScoreMultiplier => 1; public override bool Ranked => false; From f33f1b2bed13faebb9ad8d041077f172178e60fc Mon Sep 17 00:00:00 2001 From: Pasi4K5 Date: Sun, 25 Apr 2021 01:34:39 +0200 Subject: [PATCH 05/33] Remove class "ModRandomOsu" and adjust code Add documentation comment for OsuModRandom --- osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs | 34 +++++++++++++++- osu.Game/Rulesets/Mods/ModRandomOsu.cs | 47 ---------------------- 2 files changed, 32 insertions(+), 49 deletions(-) delete mode 100644 osu.Game/Rulesets/Mods/ModRandomOsu.cs diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs index f3c9040b1c..c87628b0e7 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs @@ -3,17 +3,47 @@ using System; using System.Linq; +using osu.Framework.Bindables; using osu.Game.Beatmaps; +using osu.Game.Configuration; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.UI; using osuTK; namespace osu.Game.Rulesets.Osu.Mods { - public class OsuModRandom : ModRandomOsu + /// + /// Mod that randomises the positions of the s + /// + public class OsuModRandom : ModRandom, IApplicableToBeatmap { - protected override void RandomiseHitObjectPositions(IBeatmap beatmap) + public override string Description => "Practice your reaction time!"; + public override bool Ranked => false; + + [SettingSource("Circles", "Hit circles appear at random positions")] + public Bindable RandomiseCirclePositions { get; } = new BindableBool + { + Default = true, + Value = true, + }; + + [SettingSource("Sliders", "Sliders appear at random positions")] + public Bindable RandomiseSliderPositions { get; } = new BindableBool + { + Default = true, + Value = true, + }; + + [SettingSource("Spinners", "Spinners appear at random positions")] + public Bindable RandomiseSpinnerPositions { get; } = new BindableBool + { + Default = true, + Value = true, + }; + + public void ApplyToBeatmap(IBeatmap beatmap) { var rng = new Random(); diff --git a/osu.Game/Rulesets/Mods/ModRandomOsu.cs b/osu.Game/Rulesets/Mods/ModRandomOsu.cs deleted file mode 100644 index 1581065c01..0000000000 --- a/osu.Game/Rulesets/Mods/ModRandomOsu.cs +++ /dev/null @@ -1,47 +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.Bindables; -using osu.Framework.Graphics.Sprites; -using osu.Game.Beatmaps; -using osu.Game.Configuration; -using osu.Game.Graphics; - -namespace osu.Game.Rulesets.Mods -{ - public abstract class ModRandomOsu : Mod, IApplicableToBeatmap - { - public override string Name => "Random"; - public override string Acronym => "RD"; - public override IconUsage? Icon => OsuIcon.Dice; - public override ModType Type => ModType.Conversion; - public override string Description => "Practice your reaction time!"; - public override double ScoreMultiplier => 1; - public override bool Ranked => false; - - [SettingSource("Circles", "Hit circles appear at random positions")] - public Bindable RandomiseCirclePositions { get; } = new BindableBool - { - Default = true, - Value = true, - }; - - [SettingSource("Sliders", "Sliders appear at random positions")] - public Bindable RandomiseSliderPositions { get; } = new BindableBool - { - Default = true, - Value = true, - }; - - [SettingSource("Spinners", "Spinners appear at random positions")] - public Bindable RandomiseSpinnerPositions { get; } = new BindableBool - { - Default = true, - Value = true, - }; - - public void ApplyToBeatmap(IBeatmap beatmap) => RandomiseHitObjectPositions(beatmap); - - protected abstract void RandomiseHitObjectPositions(IBeatmap beatmap); - } -} From 08821da954d3927d776534fa1550d23838b3855a Mon Sep 17 00:00:00 2001 From: Pasi4K5 Date: Sun, 25 Apr 2021 01:43:32 +0200 Subject: [PATCH 06/33] Change mod description --- osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs index c87628b0e7..40e966a686 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs @@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Mods /// public class OsuModRandom : ModRandom, IApplicableToBeatmap { - public override string Description => "Practice your reaction time!"; + public override string Description => "It never gets boring!"; public override bool Ranked => false; [SettingSource("Circles", "Hit circles appear at random positions")] From 6e85c4e0699e85df8ebf841404cfd9e1dd652bae Mon Sep 17 00:00:00 2001 From: Pasi4K5 Date: Sun, 25 Apr 2021 23:57:01 +0200 Subject: [PATCH 07/33] Change randomisation process to keep distances between objects Remove now unnecessary settings; Remove spinners; Sliders are not implemented yet --- osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs | 254 +++++++++++++++------ 1 file changed, 183 insertions(+), 71 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs index 40e966a686..1d430ba711 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs @@ -2,10 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Linq; -using osu.Framework.Bindables; using osu.Game.Beatmaps; -using osu.Game.Configuration; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Objects; @@ -22,86 +19,201 @@ namespace osu.Game.Rulesets.Osu.Mods public override string Description => "It never gets boring!"; public override bool Ranked => false; - [SettingSource("Circles", "Hit circles appear at random positions")] - public Bindable RandomiseCirclePositions { get; } = new BindableBool - { - Default = true, - Value = true, - }; - - [SettingSource("Sliders", "Sliders appear at random positions")] - public Bindable RandomiseSliderPositions { get; } = new BindableBool - { - Default = true, - Value = true, - }; - - [SettingSource("Spinners", "Spinners appear at random positions")] - public Bindable RandomiseSpinnerPositions { get; } = new BindableBool - { - Default = true, - Value = true, - }; + // The distances from the hit objects to the borders of the playfield they start to "turn around" and curve towards the middle. + // The closer the hit objects draw to the border, the sharper the turn + private const byte border_distance_x = 128; + private const byte border_distance_y = 96; public void ApplyToBeatmap(IBeatmap beatmap) { var rng = new Random(); - foreach (var hitObject in beatmap.HitObjects) + // Absolute angle + float prevAngleRad = 0; + + // Absolute positions + Vector2 prevPosUnchanged = ((OsuHitObject)beatmap.HitObjects[0]).Position; + Vector2 prevPosChanged = ((OsuHitObject)beatmap.HitObjects[0]).Position; + + // rateOfChangeMultiplier changes every i iterations to prevent shaky-line-shaped streams + byte i = 5; + float rateOfChangeMultiplier = 0; + + foreach (var beatmapHitObject in beatmap.HitObjects) { - if (RandomiseCirclePositions.Value && hitObject is HitCircle circle) + if (!(beatmapHitObject is OsuHitObject hitObject)) + return; + + // posUnchanged: position from the original beatmap (not randomised) + var posUnchanged = hitObject.EndPosition; + var posChanged = Vector2.Zero; + + // Angle of the vector pointing from the last to the current hit object + float angleRad = 0; + + if (i >= 5) { - circle.Position = new Vector2( - (float)rng.NextDouble() * OsuPlayfield.BASE_SIZE.X, - (float)rng.NextDouble() * OsuPlayfield.BASE_SIZE.Y + i = 0; + rateOfChangeMultiplier = (float)rng.NextDouble() * 2 - 1; + } + + if (hitObject is HitCircle circle) + { + var distanceToPrev = Vector2.Distance(posUnchanged, prevPosUnchanged); + + circle.Position = posChanged = getRandomisedPosition( + rateOfChangeMultiplier, + prevPosChanged, + prevAngleRad, + distanceToPrev, + out angleRad ); } - else if (RandomiseSpinnerPositions.Value && hitObject is Spinner spinner) - { - spinner.Position = new Vector2( - (float)rng.NextDouble() * OsuPlayfield.BASE_SIZE.X, - (float)rng.NextDouble() * OsuPlayfield.BASE_SIZE.Y - ); - } - else if (RandomiseSliderPositions.Value && hitObject is Slider slider) - { - // Min. distances from the slider's position to the border to prevent the slider from being partially out of the screen - float minLeft = 0, minRight = 0, minTop = 0, minBottom = 0; - var controlPointPositions = (from position - in slider.Path.ControlPoints - select position.Position.Value).ToList(); + // TODO: Implement slider position randomisation - controlPointPositions.Add(slider.EndPosition); - controlPointPositions.RemoveAt(controlPointPositions.Count - 1); - - foreach (var position in controlPointPositions) - { - if (position.X > minRight) - { - minRight = position.X; - } - else if (-position.X > minLeft) - { - minLeft = -position.X; - } - - if (position.Y > minBottom) - { - minBottom = position.Y; - } - else if (-position.Y > minTop) - { - minTop = -position.Y; - } - } - - slider.Position = new Vector2( - (float)rng.NextDouble() * (OsuPlayfield.BASE_SIZE.X - minLeft - minRight) + minLeft, - (float)rng.NextDouble() * (OsuPlayfield.BASE_SIZE.Y - minTop - minBottom) + minTop - ); - } + prevAngleRad = angleRad; + prevPosUnchanged = posUnchanged; + prevPosChanged = posChanged; + i++; } } + + /// + /// Returns the final position of the hit object + /// + /// Final position of the hit object + private Vector2 getRandomisedPosition( + float rateOfChangeMultiplier, + Vector2 prevPosChanged, + float prevAngleRad, + float distanceToPrev, + out float newAngle) + { + // The max. angle (relative to the angle of the vector pointing from the 2nd last to the last hit object) + // is proportional to the distance between the last and the current hit object + // to allow jumps and prevent too sharp turns during streams. + var maxDistance = OsuPlayfield.BASE_SIZE.LengthFast; + var randomAngleRad = rateOfChangeMultiplier * 2 * Math.PI * distanceToPrev / maxDistance; + + newAngle = (float)randomAngleRad + prevAngleRad; + if (newAngle < 0) + newAngle += 2 * (float)Math.PI; + + var posRelativeToPrev = new Vector2( + distanceToPrev * (float)Math.Cos(newAngle), + distanceToPrev * (float)Math.Sin(newAngle) + ); + + posRelativeToPrev = getRotatedVector(prevPosChanged, posRelativeToPrev); + + newAngle = (float)Math.Atan2(posRelativeToPrev.Y, posRelativeToPrev.X); + var position = Vector2.Add(prevPosChanged, posRelativeToPrev); + + // Move hit objects back into the playfield if they are outside of it, + // which would sometimes happen during big jumps otherwise. + if (position.X < 0) + position.X = 0; + else if (position.X > OsuPlayfield.BASE_SIZE.X) + position.X = OsuPlayfield.BASE_SIZE.X; + + if (position.Y < 0) + position.Y = 0; + else if (position.Y > OsuPlayfield.BASE_SIZE.Y) + position.Y = OsuPlayfield.BASE_SIZE.Y; + + return position; + } + + /// + /// Determines the position of the current hit object relative to the previous one. + /// + /// The position of the current hit object relative to the previous one + private Vector2 getRotatedVector(Vector2 prevPosChanged, Vector2 posRelativeToPrev) + { + var relativeRotationDistance = 0f; + var playfieldMiddle = Vector2.Divide(OsuPlayfield.BASE_SIZE, 2); + + if (prevPosChanged.X < playfieldMiddle.X) + { + relativeRotationDistance = Math.Max( + (border_distance_x - prevPosChanged.X) / border_distance_x, + relativeRotationDistance + ); + } + else + { + relativeRotationDistance = Math.Max( + (prevPosChanged.X - (OsuPlayfield.BASE_SIZE.X - border_distance_x)) / border_distance_x, + relativeRotationDistance + ); + } + + if (prevPosChanged.Y < playfieldMiddle.Y) + { + relativeRotationDistance = Math.Max( + (border_distance_y - prevPosChanged.Y) / border_distance_y, + relativeRotationDistance + ); + } + else + { + relativeRotationDistance = Math.Max( + (prevPosChanged.Y - (OsuPlayfield.BASE_SIZE.Y - border_distance_y)) / border_distance_y, + relativeRotationDistance + ); + } + + return rotateVectorTowardsVector( + posRelativeToPrev, + Vector2.Subtract(playfieldMiddle, prevPosChanged), + relativeRotationDistance + ); + } + + /// + /// Rotates vector "initial" towards vector "destinantion" + /// + /// Vector to rotate to "destination" + /// Vector "initial" should be rotated to + /// The angle the vector should be rotated relative to the difference between the angles of the the two vectors. + /// Resulting vector + private Vector2 rotateVectorTowardsVector(Vector2 initial, Vector2 destination, float relativeDistance) + { + var initialAngleRad = Math.Atan2(initial.Y, initial.X); + var destAngleRad = Math.Atan2(destination.Y, destination.X); + + // Divide by 2 to limit the max. angle to 90° + // (90° is enough to prevent the hit objects from leaving the playfield) + relativeDistance /= 2; + + var diff = destAngleRad - initialAngleRad; + + while (diff < -Math.PI) + { + diff += 2 * Math.PI; + } + + while (diff > Math.PI) + { + diff -= 2 * Math.PI; + } + + var finalAngle = 0d; + + if (diff > 0) + { + finalAngle = initialAngleRad + relativeDistance * diff; + } + else if (diff < 0) + { + finalAngle = initialAngleRad + relativeDistance * diff; + } + + return new Vector2( + initial.Length * (float)Math.Cos(finalAngle), + initial.Length * (float)Math.Sin(finalAngle) + ); + } } } From 19fc2243489235598d523104296ec51448d6d897 Mon Sep 17 00:00:00 2001 From: Pasi4K5 Date: Mon, 26 Apr 2021 11:52:46 +0200 Subject: [PATCH 08/33] Revert "Make OsuAutoGenerator spin the cursor around the position of the spinner instead of a set value" This reverts commit 817bb5213c48d0b4cf5f94a375c74e04c1a16ff5. --- .../Replays/OsuAutoGenerator.cs | 18 +++++++++--------- .../Replays/OsuAutoGeneratorBase.cs | 3 +++ 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs index 609799dc54..7b0cf651c8 100644 --- a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs +++ b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs @@ -155,9 +155,9 @@ namespace osu.Game.Rulesets.Osu.Replays if (spinner.SpinsRequired == 0) return; - calcSpinnerStartPosAndDirection(spinner, ((OsuReplayFrame)Frames[^1]).Position, out startPosition, out spinnerDirection); + calcSpinnerStartPosAndDirection(((OsuReplayFrame)Frames[^1]).Position, out startPosition, out spinnerDirection); - Vector2 spinCentreOffset = spinner.Position - ((OsuReplayFrame)Frames[^1]).Position; + Vector2 spinCentreOffset = SPINNER_CENTRE - ((OsuReplayFrame)Frames[^1]).Position; if (spinCentreOffset.Length > SPIN_RADIUS) { @@ -180,9 +180,9 @@ namespace osu.Game.Rulesets.Osu.Replays #region Helper subroutines - private static void calcSpinnerStartPosAndDirection(Spinner spinner, Vector2 prevPos, out Vector2 startPosition, out float spinnerDirection) + private static void calcSpinnerStartPosAndDirection(Vector2 prevPos, out Vector2 startPosition, out float spinnerDirection) { - Vector2 spinCentreOffset = spinner.Position - prevPos; + Vector2 spinCentreOffset = SPINNER_CENTRE - prevPos; float distFromCentre = spinCentreOffset.Length; float distToTangentPoint = MathF.Sqrt(distFromCentre * distFromCentre - SPIN_RADIUS * SPIN_RADIUS); @@ -216,13 +216,13 @@ namespace osu.Game.Rulesets.Osu.Replays else if (spinCentreOffset.Length > 0) { // Previous cursor position was inside spin circle, set startPosition to the nearest point on spin circle. - startPosition = spinner.Position - spinCentreOffset * (SPIN_RADIUS / spinCentreOffset.Length); + startPosition = SPINNER_CENTRE - spinCentreOffset * (SPIN_RADIUS / spinCentreOffset.Length); spinnerDirection = 1; } else { // Degenerate case where cursor position is exactly at the centre of the spin circle. - startPosition = spinner.Position + new Vector2(0, -SPIN_RADIUS); + startPosition = SPINNER_CENTRE + new Vector2(0, -SPIN_RADIUS); spinnerDirection = 1; } } @@ -335,7 +335,7 @@ namespace osu.Game.Rulesets.Osu.Replays { // We add intermediate frames for spinning / following a slider here. case Spinner spinner: - Vector2 difference = startPosition - spinner.Position; + Vector2 difference = startPosition - SPINNER_CENTRE; float radius = difference.Length; float angle = radius == 0 ? 0 : MathF.Atan2(difference.Y, difference.X); @@ -348,7 +348,7 @@ namespace osu.Game.Rulesets.Osu.Replays t = ApplyModsToTimeDelta(previousFrame, nextFrame) * spinnerDirection; angle += (float)t / 20; - Vector2 pos = spinner.Position + CirclePosition(angle, SPIN_RADIUS); + Vector2 pos = SPINNER_CENTRE + CirclePosition(angle, SPIN_RADIUS); AddFrameToReplay(new OsuReplayFrame((int)nextFrame, new Vector2(pos.X, pos.Y), action)); previousFrame = nextFrame; @@ -357,7 +357,7 @@ namespace osu.Game.Rulesets.Osu.Replays t = ApplyModsToTimeDelta(previousFrame, spinner.EndTime) * spinnerDirection; angle += (float)t / 20; - Vector2 endPosition = spinner.Position + CirclePosition(angle, SPIN_RADIUS); + Vector2 endPosition = SPINNER_CENTRE + CirclePosition(angle, SPIN_RADIUS); AddFrameToReplay(new OsuReplayFrame(spinner.EndTime, new Vector2(endPosition.X, endPosition.Y), action)); diff --git a/osu.Game.Rulesets.Osu/Replays/OsuAutoGeneratorBase.cs b/osu.Game.Rulesets.Osu/Replays/OsuAutoGeneratorBase.cs index 69eb669a8e..1cb3208c30 100644 --- a/osu.Game.Rulesets.Osu/Replays/OsuAutoGeneratorBase.cs +++ b/osu.Game.Rulesets.Osu/Replays/OsuAutoGeneratorBase.cs @@ -8,6 +8,7 @@ using System.Collections.Generic; using System.Linq; using osu.Game.Replays; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Replays; namespace osu.Game.Rulesets.Osu.Replays @@ -19,6 +20,8 @@ namespace osu.Game.Rulesets.Osu.Replays /// /// Constants (for spinners). /// + protected static readonly Vector2 SPINNER_CENTRE = OsuPlayfield.BASE_SIZE / 2; + public const float SPIN_RADIUS = 50; #endregion From 1dfe028c0209d4ef4cac6c3c2462af22797d81a9 Mon Sep 17 00:00:00 2001 From: Pasi4K5 Date: Mon, 26 Apr 2021 22:26:13 +0200 Subject: [PATCH 09/33] Fix bug causing the star rating to change when Random is enabled --- osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs | 2 +- osu.Game/Beatmaps/WorkingBeatmap.cs | 10 ++++++++++ .../Mods/IApplicableToBeatmapKeepStarRating.cs | 14 ++++++++++++++ osu.Game/Screens/Play/Player.cs | 2 ++ 4 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 osu.Game/Rulesets/Mods/IApplicableToBeatmapKeepStarRating.cs diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs index 1d430ba711..3a4b5073b5 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs @@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Osu.Mods /// /// Mod that randomises the positions of the s /// - public class OsuModRandom : ModRandom, IApplicableToBeatmap + public class OsuModRandom : ModRandom, IApplicableToBeatmapKeepStarRating { public override string Description => "It never gets boring!"; public override bool Ranked => false; diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index e0eeaf6db0..d49f6ed50b 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -32,6 +32,11 @@ namespace osu.Game.Beatmaps public readonly BeatmapMetadata Metadata; + /// + /// Only if this is set to true, changes made by mods that implement will be applied. + /// + public bool ApplyChangesToBeatmap; + protected AudioManager AudioManager { get; } protected WorkingBeatmap(BeatmapInfo beatmapInfo, AudioManager audioManager) @@ -166,10 +171,15 @@ namespace osu.Game.Beatmaps foreach (var mod in mods.OfType()) { + if (mod is IApplicableToBeatmapKeepStarRating && !ApplyChangesToBeatmap) + continue; + cancellationSource.Token.ThrowIfCancellationRequested(); mod.ApplyToBeatmap(converted); } + ApplyChangesToBeatmap = false; + return converted; } } diff --git a/osu.Game/Rulesets/Mods/IApplicableToBeatmapKeepStarRating.cs b/osu.Game/Rulesets/Mods/IApplicableToBeatmapKeepStarRating.cs new file mode 100644 index 0000000000..34db7e1be3 --- /dev/null +++ b/osu.Game/Rulesets/Mods/IApplicableToBeatmapKeepStarRating.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.Game.Beatmaps; + +namespace osu.Game.Rulesets.Mods +{ + /// + /// Interface for a that applies changes to a after conversion and post-processing has completed without changing its difficulty + /// + public interface IApplicableToBeatmapKeepStarRating : IApplicableToBeatmap + { + } +} diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 27a4fcc291..80eec64884 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -455,6 +455,8 @@ namespace osu.Game.Screens.Play rulesetInfo = Ruleset.Value ?? Beatmap.Value.BeatmapInfo.Ruleset; ruleset = rulesetInfo.CreateInstance(); + Beatmap.Value.ApplyChangesToBeatmap = true; + try { playable = Beatmap.Value.GetPlayableBeatmap(ruleset.RulesetInfo, Mods.Value); From 4b05568d2d0159e98eb0b7664e87bad48fe8200a Mon Sep 17 00:00:00 2001 From: Pasi4K5 Date: Tue, 27 Apr 2021 19:39:58 +0200 Subject: [PATCH 10/33] Revert "Fix bug causing the star rating to change when Random is enabled" This reverts commit 1dfe028c0209d4ef4cac6c3c2462af22797d81a9. --- osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs | 2 +- osu.Game/Beatmaps/WorkingBeatmap.cs | 10 ---------- .../Mods/IApplicableToBeatmapKeepStarRating.cs | 14 -------------- osu.Game/Screens/Play/Player.cs | 2 -- 4 files changed, 1 insertion(+), 27 deletions(-) delete mode 100644 osu.Game/Rulesets/Mods/IApplicableToBeatmapKeepStarRating.cs diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs index 3a4b5073b5..1d430ba711 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs @@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Osu.Mods /// /// Mod that randomises the positions of the s /// - public class OsuModRandom : ModRandom, IApplicableToBeatmapKeepStarRating + public class OsuModRandom : ModRandom, IApplicableToBeatmap { public override string Description => "It never gets boring!"; public override bool Ranked => false; diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index d49f6ed50b..e0eeaf6db0 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -32,11 +32,6 @@ namespace osu.Game.Beatmaps public readonly BeatmapMetadata Metadata; - /// - /// Only if this is set to true, changes made by mods that implement will be applied. - /// - public bool ApplyChangesToBeatmap; - protected AudioManager AudioManager { get; } protected WorkingBeatmap(BeatmapInfo beatmapInfo, AudioManager audioManager) @@ -171,15 +166,10 @@ namespace osu.Game.Beatmaps foreach (var mod in mods.OfType()) { - if (mod is IApplicableToBeatmapKeepStarRating && !ApplyChangesToBeatmap) - continue; - cancellationSource.Token.ThrowIfCancellationRequested(); mod.ApplyToBeatmap(converted); } - ApplyChangesToBeatmap = false; - return converted; } } diff --git a/osu.Game/Rulesets/Mods/IApplicableToBeatmapKeepStarRating.cs b/osu.Game/Rulesets/Mods/IApplicableToBeatmapKeepStarRating.cs deleted file mode 100644 index 34db7e1be3..0000000000 --- a/osu.Game/Rulesets/Mods/IApplicableToBeatmapKeepStarRating.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.Game.Beatmaps; - -namespace osu.Game.Rulesets.Mods -{ - /// - /// Interface for a that applies changes to a after conversion and post-processing has completed without changing its difficulty - /// - public interface IApplicableToBeatmapKeepStarRating : IApplicableToBeatmap - { - } -} diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 80eec64884..27a4fcc291 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -455,8 +455,6 @@ namespace osu.Game.Screens.Play rulesetInfo = Ruleset.Value ?? Beatmap.Value.BeatmapInfo.Ruleset; ruleset = rulesetInfo.CreateInstance(); - Beatmap.Value.ApplyChangesToBeatmap = true; - try { playable = Beatmap.Value.GetPlayableBeatmap(ruleset.RulesetInfo, Mods.Value); From a141a4e9e6641e322740240258179d5b06718b0b Mon Sep 17 00:00:00 2001 From: Pasi4K5 Date: Tue, 27 Apr 2021 20:44:36 +0200 Subject: [PATCH 11/33] Add setting "Seed" Random numbers are now generated with the seed specified in the mod settings. --- osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs index 1d430ba711..675aa4a0b3 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs @@ -2,7 +2,10 @@ // See the LICENCE file in the repository root for full licence text. using System; +using osu.Framework.Bindables; +using osu.Framework.Logging; using osu.Game.Beatmaps; +using osu.Game.Configuration; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Objects; @@ -24,9 +27,23 @@ namespace osu.Game.Rulesets.Osu.Mods private const byte border_distance_x = 128; private const byte border_distance_y = 96; + [SettingSource("Seed", "Seed for the random number generator")] + public Bindable Seed { get; } = new Bindable + { + Value = "0" + }; + public void ApplyToBeatmap(IBeatmap beatmap) { - var rng = new Random(); + if (!int.TryParse(Seed.Value, out var seed)) + { + var e = new FormatException("Seed must be an integer"); + Logger.Error(e, "Could not load beatmap: RNG seed must be an integer."); + + return; + } + + var rng = new Random(seed); // Absolute angle float prevAngleRad = 0; From 95040f7edc42262fd43cfddac8a4315bc0c402a6 Mon Sep 17 00:00:00 2001 From: Pasi4K5 Date: Tue, 27 Apr 2021 22:19:04 +0200 Subject: [PATCH 12/33] Change initial seed to a random number --- osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs index 675aa4a0b3..0124a3c28e 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs @@ -4,6 +4,7 @@ using System; using osu.Framework.Bindables; using osu.Framework.Logging; +using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Rulesets.Mods; @@ -30,7 +31,7 @@ namespace osu.Game.Rulesets.Osu.Mods [SettingSource("Seed", "Seed for the random number generator")] public Bindable Seed { get; } = new Bindable { - Value = "0" + Value = RNG.Next().ToString() }; public void ApplyToBeatmap(IBeatmap beatmap) From 6bed268bd8ff7719b36bdba5e253de0291c6deaa Mon Sep 17 00:00:00 2001 From: Pasi4K5 Date: Sat, 1 May 2021 04:01:43 +0200 Subject: [PATCH 13/33] Enhance mod settings and add option "Random seed" + slight adjustments --- osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs | 161 ++++++++++++++++++--- 1 file changed, 144 insertions(+), 17 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs index 0124a3c28e..9a6127cdad 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs @@ -2,13 +2,20 @@ // See the LICENCE file in the repository root for full licence text. using System; +using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Logging; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Platform; using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Configuration; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays.Settings; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.UI; using osuTK; @@ -25,36 +32,58 @@ namespace osu.Game.Rulesets.Osu.Mods // The distances from the hit objects to the borders of the playfield they start to "turn around" and curve towards the middle. // The closer the hit objects draw to the border, the sharper the turn - private const byte border_distance_x = 128; - private const byte border_distance_y = 96; + private const byte border_distance_x = 192; + private const byte border_distance_y = 144; - [SettingSource("Seed", "Seed for the random number generator")] - public Bindable Seed { get; } = new Bindable + private static readonly Bindable seed = new Bindable { - Value = RNG.Next().ToString() + Default = -1 }; - public void ApplyToBeatmap(IBeatmap beatmap) + private static readonly BindableBool random_seed = new BindableBool { - if (!int.TryParse(Seed.Value, out var seed)) - { - var e = new FormatException("Seed must be an integer"); - Logger.Error(e, "Could not load beatmap: RNG seed must be an integer."); + Value = true, + Default = true + }; + [SettingSource("Random seed", "Generate a random seed for the beatmap generation")] + public BindableBool RandomSeed => random_seed; + + [SettingSource("Seed", "Seed for the random beatmap generation", SettingControlType = typeof(OsuModRandomSettingsControl))] + public Bindable Seed => seed; + + internal static bool CustomSeedDisabled => random_seed.Value; + + public OsuModRandom() + { + if (seed.Default != -1) return; - } - var rng = new Random(seed); + var random = RNG.Next(); + seed.Value = random; + seed.Default = random; + seed.BindValueChanged(e => seed.Default = e.NewValue); + } + + public void ApplyToBeatmap(IBeatmap iBeatmap) + { + if (!(iBeatmap is OsuBeatmap beatmap)) + return; + + if (RandomSeed.Value) + seed.Value = RNG.Next(); + + var rng = new Random(seed.Value); // Absolute angle float prevAngleRad = 0; // Absolute positions - Vector2 prevPosUnchanged = ((OsuHitObject)beatmap.HitObjects[0]).Position; - Vector2 prevPosChanged = ((OsuHitObject)beatmap.HitObjects[0]).Position; + Vector2 prevPosUnchanged = beatmap.HitObjects[0].Position; + Vector2 prevPosChanged = beatmap.HitObjects[0].Position; // rateOfChangeMultiplier changes every i iterations to prevent shaky-line-shaped streams - byte i = 5; + byte i = 3; float rateOfChangeMultiplier = 0; foreach (var beatmapHitObject in beatmap.HitObjects) @@ -69,7 +98,7 @@ namespace osu.Game.Rulesets.Osu.Mods // Angle of the vector pointing from the last to the current hit object float angleRad = 0; - if (i >= 5) + if (i >= 3) { i = 0; rateOfChangeMultiplier = (float)rng.NextDouble() * 2 - 1; @@ -234,4 +263,102 @@ namespace osu.Game.Rulesets.Osu.Mods ); } } + + public class OsuModRandomSettingsControl : SettingsItem + { + [Resolved] + private static GameHost host { get; set; } + + [BackgroundDependencyLoader] + private void load(GameHost gameHost) => host = gameHost; + + protected override Drawable CreateControl() => new SeedControl + { + RelativeSizeAxes = Axes.X, + Margin = new MarginPadding { Top = 5 } + }; + + private sealed class SeedControl : CompositeDrawable, IHasCurrentValue + { + private readonly BindableWithCurrent current = new BindableWithCurrent(); + + public Bindable Current + { + get => current; + set => Scheduler.Add(() => current.Current = value); + } + + private readonly OsuNumberBox seedNumberBox; + + public SeedControl() + { + AutoSizeAxes = Axes.Y; + + InternalChildren = new[] + { + new GridContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + ColumnDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.Absolute, 2), + new Dimension(GridSizeMode.Relative, 0.25f) + }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize) + }, + Content = new[] + { + new Drawable[] + { + seedNumberBox = new OsuNumberBox + { + RelativeSizeAxes = Axes.X, + CommitOnFocusLost = true + }, + null, + new TriangleButton + { + RelativeSizeAxes = Axes.Both, + Height = 1, + Text = "Copy", + Action = copySeedToClipboard + } + } + } + } + }; + + seedNumberBox.Current.BindValueChanged(onTextBoxValueChanged); + } + + private void onTextBoxValueChanged(ValueChangedEvent e) + { + string seed = e.NewValue; + + while (!string.IsNullOrEmpty(seed) && !int.TryParse(seed, out _)) + seed = seed[..^1]; + + if (!int.TryParse(seed, out var intVal)) + intVal = 0; + + current.Value = intVal; + } + + private void copySeedToClipboard() => host.GetClipboard().SetText(seedNumberBox.Text); + + protected override void Update() + { + seedNumberBox.ReadOnly = OsuModRandom.CustomSeedDisabled; + + if (seedNumberBox.HasFocus) + return; + + seedNumberBox.Text = current.Current.Value.ToString(); + } + } + } } From 946abfbb83d89caea8e7273901a1e1a020824813 Mon Sep 17 00:00:00 2001 From: Pasi4K5 Date: Wed, 12 May 2021 18:11:50 +0200 Subject: [PATCH 14/33] Rework settings; Add seed to ScorePanel; Apply requested changes from @bdach --- osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs | 200 +++++++----------- osu.Game/Rulesets/Mods/ModRandom.cs | 1 + osu.Game/Scoring/ScoreInfo.cs | 18 ++ .../Expanded/ExpandedPanelMiddleContent.cs | 47 +++- osu.Game/Screens/Ranking/ScorePanel.cs | 8 +- 5 files changed, 152 insertions(+), 122 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs index 9a6127cdad..2d7c52d535 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs @@ -35,68 +35,46 @@ namespace osu.Game.Rulesets.Osu.Mods private const byte border_distance_x = 192; private const byte border_distance_y = 144; - private static readonly Bindable seed = new Bindable + [SettingSource("Custom seed", "Use a custom seed instead of a random one", SettingControlType = typeof(OsuModRandomSettingsControl))] + public Bindable CustomSeed { get; } = new Bindable { - Default = -1 + Default = null, + Value = null }; - private static readonly BindableBool random_seed = new BindableBool + public void ApplyToBeatmap(IBeatmap beatmap) { - Value = true, - Default = true - }; - - [SettingSource("Random seed", "Generate a random seed for the beatmap generation")] - public BindableBool RandomSeed => random_seed; - - [SettingSource("Seed", "Seed for the random beatmap generation", SettingControlType = typeof(OsuModRandomSettingsControl))] - public Bindable Seed => seed; - - internal static bool CustomSeedDisabled => random_seed.Value; - - public OsuModRandom() - { - if (seed.Default != -1) + if (!(beatmap is OsuBeatmap osuBeatmap)) return; - var random = RNG.Next(); - seed.Value = random; - seed.Default = random; - seed.BindValueChanged(e => seed.Default = e.NewValue); - } + var seed = RNG.Next(); - public void ApplyToBeatmap(IBeatmap iBeatmap) - { - if (!(iBeatmap is OsuBeatmap beatmap)) - return; + if (CustomSeed.Value != null) + seed = (int)CustomSeed.Value; - if (RandomSeed.Value) - seed.Value = RNG.Next(); + Seed = seed; - var rng = new Random(seed.Value); + var rng = new Random(seed); - // Absolute angle - float prevAngleRad = 0; - - // Absolute positions - Vector2 prevPosUnchanged = beatmap.HitObjects[0].Position; - Vector2 prevPosChanged = beatmap.HitObjects[0].Position; + var prevObjectInfo = new HitObjectInfo + { + AngleRad = 0, + PosUnchanged = osuBeatmap.HitObjects[0].Position, + PosChanged = osuBeatmap.HitObjects[0].Position + }; // rateOfChangeMultiplier changes every i iterations to prevent shaky-line-shaped streams byte i = 3; float rateOfChangeMultiplier = 0; - foreach (var beatmapHitObject in beatmap.HitObjects) + foreach (var currentHitObject in osuBeatmap.HitObjects) { - if (!(beatmapHitObject is OsuHitObject hitObject)) - return; - - // posUnchanged: position from the original beatmap (not randomised) - var posUnchanged = hitObject.EndPosition; - var posChanged = Vector2.Zero; - - // Angle of the vector pointing from the last to the current hit object - float angleRad = 0; + var currentObjectInfo = new HitObjectInfo + { + AngleRad = 0, + PosUnchanged = currentHitObject.EndPosition, + PosChanged = Vector2.Zero + }; if (i >= 3) { @@ -104,24 +82,23 @@ namespace osu.Game.Rulesets.Osu.Mods rateOfChangeMultiplier = (float)rng.NextDouble() * 2 - 1; } - if (hitObject is HitCircle circle) + if (currentHitObject is HitCircle circle) { - var distanceToPrev = Vector2.Distance(posUnchanged, prevPosUnchanged); + var distanceToPrev = Vector2.Distance(currentObjectInfo.PosUnchanged, prevObjectInfo.PosUnchanged); - circle.Position = posChanged = getRandomisedPosition( + getObjectInfo( rateOfChangeMultiplier, - prevPosChanged, - prevAngleRad, + prevObjectInfo, distanceToPrev, - out angleRad + ref currentObjectInfo ); + + circle.Position = currentObjectInfo.PosChanged; } // TODO: Implement slider position randomisation - prevAngleRad = angleRad; - prevPosUnchanged = posUnchanged; - prevPosChanged = posChanged; + prevObjectInfo = currentObjectInfo; i++; } } @@ -130,12 +107,11 @@ namespace osu.Game.Rulesets.Osu.Mods /// Returns the final position of the hit object /// /// Final position of the hit object - private Vector2 getRandomisedPosition( + private void getObjectInfo( float rateOfChangeMultiplier, - Vector2 prevPosChanged, - float prevAngleRad, + HitObjectInfo prevObjectInfo, float distanceToPrev, - out float newAngle) + ref HitObjectInfo currentObjectInfo) { // The max. angle (relative to the angle of the vector pointing from the 2nd last to the last hit object) // is proportional to the distance between the last and the current hit object @@ -143,19 +119,19 @@ namespace osu.Game.Rulesets.Osu.Mods var maxDistance = OsuPlayfield.BASE_SIZE.LengthFast; var randomAngleRad = rateOfChangeMultiplier * 2 * Math.PI * distanceToPrev / maxDistance; - newAngle = (float)randomAngleRad + prevAngleRad; - if (newAngle < 0) - newAngle += 2 * (float)Math.PI; + currentObjectInfo.AngleRad = (float)randomAngleRad + prevObjectInfo.AngleRad; + if (currentObjectInfo.AngleRad < 0) + currentObjectInfo.AngleRad += 2 * (float)Math.PI; var posRelativeToPrev = new Vector2( - distanceToPrev * (float)Math.Cos(newAngle), - distanceToPrev * (float)Math.Sin(newAngle) + distanceToPrev * (float)Math.Cos(currentObjectInfo.AngleRad), + distanceToPrev * (float)Math.Sin(currentObjectInfo.AngleRad) ); - posRelativeToPrev = getRotatedVector(prevPosChanged, posRelativeToPrev); + posRelativeToPrev = getRotatedVector(prevObjectInfo.PosChanged, posRelativeToPrev); - newAngle = (float)Math.Atan2(posRelativeToPrev.Y, posRelativeToPrev.X); - var position = Vector2.Add(prevPosChanged, posRelativeToPrev); + currentObjectInfo.AngleRad = (float)Math.Atan2(posRelativeToPrev.Y, posRelativeToPrev.X); + var position = Vector2.Add(prevObjectInfo.PosChanged, posRelativeToPrev); // Move hit objects back into the playfield if they are outside of it, // which would sometimes happen during big jumps otherwise. @@ -169,7 +145,7 @@ namespace osu.Game.Rulesets.Osu.Mods else if (position.Y > OsuPlayfield.BASE_SIZE.Y) position.Y = OsuPlayfield.BASE_SIZE.Y; - return position; + currentObjectInfo.PosChanged = position; } /// @@ -214,7 +190,7 @@ namespace osu.Game.Rulesets.Osu.Mods return rotateVectorTowardsVector( posRelativeToPrev, Vector2.Subtract(playfieldMiddle, prevPosChanged), - relativeRotationDistance + relativeRotationDistance / 2 ); } @@ -230,10 +206,6 @@ namespace osu.Game.Rulesets.Osu.Mods var initialAngleRad = Math.Atan2(initial.Y, initial.X); var destAngleRad = Math.Atan2(destination.Y, destination.X); - // Divide by 2 to limit the max. angle to 90° - // (90° is enough to prevent the hit objects from leaving the playfield) - relativeDistance /= 2; - var diff = destAngleRad - initialAngleRad; while (diff < -Math.PI) @@ -246,46 +218,45 @@ namespace osu.Game.Rulesets.Osu.Mods diff -= 2 * Math.PI; } - var finalAngle = 0d; - - if (diff > 0) - { - finalAngle = initialAngleRad + relativeDistance * diff; - } - else if (diff < 0) - { - finalAngle = initialAngleRad + relativeDistance * diff; - } + var finalAngleRad = initialAngleRad + relativeDistance * diff; return new Vector2( - initial.Length * (float)Math.Cos(finalAngle), - initial.Length * (float)Math.Sin(finalAngle) + initial.Length * (float)Math.Cos(finalAngleRad), + initial.Length * (float)Math.Sin(finalAngleRad) ); } + + private struct HitObjectInfo + { + internal float AngleRad { get; set; } + internal Vector2 PosUnchanged { get; set; } + internal Vector2 PosChanged { get; set; } + } } - public class OsuModRandomSettingsControl : SettingsItem + public class OsuModRandomSettingsControl : SettingsItem { - [Resolved] - private static GameHost host { get; set; } - - [BackgroundDependencyLoader] - private void load(GameHost gameHost) => host = gameHost; - protected override Drawable CreateControl() => new SeedControl { RelativeSizeAxes = Axes.X, Margin = new MarginPadding { Top = 5 } }; - private sealed class SeedControl : CompositeDrawable, IHasCurrentValue + private sealed class SeedControl : CompositeDrawable, IHasCurrentValue { - private readonly BindableWithCurrent current = new BindableWithCurrent(); + [Resolved] + private GameHost host { get; set; } - public Bindable Current + private readonly BindableWithCurrent current = new BindableWithCurrent(); + + public Bindable Current { get => current; - set => Scheduler.Add(() => current.Current = value); + set + { + current.Current = value; + seedNumberBox.Text = value.Value.ToString(); + } } private readonly OsuNumberBox seedNumberBox; @@ -324,40 +295,29 @@ namespace osu.Game.Rulesets.Osu.Mods { RelativeSizeAxes = Axes.Both, Height = 1, - Text = "Copy", - Action = copySeedToClipboard + Text = "Paste", + Action = () => seedNumberBox.Text = host.GetClipboard().GetText() } } } } }; - seedNumberBox.Current.BindValueChanged(onTextBoxValueChanged); + seedNumberBox.Current.BindValueChanged(e => + { + int? value = null; + + if (int.TryParse(e.NewValue, out var intVal)) + value = intVal; + + current.Value = value; + }); } - private void onTextBoxValueChanged(ValueChangedEvent e) - { - string seed = e.NewValue; - - while (!string.IsNullOrEmpty(seed) && !int.TryParse(seed, out _)) - seed = seed[..^1]; - - if (!int.TryParse(seed, out var intVal)) - intVal = 0; - - current.Value = intVal; - } - - private void copySeedToClipboard() => host.GetClipboard().SetText(seedNumberBox.Text); - protected override void Update() { - seedNumberBox.ReadOnly = OsuModRandom.CustomSeedDisabled; - - if (seedNumberBox.HasFocus) - return; - - seedNumberBox.Text = current.Current.Value.ToString(); + if (current.Value == null) + seedNumberBox.Text = current.Current.Value.ToString(); } } } diff --git a/osu.Game/Rulesets/Mods/ModRandom.cs b/osu.Game/Rulesets/Mods/ModRandom.cs index da55ab3fbf..382792f75c 100644 --- a/osu.Game/Rulesets/Mods/ModRandom.cs +++ b/osu.Game/Rulesets/Mods/ModRandom.cs @@ -13,5 +13,6 @@ namespace osu.Game.Rulesets.Mods public override ModType Type => ModType.Conversion; public override IconUsage? Icon => OsuIcon.Dice; public override double ScoreMultiplier => 1; + public int? Seed { get; protected set; } } } diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index a6faaf6379..b584d24370 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -88,6 +88,24 @@ namespace osu.Game.Scoring } } + public bool ContainsModOfType(out T mod) + { + if (mods != null) + { + foreach (var currentMod in mods) + { + if (!(currentMod is T modOfType)) + continue; + + mod = modOfType; + return true; + } + } + + mod = default; + return false; + } + // Used for API serialisation/deserialisation. [JsonProperty("mods")] [NotMapped] diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs index 6a6b39b61c..4d81290a75 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs @@ -7,11 +7,13 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Localisation; +using osu.Framework.Platform; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Screens.Play.HUD; @@ -55,6 +57,9 @@ namespace osu.Game.Screens.Ranking.Expanded Padding = new MarginPadding(padding); } + [Resolved] + private GameHost host { get; set; } + [BackgroundDependencyLoader] private void load(BeatmapDifficultyCache beatmapDifficultyCache) { @@ -224,7 +229,47 @@ namespace osu.Game.Screens.Ranking.Expanded } } } - } + }.With(t => + { + if (!score.ContainsModOfType(out var mod) || mod.Seed == null) + return; + + t.Add(new GridContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Content = new[] + { + new Drawable[] + { + new OsuSpriteText + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Margin = new MarginPadding + { + Top = 3f, + Right = 5f + }, + Font = OsuFont.GetFont(size: 12, weight: FontWeight.Medium), + Text = $"Seed: {mod.Seed}" + }, + new TriangleButton + { + RelativeSizeAxes = Axes.Both, + Height = 1.2f, + Width = 0.5f, + Text = "Copy", + Action = () => host.GetClipboard().SetText(mod.Seed.ToString()) + } + } + }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + } + }); + }) } }, new OsuSpriteText diff --git a/osu.Game/Screens/Ranking/ScorePanel.cs b/osu.Game/Screens/Ranking/ScorePanel.cs index df710e4eb8..33b06571fe 100644 --- a/osu.Game/Screens/Ranking/ScorePanel.cs +++ b/osu.Game/Screens/Ranking/ScorePanel.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; +using osu.Game.Rulesets.Mods; using osu.Game.Scoring; using osu.Game.Screens.Ranking.Contracted; using osu.Game.Screens.Ranking.Expanded; @@ -206,7 +207,12 @@ namespace osu.Game.Screens.Ranking switch (state) { case PanelState.Expanded: - Size = new Vector2(EXPANDED_WIDTH, expanded_height); + var height = expanded_height; + + if (Score.ContainsModOfType(out var mod) && mod.Seed != null) + height += 20f; + + Size = new Vector2(EXPANDED_WIDTH, height); topLayerBackground.FadeColour(expanded_top_layer_colour, resize_duration, Easing.OutQuint); middleLayerBackground.FadeColour(expanded_middle_layer_colour, resize_duration, Easing.OutQuint); From a9d5211e81593f30be9f7d7c5a91218a52b47a2a Mon Sep 17 00:00:00 2001 From: Pasi4K5 Date: Fri, 14 May 2021 01:42:39 +0200 Subject: [PATCH 15/33] Remove seed from the ScorePanel and "Paste" button --- osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs | 15 ------ osu.Game/Rulesets/Mods/ModRandom.cs | 1 - osu.Game/Scoring/ScoreInfo.cs | 18 ------- .../Expanded/ExpandedPanelMiddleContent.cs | 47 +------------------ osu.Game/Screens/Ranking/ScorePanel.cs | 8 +--- 5 files changed, 2 insertions(+), 87 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs index 2d7c52d535..1b8f8ee7f5 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs @@ -2,12 +2,10 @@ // See the LICENCE file in the repository root for full licence text. using System; -using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.UserInterface; -using osu.Framework.Platform; using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Configuration; @@ -52,8 +50,6 @@ namespace osu.Game.Rulesets.Osu.Mods if (CustomSeed.Value != null) seed = (int)CustomSeed.Value; - Seed = seed; - var rng = new Random(seed); var prevObjectInfo = new HitObjectInfo @@ -244,9 +240,6 @@ namespace osu.Game.Rulesets.Osu.Mods private sealed class SeedControl : CompositeDrawable, IHasCurrentValue { - [Resolved] - private GameHost host { get; set; } - private readonly BindableWithCurrent current = new BindableWithCurrent(); public Bindable Current @@ -289,14 +282,6 @@ namespace osu.Game.Rulesets.Osu.Mods { RelativeSizeAxes = Axes.X, CommitOnFocusLost = true - }, - null, - new TriangleButton - { - RelativeSizeAxes = Axes.Both, - Height = 1, - Text = "Paste", - Action = () => seedNumberBox.Text = host.GetClipboard().GetText() } } } diff --git a/osu.Game/Rulesets/Mods/ModRandom.cs b/osu.Game/Rulesets/Mods/ModRandom.cs index 382792f75c..da55ab3fbf 100644 --- a/osu.Game/Rulesets/Mods/ModRandom.cs +++ b/osu.Game/Rulesets/Mods/ModRandom.cs @@ -13,6 +13,5 @@ namespace osu.Game.Rulesets.Mods public override ModType Type => ModType.Conversion; public override IconUsage? Icon => OsuIcon.Dice; public override double ScoreMultiplier => 1; - public int? Seed { get; protected set; } } } diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index b584d24370..a6faaf6379 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -88,24 +88,6 @@ namespace osu.Game.Scoring } } - public bool ContainsModOfType(out T mod) - { - if (mods != null) - { - foreach (var currentMod in mods) - { - if (!(currentMod is T modOfType)) - continue; - - mod = modOfType; - return true; - } - } - - mod = default; - return false; - } - // Used for API serialisation/deserialisation. [JsonProperty("mods")] [NotMapped] diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs index 4d81290a75..6a6b39b61c 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs @@ -7,13 +7,11 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Localisation; -using osu.Framework.Platform; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; -using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Screens.Play.HUD; @@ -57,9 +55,6 @@ namespace osu.Game.Screens.Ranking.Expanded Padding = new MarginPadding(padding); } - [Resolved] - private GameHost host { get; set; } - [BackgroundDependencyLoader] private void load(BeatmapDifficultyCache beatmapDifficultyCache) { @@ -229,47 +224,7 @@ namespace osu.Game.Screens.Ranking.Expanded } } } - }.With(t => - { - if (!score.ContainsModOfType(out var mod) || mod.Seed == null) - return; - - t.Add(new GridContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Content = new[] - { - new Drawable[] - { - new OsuSpriteText - { - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - Margin = new MarginPadding - { - Top = 3f, - Right = 5f - }, - Font = OsuFont.GetFont(size: 12, weight: FontWeight.Medium), - Text = $"Seed: {mod.Seed}" - }, - new TriangleButton - { - RelativeSizeAxes = Axes.Both, - Height = 1.2f, - Width = 0.5f, - Text = "Copy", - Action = () => host.GetClipboard().SetText(mod.Seed.ToString()) - } - } - }, - RowDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize), - } - }); - }) + } } }, new OsuSpriteText diff --git a/osu.Game/Screens/Ranking/ScorePanel.cs b/osu.Game/Screens/Ranking/ScorePanel.cs index 33b06571fe..df710e4eb8 100644 --- a/osu.Game/Screens/Ranking/ScorePanel.cs +++ b/osu.Game/Screens/Ranking/ScorePanel.cs @@ -10,7 +10,6 @@ using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; -using osu.Game.Rulesets.Mods; using osu.Game.Scoring; using osu.Game.Screens.Ranking.Contracted; using osu.Game.Screens.Ranking.Expanded; @@ -207,12 +206,7 @@ namespace osu.Game.Screens.Ranking switch (state) { case PanelState.Expanded: - var height = expanded_height; - - if (Score.ContainsModOfType(out var mod) && mod.Seed != null) - height += 20f; - - Size = new Vector2(EXPANDED_WIDTH, height); + Size = new Vector2(EXPANDED_WIDTH, expanded_height); topLayerBackground.FadeColour(expanded_top_layer_colour, resize_duration, Easing.OutQuint); middleLayerBackground.FadeColour(expanded_middle_layer_colour, resize_duration, Easing.OutQuint); From ac04e8afa28a8bc5041176f0c59f8c77a109d2f2 Mon Sep 17 00:00:00 2001 From: Pasi4K5 Date: Fri, 14 May 2021 01:50:11 +0200 Subject: [PATCH 16/33] Change name of option "Custom seed" to "Seed" and set its value to the generated seed --- osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs index 1b8f8ee7f5..1141de58a2 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs @@ -33,8 +33,8 @@ namespace osu.Game.Rulesets.Osu.Mods private const byte border_distance_x = 192; private const byte border_distance_y = 144; - [SettingSource("Custom seed", "Use a custom seed instead of a random one", SettingControlType = typeof(OsuModRandomSettingsControl))] - public Bindable CustomSeed { get; } = new Bindable + [SettingSource("Seed", "Use a custom seed instead of a random one", SettingControlType = typeof(OsuModRandomSettingsControl))] + public Bindable Seed { get; } = new Bindable { Default = null, Value = null @@ -45,12 +45,9 @@ namespace osu.Game.Rulesets.Osu.Mods if (!(beatmap is OsuBeatmap osuBeatmap)) return; - var seed = RNG.Next(); + Seed.Value ??= RNG.Next(); - if (CustomSeed.Value != null) - seed = (int)CustomSeed.Value; - - var rng = new Random(seed); + var rng = new Random((int)Seed.Value); var prevObjectInfo = new HitObjectInfo { From dbc23187105279560e8a7bb26d206a450cf4604d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 May 2021 14:13:35 +0900 Subject: [PATCH 17/33] Initial tidying up --- osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs | 29 +++++++++------------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs index 1141de58a2..dc194aa464 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs @@ -45,6 +45,8 @@ namespace osu.Game.Rulesets.Osu.Mods if (!(beatmap is OsuBeatmap osuBeatmap)) return; + var hitObjects = osuBeatmap.HitObjects; + Seed.Value ??= RNG.Next(); var rng = new Random((int)Seed.Value); @@ -52,30 +54,28 @@ namespace osu.Game.Rulesets.Osu.Mods var prevObjectInfo = new HitObjectInfo { AngleRad = 0, - PosUnchanged = osuBeatmap.HitObjects[0].Position, - PosChanged = osuBeatmap.HitObjects[0].Position + PosUnchanged = hitObjects[0].Position, + PosChanged = hitObjects[0].Position }; - // rateOfChangeMultiplier changes every i iterations to prevent shaky-line-shaped streams - byte i = 3; float rateOfChangeMultiplier = 0; - foreach (var currentHitObject in osuBeatmap.HitObjects) + for (int i = 0; i < hitObjects.Count; i++) { + var h = hitObjects[i]; + var currentObjectInfo = new HitObjectInfo { AngleRad = 0, - PosUnchanged = currentHitObject.EndPosition, + PosUnchanged = h.EndPosition, PosChanged = Vector2.Zero }; - if (i >= 3) - { - i = 0; + // rateOfChangeMultiplier only changes every i iterations to prevent shaky-line-shaped streams + if (i % 3 == 0) rateOfChangeMultiplier = (float)rng.NextDouble() * 2 - 1; - } - if (currentHitObject is HitCircle circle) + if (h is HitCircle circle) { var distanceToPrev = Vector2.Distance(currentObjectInfo.PosUnchanged, prevObjectInfo.PosUnchanged); @@ -92,7 +92,6 @@ namespace osu.Game.Rulesets.Osu.Mods // TODO: Implement slider position randomisation prevObjectInfo = currentObjectInfo; - i++; } } @@ -100,11 +99,7 @@ namespace osu.Game.Rulesets.Osu.Mods /// Returns the final position of the hit object /// /// Final position of the hit object - private void getObjectInfo( - float rateOfChangeMultiplier, - HitObjectInfo prevObjectInfo, - float distanceToPrev, - ref HitObjectInfo currentObjectInfo) + private void getObjectInfo(float rateOfChangeMultiplier, HitObjectInfo prevObjectInfo, float distanceToPrev, ref HitObjectInfo currentObjectInfo) { // The max. angle (relative to the angle of the vector pointing from the 2nd last to the last hit object) // is proportional to the distance between the last and the current hit object From 3fa6a0413b8a3a3223432301e5f35057177e81dc Mon Sep 17 00:00:00 2001 From: Pasi4K5 Date: Fri, 14 May 2021 23:04:09 +0200 Subject: [PATCH 18/33] Add slider position randomisation --- osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs | 90 ++++++++++++++++------ 1 file changed, 68 insertions(+), 22 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs index dc194aa464..90036e6839 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs @@ -53,9 +53,10 @@ namespace osu.Game.Rulesets.Osu.Mods var prevObjectInfo = new HitObjectInfo { - AngleRad = 0, - PosUnchanged = hitObjects[0].Position, - PosChanged = hitObjects[0].Position + StartPosUnchanged = hitObjects[0].Position, + EndPosUnchanged = hitObjects[0].EndPosition, + StartPosChanged = hitObjects[0].Position, + EndPosChanged = hitObjects[0].EndPosition }; float rateOfChangeMultiplier = 0; @@ -66,31 +67,51 @@ namespace osu.Game.Rulesets.Osu.Mods var currentObjectInfo = new HitObjectInfo { - AngleRad = 0, - PosUnchanged = h.EndPosition, - PosChanged = Vector2.Zero + StartPosUnchanged = h.Position, + EndPosUnchanged = h.EndPosition, + StartPosChanged = Vector2.Zero, + EndPosChanged = Vector2.Zero }; // rateOfChangeMultiplier only changes every i iterations to prevent shaky-line-shaped streams if (i % 3 == 0) rateOfChangeMultiplier = (float)rng.NextDouble() * 2 - 1; - if (h is HitCircle circle) + var distanceToPrev = Vector2.Distance(prevObjectInfo.EndPosUnchanged, currentObjectInfo.StartPosUnchanged); + + switch (h) { - var distanceToPrev = Vector2.Distance(currentObjectInfo.PosUnchanged, prevObjectInfo.PosUnchanged); + case HitCircle circle: + getObjectInfo( + rateOfChangeMultiplier, + prevObjectInfo, + distanceToPrev, + ref currentObjectInfo + ); - getObjectInfo( - rateOfChangeMultiplier, - prevObjectInfo, - distanceToPrev, - ref currentObjectInfo - ); + circle.Position = currentObjectInfo.StartPosChanged; + currentObjectInfo.EndPosChanged = currentObjectInfo.StartPosChanged; + break; - circle.Position = currentObjectInfo.PosChanged; + case Slider slider: + currentObjectInfo.EndPosUnchanged = slider.EndPosition; + + currentObjectInfo.EndPosUnchanged = slider.TailCircle.Position; + + getObjectInfo( + rateOfChangeMultiplier, + prevObjectInfo, + distanceToPrev, + ref currentObjectInfo + ); + + slider.Position = currentObjectInfo.StartPosChanged; + currentObjectInfo.EndPosChanged = slider.TailCircle.Position; + + moveSliderIntoPlayfield(ref slider, ref currentObjectInfo); + break; } - // TODO: Implement slider position randomisation - prevObjectInfo = currentObjectInfo; } } @@ -116,10 +137,10 @@ namespace osu.Game.Rulesets.Osu.Mods distanceToPrev * (float)Math.Sin(currentObjectInfo.AngleRad) ); - posRelativeToPrev = getRotatedVector(prevObjectInfo.PosChanged, posRelativeToPrev); + posRelativeToPrev = getRotatedVector(prevObjectInfo.EndPosChanged, posRelativeToPrev); currentObjectInfo.AngleRad = (float)Math.Atan2(posRelativeToPrev.Y, posRelativeToPrev.X); - var position = Vector2.Add(prevObjectInfo.PosChanged, posRelativeToPrev); + var position = Vector2.Add(prevObjectInfo.EndPosChanged, posRelativeToPrev); // Move hit objects back into the playfield if they are outside of it, // which would sometimes happen during big jumps otherwise. @@ -133,7 +154,30 @@ namespace osu.Game.Rulesets.Osu.Mods else if (position.Y > OsuPlayfield.BASE_SIZE.Y) position.Y = OsuPlayfield.BASE_SIZE.Y; - currentObjectInfo.PosChanged = position; + currentObjectInfo.StartPosChanged = position; + } + + private void moveSliderIntoPlayfield(ref Slider slider, ref HitObjectInfo currentObjectInfo) + { + foreach (var controlPoint in slider.Path.ControlPoints) + { + // Position of controlPoint relative to slider.Position + var pos = controlPoint.Position.Value; + + var playfieldSize = OsuPlayfield.BASE_SIZE; + + if (pos.X + slider.Position.X < 0) + slider.Position = new Vector2(-pos.X, slider.Position.Y); + else if (pos.X + slider.Position.X > playfieldSize.X) + slider.Position = new Vector2(playfieldSize.X - pos.X, slider.Position.Y); + + if (pos.Y + slider.Position.Y < 0) + slider.Position = new Vector2(slider.Position.X, -pos.Y); + else if (pos.Y + slider.Position.Y > playfieldSize.Y) + slider.Position = new Vector2(slider.Position.X, playfieldSize.Y - pos.Y); + } + + currentObjectInfo.EndPosChanged = slider.TailCircle.Position; } /// @@ -217,8 +261,10 @@ namespace osu.Game.Rulesets.Osu.Mods private struct HitObjectInfo { internal float AngleRad { get; set; } - internal Vector2 PosUnchanged { get; set; } - internal Vector2 PosChanged { get; set; } + internal Vector2 StartPosUnchanged { get; set; } + internal Vector2 EndPosUnchanged { get; set; } + internal Vector2 StartPosChanged { get; set; } + internal Vector2 EndPosChanged { get; set; } } } From 878182fbdf480348d1114f0bc1e9f47c6141552c Mon Sep 17 00:00:00 2001 From: Pasi4K5 Date: Sat, 15 May 2021 02:07:24 +0200 Subject: [PATCH 19/33] Fix slider ticks not being shifted along with their parent sliders --- osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs index 90036e6839..d06e807500 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Linq; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -91,6 +92,7 @@ namespace osu.Game.Rulesets.Osu.Mods circle.Position = currentObjectInfo.StartPosChanged; currentObjectInfo.EndPosChanged = currentObjectInfo.StartPosChanged; + break; case Slider slider: @@ -109,6 +111,12 @@ namespace osu.Game.Rulesets.Osu.Mods currentObjectInfo.EndPosChanged = slider.TailCircle.Position; moveSliderIntoPlayfield(ref slider, ref currentObjectInfo); + + var sliderShift = Vector2.Subtract(slider.Position, currentObjectInfo.StartPosUnchanged); + + foreach (var tick in slider.NestedHitObjects.OfType()) + tick.Position = Vector2.Add(tick.Position, sliderShift); + break; } From 8dd3f11d28db64bb85256c4901f5ff8c26e47108 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 24 May 2021 14:19:10 +0900 Subject: [PATCH 20/33] Tidy up struct and previous object handling --- osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs | 188 +++++++++++---------- 1 file changed, 97 insertions(+), 91 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs index d06e807500..5214020a84 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs @@ -52,35 +52,32 @@ namespace osu.Game.Rulesets.Osu.Mods var rng = new Random((int)Seed.Value); - var prevObjectInfo = new HitObjectInfo + var prevObjectInfo = new RandomObjectInfo { - StartPosUnchanged = hitObjects[0].Position, - EndPosUnchanged = hitObjects[0].EndPosition, - StartPosChanged = hitObjects[0].Position, - EndPosChanged = hitObjects[0].EndPosition + PositionOriginal = hitObjects[0].Position, + EndPositionOriginal = hitObjects[0].EndPosition, + PositionRandomised = hitObjects[0].Position, + EndPositionRandomised = hitObjects[0].EndPosition }; float rateOfChangeMultiplier = 0; for (int i = 0; i < hitObjects.Count; i++) { - var h = hitObjects[i]; + var hitObject = hitObjects[i]; - var currentObjectInfo = new HitObjectInfo - { - StartPosUnchanged = h.Position, - EndPosUnchanged = h.EndPosition, - StartPosChanged = Vector2.Zero, - EndPosChanged = Vector2.Zero - }; + var currentObjectInfo = new RandomObjectInfo(hitObject); + + if (i == 0) + prevObjectInfo = currentObjectInfo; // rateOfChangeMultiplier only changes every i iterations to prevent shaky-line-shaped streams if (i % 3 == 0) rateOfChangeMultiplier = (float)rng.NextDouble() * 2 - 1; - var distanceToPrev = Vector2.Distance(prevObjectInfo.EndPosUnchanged, currentObjectInfo.StartPosUnchanged); + var distanceToPrev = Vector2.Distance(prevObjectInfo.EndPositionOriginal, currentObjectInfo.PositionOriginal); - switch (h) + switch (hitObject) { case HitCircle circle: getObjectInfo( @@ -90,15 +87,15 @@ namespace osu.Game.Rulesets.Osu.Mods ref currentObjectInfo ); - circle.Position = currentObjectInfo.StartPosChanged; - currentObjectInfo.EndPosChanged = currentObjectInfo.StartPosChanged; + circle.Position = currentObjectInfo.PositionRandomised; + currentObjectInfo.EndPositionRandomised = currentObjectInfo.PositionRandomised; break; case Slider slider: - currentObjectInfo.EndPosUnchanged = slider.EndPosition; + currentObjectInfo.EndPositionOriginal = slider.EndPosition; - currentObjectInfo.EndPosUnchanged = slider.TailCircle.Position; + currentObjectInfo.EndPositionOriginal = slider.TailCircle.Position; getObjectInfo( rateOfChangeMultiplier, @@ -107,12 +104,12 @@ namespace osu.Game.Rulesets.Osu.Mods ref currentObjectInfo ); - slider.Position = currentObjectInfo.StartPosChanged; - currentObjectInfo.EndPosChanged = slider.TailCircle.Position; + slider.Position = currentObjectInfo.PositionRandomised; + currentObjectInfo.EndPositionRandomised = slider.TailCircle.Position; moveSliderIntoPlayfield(ref slider, ref currentObjectInfo); - var sliderShift = Vector2.Subtract(slider.Position, currentObjectInfo.StartPosUnchanged); + var sliderShift = Vector2.Subtract(slider.Position, currentObjectInfo.PositionOriginal); foreach (var tick in slider.NestedHitObjects.OfType()) tick.Position = Vector2.Add(tick.Position, sliderShift); @@ -128,7 +125,7 @@ namespace osu.Game.Rulesets.Osu.Mods /// Returns the final position of the hit object /// /// Final position of the hit object - private void getObjectInfo(float rateOfChangeMultiplier, HitObjectInfo prevObjectInfo, float distanceToPrev, ref HitObjectInfo currentObjectInfo) + private void getObjectInfo(float rateOfChangeMultiplier, RandomObjectInfo prevObjectInfo, float distanceToPrev, ref RandomObjectInfo currentObjectInfo) { // The max. angle (relative to the angle of the vector pointing from the 2nd last to the last hit object) // is proportional to the distance between the last and the current hit object @@ -145,10 +142,10 @@ namespace osu.Game.Rulesets.Osu.Mods distanceToPrev * (float)Math.Sin(currentObjectInfo.AngleRad) ); - posRelativeToPrev = getRotatedVector(prevObjectInfo.EndPosChanged, posRelativeToPrev); + posRelativeToPrev = getRotatedVector(prevObjectInfo.EndPositionRandomised, posRelativeToPrev); currentObjectInfo.AngleRad = (float)Math.Atan2(posRelativeToPrev.Y, posRelativeToPrev.X); - var position = Vector2.Add(prevObjectInfo.EndPosChanged, posRelativeToPrev); + var position = Vector2.Add(prevObjectInfo.EndPositionRandomised, posRelativeToPrev); // Move hit objects back into the playfield if they are outside of it, // which would sometimes happen during big jumps otherwise. @@ -162,10 +159,10 @@ namespace osu.Game.Rulesets.Osu.Mods else if (position.Y > OsuPlayfield.BASE_SIZE.Y) position.Y = OsuPlayfield.BASE_SIZE.Y; - currentObjectInfo.StartPosChanged = position; + currentObjectInfo.PositionRandomised = position; } - private void moveSliderIntoPlayfield(ref Slider slider, ref HitObjectInfo currentObjectInfo) + private void moveSliderIntoPlayfield(ref Slider slider, ref RandomObjectInfo currentObjectInfo) { foreach (var controlPoint in slider.Path.ControlPoints) { @@ -185,7 +182,7 @@ namespace osu.Game.Rulesets.Osu.Mods slider.Position = new Vector2(slider.Position.X, playfieldSize.Y - pos.Y); } - currentObjectInfo.EndPosChanged = slider.TailCircle.Position; + currentObjectInfo.EndPositionRandomised = slider.TailCircle.Position; } /// @@ -266,89 +263,98 @@ namespace osu.Game.Rulesets.Osu.Mods ); } - private struct HitObjectInfo + private struct RandomObjectInfo { internal float AngleRad { get; set; } - internal Vector2 StartPosUnchanged { get; set; } - internal Vector2 EndPosUnchanged { get; set; } - internal Vector2 StartPosChanged { get; set; } - internal Vector2 EndPosChanged { get; set; } - } - } - public class OsuModRandomSettingsControl : SettingsItem - { - protected override Drawable CreateControl() => new SeedControl - { - RelativeSizeAxes = Axes.X, - Margin = new MarginPadding { Top = 5 } - }; + internal Vector2 PositionOriginal { get; set; } + internal Vector2 PositionRandomised { get; set; } - private sealed class SeedControl : CompositeDrawable, IHasCurrentValue - { - private readonly BindableWithCurrent current = new BindableWithCurrent(); + internal Vector2 EndPositionOriginal { get; set; } + internal Vector2 EndPositionRandomised { get; set; } - public Bindable Current + public RandomObjectInfo(OsuHitObject hitObject) { - get => current; - set - { - current.Current = value; - seedNumberBox.Text = value.Value.ToString(); - } + PositionRandomised = PositionOriginal = hitObject.Position; + EndPositionRandomised = EndPositionOriginal = hitObject.EndPosition; + AngleRad = 0; } + } - private readonly OsuNumberBox seedNumberBox; - - public SeedControl() + public class OsuModRandomSettingsControl : SettingsItem + { + protected override Drawable CreateControl() => new SeedControl { - AutoSizeAxes = Axes.Y; + RelativeSizeAxes = Axes.X, + Margin = new MarginPadding { Top = 5 } + }; - InternalChildren = new[] + private sealed class SeedControl : CompositeDrawable, IHasCurrentValue + { + private readonly BindableWithCurrent current = new BindableWithCurrent(); + + public Bindable Current { - new GridContainer + get => current; + set { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - ColumnDimensions = new[] + current.Current = value; + seedNumberBox.Text = value.Value.ToString(); + } + } + + private readonly OsuNumberBox seedNumberBox; + + public SeedControl() + { + AutoSizeAxes = Axes.Y; + + InternalChildren = new[] + { + new GridContainer { - new Dimension(), - new Dimension(GridSizeMode.Absolute, 2), - new Dimension(GridSizeMode.Relative, 0.25f) - }, - RowDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize) - }, - Content = new[] - { - new Drawable[] + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + ColumnDimensions = new[] { - seedNumberBox = new OsuNumberBox + new Dimension(), + new Dimension(GridSizeMode.Absolute, 2), + new Dimension(GridSizeMode.Relative, 0.25f) + }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize) + }, + Content = new[] + { + new Drawable[] { - RelativeSizeAxes = Axes.X, - CommitOnFocusLost = true + seedNumberBox = new OsuNumberBox + { + RelativeSizeAxes = Axes.X, + CommitOnFocusLost = true + } } } } - } - }; + }; - seedNumberBox.Current.BindValueChanged(e => + seedNumberBox.Current.BindValueChanged(e => + { + int? value = null; + + if (int.TryParse(e.NewValue, out var intVal)) + value = intVal; + + current.Value = value; + }); + } + + protected override void Update() { - int? value = null; - - if (int.TryParse(e.NewValue, out var intVal)) - value = intVal; - - current.Value = value; - }); - } - - protected override void Update() - { - if (current.Value == null) - seedNumberBox.Text = current.Current.Value.ToString(); + if (current.Value == null) + seedNumberBox.Text = current.Current.Value.ToString(); + } } } } From 88d7bc195dea687d508e6667b8dfd91c1162d1e0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 24 May 2021 14:24:56 +0900 Subject: [PATCH 21/33] Split out and clean up playfield sizing references --- osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs | 27 +++++++++++----------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs index 5214020a84..bbae746f67 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs @@ -29,10 +29,16 @@ namespace osu.Game.Rulesets.Osu.Mods public override string Description => "It never gets boring!"; public override bool Ranked => false; - // The distances from the hit objects to the borders of the playfield they start to "turn around" and curve towards the middle. + // The relative distance to the edge of the playfield before objects' positions should start to "turn around" and curve towards the middle. // The closer the hit objects draw to the border, the sharper the turn - private const byte border_distance_x = 192; - private const byte border_distance_y = 144; + private const float playfield_edge_ratio = 0.375f; + + private static readonly float border_distance_x = OsuPlayfield.BASE_SIZE.X * playfield_edge_ratio; + private static readonly float border_distance_y = OsuPlayfield.BASE_SIZE.Y * playfield_edge_ratio; + + private static readonly Vector2 playfield_middle = Vector2.Divide(OsuPlayfield.BASE_SIZE, 2); + + private static readonly float playfield_diagonal = OsuPlayfield.BASE_SIZE.LengthFast; [SettingSource("Seed", "Use a custom seed instead of a random one", SettingControlType = typeof(OsuModRandomSettingsControl))] public Bindable Seed { get; } = new Bindable @@ -130,8 +136,7 @@ namespace osu.Game.Rulesets.Osu.Mods // The max. angle (relative to the angle of the vector pointing from the 2nd last to the last hit object) // is proportional to the distance between the last and the current hit object // to allow jumps and prevent too sharp turns during streams. - var maxDistance = OsuPlayfield.BASE_SIZE.LengthFast; - var randomAngleRad = rateOfChangeMultiplier * 2 * Math.PI * distanceToPrev / maxDistance; + var randomAngleRad = rateOfChangeMultiplier * 2 * Math.PI * distanceToPrev / playfield_diagonal; currentObjectInfo.AngleRad = (float)randomAngleRad + prevObjectInfo.AngleRad; if (currentObjectInfo.AngleRad < 0) @@ -145,6 +150,7 @@ namespace osu.Game.Rulesets.Osu.Mods posRelativeToPrev = getRotatedVector(prevObjectInfo.EndPositionRandomised, posRelativeToPrev); currentObjectInfo.AngleRad = (float)Math.Atan2(posRelativeToPrev.Y, posRelativeToPrev.X); + var position = Vector2.Add(prevObjectInfo.EndPositionRandomised, posRelativeToPrev); // Move hit objects back into the playfield if they are outside of it, @@ -192,9 +198,8 @@ namespace osu.Game.Rulesets.Osu.Mods private Vector2 getRotatedVector(Vector2 prevPosChanged, Vector2 posRelativeToPrev) { var relativeRotationDistance = 0f; - var playfieldMiddle = Vector2.Divide(OsuPlayfield.BASE_SIZE, 2); - if (prevPosChanged.X < playfieldMiddle.X) + if (prevPosChanged.X < playfield_middle.X) { relativeRotationDistance = Math.Max( (border_distance_x - prevPosChanged.X) / border_distance_x, @@ -209,7 +214,7 @@ namespace osu.Game.Rulesets.Osu.Mods ); } - if (prevPosChanged.Y < playfieldMiddle.Y) + if (prevPosChanged.Y < playfield_middle.Y) { relativeRotationDistance = Math.Max( (border_distance_y - prevPosChanged.Y) / border_distance_y, @@ -224,11 +229,7 @@ namespace osu.Game.Rulesets.Osu.Mods ); } - return rotateVectorTowardsVector( - posRelativeToPrev, - Vector2.Subtract(playfieldMiddle, prevPosChanged), - relativeRotationDistance / 2 - ); + return rotateVectorTowardsVector(posRelativeToPrev, playfield_middle - prevPosChanged, relativeRotationDistance / 2); } /// From a92ded8a2fc8e9e947d1258777000d5252aa81cd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 24 May 2021 14:28:07 +0900 Subject: [PATCH 22/33] Apply renaming and general code clean-up --- osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs index bbae746f67..61559c06b9 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs @@ -86,7 +86,7 @@ namespace osu.Game.Rulesets.Osu.Mods switch (hitObject) { case HitCircle circle: - getObjectInfo( + applyRandomisation( rateOfChangeMultiplier, prevObjectInfo, distanceToPrev, @@ -103,7 +103,7 @@ namespace osu.Game.Rulesets.Osu.Mods currentObjectInfo.EndPositionOriginal = slider.TailCircle.Position; - getObjectInfo( + applyRandomisation( rateOfChangeMultiplier, prevObjectInfo, distanceToPrev, @@ -131,7 +131,7 @@ namespace osu.Game.Rulesets.Osu.Mods /// Returns the final position of the hit object /// /// Final position of the hit object - private void getObjectInfo(float rateOfChangeMultiplier, RandomObjectInfo prevObjectInfo, float distanceToPrev, ref RandomObjectInfo currentObjectInfo) + private void applyRandomisation(float rateOfChangeMultiplier, RandomObjectInfo prevObjectInfo, float distanceToPrev, ref RandomObjectInfo currentObjectInfo) { // The max. angle (relative to the angle of the vector pointing from the 2nd last to the last hit object) // is proportional to the distance between the last and the current hit object @@ -155,15 +155,8 @@ namespace osu.Game.Rulesets.Osu.Mods // Move hit objects back into the playfield if they are outside of it, // which would sometimes happen during big jumps otherwise. - if (position.X < 0) - position.X = 0; - else if (position.X > OsuPlayfield.BASE_SIZE.X) - position.X = OsuPlayfield.BASE_SIZE.X; - - if (position.Y < 0) - position.Y = 0; - else if (position.Y > OsuPlayfield.BASE_SIZE.Y) - position.Y = OsuPlayfield.BASE_SIZE.Y; + position.X = MathHelper.Clamp(position.X, 0, OsuPlayfield.BASE_SIZE.X); + position.Y = MathHelper.Clamp(position.Y, 0, OsuPlayfield.BASE_SIZE.Y); currentObjectInfo.PositionRandomised = position; } From 53b5341bb98d7427b53edccbaed14bdd5250fbe5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 24 May 2021 14:33:07 +0900 Subject: [PATCH 23/33] Simplify application logic --- osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs | 52 +++++++--------------- 1 file changed, 17 insertions(+), 35 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs index 61559c06b9..cc7732372f 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs @@ -83,36 +83,24 @@ namespace osu.Game.Rulesets.Osu.Mods var distanceToPrev = Vector2.Distance(prevObjectInfo.EndPositionOriginal, currentObjectInfo.PositionOriginal); + if (hitObject is Spinner) + continue; + + applyRandomisation( + rateOfChangeMultiplier, + prevObjectInfo, + distanceToPrev, + ref currentObjectInfo + ); + + hitObject.Position = currentObjectInfo.PositionRandomised; + + // update end position as it may have changed as a result of the position update. + currentObjectInfo.EndPositionRandomised = currentObjectInfo.PositionRandomised; + switch (hitObject) { - case HitCircle circle: - applyRandomisation( - rateOfChangeMultiplier, - prevObjectInfo, - distanceToPrev, - ref currentObjectInfo - ); - - circle.Position = currentObjectInfo.PositionRandomised; - currentObjectInfo.EndPositionRandomised = currentObjectInfo.PositionRandomised; - - break; - case Slider slider: - currentObjectInfo.EndPositionOriginal = slider.EndPosition; - - currentObjectInfo.EndPositionOriginal = slider.TailCircle.Position; - - applyRandomisation( - rateOfChangeMultiplier, - prevObjectInfo, - distanceToPrev, - ref currentObjectInfo - ); - - slider.Position = currentObjectInfo.PositionRandomised; - currentObjectInfo.EndPositionRandomised = slider.TailCircle.Position; - moveSliderIntoPlayfield(ref slider, ref currentObjectInfo); var sliderShift = Vector2.Subtract(slider.Position, currentObjectInfo.PositionOriginal); @@ -239,15 +227,9 @@ namespace osu.Game.Rulesets.Osu.Mods var diff = destAngleRad - initialAngleRad; - while (diff < -Math.PI) - { - diff += 2 * Math.PI; - } + while (diff < -Math.PI) diff += 2 * Math.PI; - while (diff > Math.PI) - { - diff -= 2 * Math.PI; - } + while (diff > Math.PI) diff -= 2 * Math.PI; var finalAngleRad = initialAngleRad + relativeDistance * diff; From 098d8c213178827f6b759ec9841cbb6a8cb8549b Mon Sep 17 00:00:00 2001 From: Pasi4K5 Date: Mon, 24 May 2021 15:13:31 +0200 Subject: [PATCH 24/33] Add complete randomisation for first hit object and hit objects after spinners --- osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs | 33 ++++++++++++++-------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs index cc7732372f..ad2f4585f6 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs @@ -58,35 +58,32 @@ namespace osu.Game.Rulesets.Osu.Mods var rng = new Random((int)Seed.Value); - var prevObjectInfo = new RandomObjectInfo - { - PositionOriginal = hitObjects[0].Position, - EndPositionOriginal = hitObjects[0].EndPosition, - PositionRandomised = hitObjects[0].Position, - EndPositionRandomised = hitObjects[0].EndPosition - }; + RandomObjectInfo? prevObjectInfo = null; float rateOfChangeMultiplier = 0; for (int i = 0; i < hitObjects.Count; i++) { + var distanceToPrev = 0f; + var hitObject = hitObjects[i]; var currentObjectInfo = new RandomObjectInfo(hitObject); - if (i == 0) - prevObjectInfo = currentObjectInfo; + if (i > 0 && hitObjects[i - 1] is Spinner) + prevObjectInfo = null; + else if (prevObjectInfo != null) + distanceToPrev = Vector2.Distance(((RandomObjectInfo)prevObjectInfo).EndPositionOriginal, currentObjectInfo.PositionOriginal); // rateOfChangeMultiplier only changes every i iterations to prevent shaky-line-shaped streams if (i % 3 == 0) rateOfChangeMultiplier = (float)rng.NextDouble() * 2 - 1; - var distanceToPrev = Vector2.Distance(prevObjectInfo.EndPositionOriginal, currentObjectInfo.PositionOriginal); - if (hitObject is Spinner) continue; applyRandomisation( + rng, rateOfChangeMultiplier, prevObjectInfo, distanceToPrev, @@ -119,8 +116,20 @@ namespace osu.Game.Rulesets.Osu.Mods /// Returns the final position of the hit object /// /// Final position of the hit object - private void applyRandomisation(float rateOfChangeMultiplier, RandomObjectInfo prevObjectInfo, float distanceToPrev, ref RandomObjectInfo currentObjectInfo) + private void applyRandomisation(Random rng, float rateOfChangeMultiplier, RandomObjectInfo? prevObjectInfoNullable, float distanceToPrev, ref RandomObjectInfo currentObjectInfo) { + if (prevObjectInfoNullable == null) + { + var playfieldSize = OsuPlayfield.BASE_SIZE; + + currentObjectInfo.AngleRad = (float)(rng.NextDouble() * 2 * Math.PI - Math.PI); + currentObjectInfo.PositionRandomised = new Vector2((float)rng.NextDouble() * playfieldSize.X, (float)rng.NextDouble() * playfieldSize.Y); + + return; + } + + var prevObjectInfo = (RandomObjectInfo)prevObjectInfoNullable; + // The max. angle (relative to the angle of the vector pointing from the 2nd last to the last hit object) // is proportional to the distance between the last and the current hit object // to allow jumps and prevent too sharp turns during streams. From bdbd64c88d6a9cace3862fcdd45ccb6109d38cda Mon Sep 17 00:00:00 2001 From: Pasi4K5 Date: Tue, 25 May 2021 21:32:18 +0200 Subject: [PATCH 25/33] Fix sliders being partly outside of the playfield in some cases --- osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs | 68 ++++++++++++++++------ 1 file changed, 49 insertions(+), 19 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs index ad2f4585f6..98488fef8c 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs @@ -98,13 +98,9 @@ namespace osu.Game.Rulesets.Osu.Mods switch (hitObject) { case Slider slider: + shiftNestedObjects(slider, Vector2.Subtract(slider.Position, currentObjectInfo.PositionOriginal)); moveSliderIntoPlayfield(ref slider, ref currentObjectInfo); - var sliderShift = Vector2.Subtract(slider.Position, currentObjectInfo.PositionOriginal); - - foreach (var tick in slider.NestedHitObjects.OfType()) - tick.Position = Vector2.Add(tick.Position, sliderShift); - break; } @@ -158,27 +154,61 @@ namespace osu.Game.Rulesets.Osu.Mods currentObjectInfo.PositionRandomised = position; } + /// + /// Moves the and all necessary nested s into the if they aren't already. + /// private void moveSliderIntoPlayfield(ref Slider slider, ref RandomObjectInfo currentObjectInfo) { - foreach (var controlPoint in slider.Path.ControlPoints) + var oldPos = new Vector2(slider.Position.X, slider.Position.Y); + + // Min. distances from the slider's position to the playfield border + var minMargin = new MarginPadding(0); + + foreach (var hitObject in slider.NestedHitObjects.Where(o => o is SliderTick || o is SliderEndCircle)) { - // Position of controlPoint relative to slider.Position - var pos = controlPoint.Position.Value; + if (!(hitObject is OsuHitObject osuHitObject)) + continue; - var playfieldSize = OsuPlayfield.BASE_SIZE; + var relativePos = Vector2.Subtract(osuHitObject.Position, slider.Position); - if (pos.X + slider.Position.X < 0) - slider.Position = new Vector2(-pos.X, slider.Position.Y); - else if (pos.X + slider.Position.X > playfieldSize.X) - slider.Position = new Vector2(playfieldSize.X - pos.X, slider.Position.Y); - - if (pos.Y + slider.Position.Y < 0) - slider.Position = new Vector2(slider.Position.X, -pos.Y); - else if (pos.Y + slider.Position.Y > playfieldSize.Y) - slider.Position = new Vector2(slider.Position.X, playfieldSize.Y - pos.Y); + minMargin.Left = Math.Max(minMargin.Left, -relativePos.X); + minMargin.Right = Math.Max(minMargin.Right, relativePos.X); + minMargin.Top = Math.Max(minMargin.Top, -relativePos.Y); + minMargin.Bottom = Math.Max(minMargin.Bottom, relativePos.Y); } - currentObjectInfo.EndPositionRandomised = slider.TailCircle.Position; + if (slider.Position.X < minMargin.Left) + slider.Position = new Vector2(minMargin.Left, slider.Position.Y); + else if (slider.Position.X + minMargin.Right > OsuPlayfield.BASE_SIZE.X) + slider.Position = new Vector2(OsuPlayfield.BASE_SIZE.X - minMargin.Right, slider.Position.Y); + + if (slider.Position.Y < minMargin.Top) + slider.Position = new Vector2(slider.Position.X, minMargin.Top); + else if (slider.Position.Y + minMargin.Bottom > OsuPlayfield.BASE_SIZE.Y) + slider.Position = new Vector2(slider.Position.X, OsuPlayfield.BASE_SIZE.Y - minMargin.Bottom); + + currentObjectInfo.PositionRandomised = slider.Position; + currentObjectInfo.EndPositionRandomised = slider.EndPosition; + + var shift = Vector2.Subtract(slider.Position, oldPos); + + shiftNestedObjects(slider, shift); + } + + /// + /// Shifts all nested s and s by the specified shift. + /// + /// whose nested s and s should be shifted + /// The the 's nested s and s should be shifted by + private void shiftNestedObjects(Slider slider, Vector2 shift) + { + foreach (var hitObject in slider.NestedHitObjects.Where(o => o is SliderTick || o is SliderRepeat)) + { + if (!(hitObject is OsuHitObject osuHitObject)) + continue; + + osuHitObject.Position = Vector2.Add(osuHitObject.Position, shift); + } } /// From c85d5513bedb30f407594555b6bdaba3d10a2864 Mon Sep 17 00:00:00 2001 From: Pasi4K5 Date: Tue, 25 May 2021 21:42:26 +0200 Subject: [PATCH 26/33] Remove redundant parameter and unused setters --- osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs index 98488fef8c..0f6b6d1afa 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs @@ -162,7 +162,7 @@ namespace osu.Game.Rulesets.Osu.Mods var oldPos = new Vector2(slider.Position.X, slider.Position.Y); // Min. distances from the slider's position to the playfield border - var minMargin = new MarginPadding(0); + var minMargin = new MarginPadding(); foreach (var hitObject in slider.NestedHitObjects.Where(o => o is SliderTick || o is SliderEndCircle)) { @@ -282,10 +282,10 @@ namespace osu.Game.Rulesets.Osu.Mods { internal float AngleRad { get; set; } - internal Vector2 PositionOriginal { get; set; } + internal Vector2 PositionOriginal { get; } internal Vector2 PositionRandomised { get; set; } - internal Vector2 EndPositionOriginal { get; set; } + internal Vector2 EndPositionOriginal { get; } internal Vector2 EndPositionRandomised { get; set; } public RandomObjectInfo(OsuHitObject hitObject) From c5ff05209602584c388c8263b65395a0b8f1d22f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 26 May 2021 16:31:25 +0900 Subject: [PATCH 27/33] Change `internal` to `public` --- osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs index 0f6b6d1afa..b81e9fe3c5 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs @@ -280,13 +280,13 @@ namespace osu.Game.Rulesets.Osu.Mods private struct RandomObjectInfo { - internal float AngleRad { get; set; } + public float AngleRad { get; set; } - internal Vector2 PositionOriginal { get; } - internal Vector2 PositionRandomised { get; set; } + public Vector2 PositionOriginal { get; } + public Vector2 PositionRandomised { get; set; } - internal Vector2 EndPositionOriginal { get; } - internal Vector2 EndPositionRandomised { get; set; } + public Vector2 EndPositionOriginal { get; } + public Vector2 EndPositionRandomised { get; set; } public RandomObjectInfo(OsuHitObject hitObject) { From 6181b1ac92302f5c8bae6f93ff2091da5cf81bde Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 26 May 2021 16:36:14 +0900 Subject: [PATCH 28/33] Simplify previous object handling by using a class instead of struct --- osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs | 31 +++++++++++----------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs index b81e9fe3c5..d1d1d24f1b 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs @@ -58,7 +58,7 @@ namespace osu.Game.Rulesets.Osu.Mods var rng = new Random((int)Seed.Value); - RandomObjectInfo? prevObjectInfo = null; + RandomObjectInfo prevObjectInfo = null; float rateOfChangeMultiplier = 0; @@ -70,24 +70,25 @@ namespace osu.Game.Rulesets.Osu.Mods var currentObjectInfo = new RandomObjectInfo(hitObject); - if (i > 0 && hitObjects[i - 1] is Spinner) - prevObjectInfo = null; - else if (prevObjectInfo != null) - distanceToPrev = Vector2.Distance(((RandomObjectInfo)prevObjectInfo).EndPositionOriginal, currentObjectInfo.PositionOriginal); + if (prevObjectInfo != null) + distanceToPrev = Vector2.Distance(prevObjectInfo.EndPositionOriginal, currentObjectInfo.PositionOriginal); // rateOfChangeMultiplier only changes every i iterations to prevent shaky-line-shaped streams if (i % 3 == 0) rateOfChangeMultiplier = (float)rng.NextDouble() * 2 - 1; if (hitObject is Spinner) + { + prevObjectInfo = null; continue; + } applyRandomisation( rng, rateOfChangeMultiplier, prevObjectInfo, distanceToPrev, - ref currentObjectInfo + currentObjectInfo ); hitObject.Position = currentObjectInfo.PositionRandomised; @@ -99,7 +100,7 @@ namespace osu.Game.Rulesets.Osu.Mods { case Slider slider: shiftNestedObjects(slider, Vector2.Subtract(slider.Position, currentObjectInfo.PositionOriginal)); - moveSliderIntoPlayfield(ref slider, ref currentObjectInfo); + moveSliderIntoPlayfield(slider, currentObjectInfo); break; } @@ -112,9 +113,9 @@ namespace osu.Game.Rulesets.Osu.Mods /// Returns the final position of the hit object /// /// Final position of the hit object - private void applyRandomisation(Random rng, float rateOfChangeMultiplier, RandomObjectInfo? prevObjectInfoNullable, float distanceToPrev, ref RandomObjectInfo currentObjectInfo) + private void applyRandomisation(float rateOfChangeMultiplier, RandomObjectInfo prevObject, float distanceToPrev, RandomObjectInfo currentObjectInfo) { - if (prevObjectInfoNullable == null) + if (prevObject == null) { var playfieldSize = OsuPlayfield.BASE_SIZE; @@ -124,14 +125,12 @@ namespace osu.Game.Rulesets.Osu.Mods return; } - var prevObjectInfo = (RandomObjectInfo)prevObjectInfoNullable; - // The max. angle (relative to the angle of the vector pointing from the 2nd last to the last hit object) // is proportional to the distance between the last and the current hit object // to allow jumps and prevent too sharp turns during streams. var randomAngleRad = rateOfChangeMultiplier * 2 * Math.PI * distanceToPrev / playfield_diagonal; - currentObjectInfo.AngleRad = (float)randomAngleRad + prevObjectInfo.AngleRad; + currentObjectInfo.AngleRad = (float)randomAngleRad + prevObject.AngleRad; if (currentObjectInfo.AngleRad < 0) currentObjectInfo.AngleRad += 2 * (float)Math.PI; @@ -140,11 +139,11 @@ namespace osu.Game.Rulesets.Osu.Mods distanceToPrev * (float)Math.Sin(currentObjectInfo.AngleRad) ); - posRelativeToPrev = getRotatedVector(prevObjectInfo.EndPositionRandomised, posRelativeToPrev); + posRelativeToPrev = getRotatedVector(prevObject.EndPositionRandomised, posRelativeToPrev); currentObjectInfo.AngleRad = (float)Math.Atan2(posRelativeToPrev.Y, posRelativeToPrev.X); - var position = Vector2.Add(prevObjectInfo.EndPositionRandomised, posRelativeToPrev); + var position = Vector2.Add(prevObject.EndPositionRandomised, posRelativeToPrev); // Move hit objects back into the playfield if they are outside of it, // which would sometimes happen during big jumps otherwise. @@ -157,7 +156,7 @@ namespace osu.Game.Rulesets.Osu.Mods /// /// Moves the and all necessary nested s into the if they aren't already. /// - private void moveSliderIntoPlayfield(ref Slider slider, ref RandomObjectInfo currentObjectInfo) + private void moveSliderIntoPlayfield(Slider slider, RandomObjectInfo currentObjectInfo) { var oldPos = new Vector2(slider.Position.X, slider.Position.Y); @@ -278,7 +277,7 @@ namespace osu.Game.Rulesets.Osu.Mods ); } - private struct RandomObjectInfo + private class RandomObjectInfo { public float AngleRad { get; set; } From 6ca9b37c28279e05d599903047c2be4484a28ce6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 26 May 2021 16:37:30 +0900 Subject: [PATCH 29/33] Make random generator a field to avoid passing around internally --- osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs index d1d1d24f1b..c53c262ffb 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs @@ -40,6 +40,8 @@ namespace osu.Game.Rulesets.Osu.Mods private static readonly float playfield_diagonal = OsuPlayfield.BASE_SIZE.LengthFast; + private Random rng; + [SettingSource("Seed", "Use a custom seed instead of a random one", SettingControlType = typeof(OsuModRandomSettingsControl))] public Bindable Seed { get; } = new Bindable { @@ -56,7 +58,7 @@ namespace osu.Game.Rulesets.Osu.Mods Seed.Value ??= RNG.Next(); - var rng = new Random((int)Seed.Value); + rng = new Random((int)Seed.Value); RandomObjectInfo prevObjectInfo = null; @@ -83,9 +85,7 @@ namespace osu.Game.Rulesets.Osu.Mods continue; } - applyRandomisation( - rng, - rateOfChangeMultiplier, + applyRandomisation(rateOfChangeMultiplier, prevObjectInfo, distanceToPrev, currentObjectInfo From ad3e4287cd24da058cff136293b3b7913b68730c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 26 May 2021 16:44:05 +0900 Subject: [PATCH 30/33] Move `distanceToPrev` inside randomisation function --- osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs | 39 +++++++++------------- 1 file changed, 16 insertions(+), 23 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs index c53c262ffb..1c59569517 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs @@ -66,15 +66,10 @@ namespace osu.Game.Rulesets.Osu.Mods for (int i = 0; i < hitObjects.Count; i++) { - var distanceToPrev = 0f; - var hitObject = hitObjects[i]; var currentObjectInfo = new RandomObjectInfo(hitObject); - if (prevObjectInfo != null) - distanceToPrev = Vector2.Distance(prevObjectInfo.EndPositionOriginal, currentObjectInfo.PositionOriginal); - // rateOfChangeMultiplier only changes every i iterations to prevent shaky-line-shaped streams if (i % 3 == 0) rateOfChangeMultiplier = (float)rng.NextDouble() * 2 - 1; @@ -85,11 +80,7 @@ namespace osu.Game.Rulesets.Osu.Mods continue; } - applyRandomisation(rateOfChangeMultiplier, - prevObjectInfo, - distanceToPrev, - currentObjectInfo - ); + applyRandomisation(rateOfChangeMultiplier, prevObjectInfo, currentObjectInfo); hitObject.Position = currentObjectInfo.PositionRandomised; @@ -113,44 +104,46 @@ namespace osu.Game.Rulesets.Osu.Mods /// Returns the final position of the hit object /// /// Final position of the hit object - private void applyRandomisation(float rateOfChangeMultiplier, RandomObjectInfo prevObject, float distanceToPrev, RandomObjectInfo currentObjectInfo) + private void applyRandomisation(float rateOfChangeMultiplier, RandomObjectInfo previous, RandomObjectInfo current) { - if (prevObject == null) + if (previous == null) { var playfieldSize = OsuPlayfield.BASE_SIZE; - currentObjectInfo.AngleRad = (float)(rng.NextDouble() * 2 * Math.PI - Math.PI); - currentObjectInfo.PositionRandomised = new Vector2((float)rng.NextDouble() * playfieldSize.X, (float)rng.NextDouble() * playfieldSize.Y); + current.AngleRad = (float)(rng.NextDouble() * 2 * Math.PI - Math.PI); + current.PositionRandomised = new Vector2((float)rng.NextDouble() * playfieldSize.X, (float)rng.NextDouble() * playfieldSize.Y); return; } + float distanceToPrev = Vector2.Distance(previous.EndPositionOriginal, current.PositionOriginal); + // The max. angle (relative to the angle of the vector pointing from the 2nd last to the last hit object) // is proportional to the distance between the last and the current hit object // to allow jumps and prevent too sharp turns during streams. var randomAngleRad = rateOfChangeMultiplier * 2 * Math.PI * distanceToPrev / playfield_diagonal; - currentObjectInfo.AngleRad = (float)randomAngleRad + prevObject.AngleRad; - if (currentObjectInfo.AngleRad < 0) - currentObjectInfo.AngleRad += 2 * (float)Math.PI; + current.AngleRad = (float)randomAngleRad + previous.AngleRad; + if (current.AngleRad < 0) + current.AngleRad += 2 * (float)Math.PI; var posRelativeToPrev = new Vector2( - distanceToPrev * (float)Math.Cos(currentObjectInfo.AngleRad), - distanceToPrev * (float)Math.Sin(currentObjectInfo.AngleRad) + distanceToPrev * (float)Math.Cos(current.AngleRad), + distanceToPrev * (float)Math.Sin(current.AngleRad) ); - posRelativeToPrev = getRotatedVector(prevObject.EndPositionRandomised, posRelativeToPrev); + posRelativeToPrev = getRotatedVector(previous.EndPositionRandomised, posRelativeToPrev); - currentObjectInfo.AngleRad = (float)Math.Atan2(posRelativeToPrev.Y, posRelativeToPrev.X); + current.AngleRad = (float)Math.Atan2(posRelativeToPrev.Y, posRelativeToPrev.X); - var position = Vector2.Add(prevObject.EndPositionRandomised, posRelativeToPrev); + var position = Vector2.Add(previous.EndPositionRandomised, posRelativeToPrev); // Move hit objects back into the playfield if they are outside of it, // which would sometimes happen during big jumps otherwise. position.X = MathHelper.Clamp(position.X, 0, OsuPlayfield.BASE_SIZE.X); position.Y = MathHelper.Clamp(position.Y, 0, OsuPlayfield.BASE_SIZE.Y); - currentObjectInfo.PositionRandomised = position; + current.PositionRandomised = position; } /// From d6c4be207b05530b39fc520cb03a28f7c082680b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 26 May 2021 16:44:44 +0900 Subject: [PATCH 31/33] Simplify naming --- osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs index 1c59569517..a9aeba99fa 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs @@ -60,7 +60,7 @@ namespace osu.Game.Rulesets.Osu.Mods rng = new Random((int)Seed.Value); - RandomObjectInfo prevObjectInfo = null; + RandomObjectInfo previous = null; float rateOfChangeMultiplier = 0; @@ -68,7 +68,7 @@ namespace osu.Game.Rulesets.Osu.Mods { var hitObject = hitObjects[i]; - var currentObjectInfo = new RandomObjectInfo(hitObject); + var current = new RandomObjectInfo(hitObject); // rateOfChangeMultiplier only changes every i iterations to prevent shaky-line-shaped streams if (i % 3 == 0) @@ -76,27 +76,27 @@ namespace osu.Game.Rulesets.Osu.Mods if (hitObject is Spinner) { - prevObjectInfo = null; + previous = null; continue; } - applyRandomisation(rateOfChangeMultiplier, prevObjectInfo, currentObjectInfo); + applyRandomisation(rateOfChangeMultiplier, previous, current); - hitObject.Position = currentObjectInfo.PositionRandomised; + hitObject.Position = current.PositionRandomised; // update end position as it may have changed as a result of the position update. - currentObjectInfo.EndPositionRandomised = currentObjectInfo.PositionRandomised; + current.EndPositionRandomised = current.PositionRandomised; switch (hitObject) { case Slider slider: - shiftNestedObjects(slider, Vector2.Subtract(slider.Position, currentObjectInfo.PositionOriginal)); - moveSliderIntoPlayfield(slider, currentObjectInfo); + shiftNestedObjects(slider, Vector2.Subtract(slider.Position, current.PositionOriginal)); + moveSliderIntoPlayfield(slider, current); break; } - prevObjectInfo = currentObjectInfo; + previous = current; } } From a08a4aa9111448432fb708fdc17a8014f66a1c59 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 26 May 2021 16:48:16 +0900 Subject: [PATCH 32/33] Move second call to `shiftNestedObjects` to a more understandable location --- osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs index a9aeba99fa..58ace92905 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs @@ -91,8 +91,13 @@ namespace osu.Game.Rulesets.Osu.Mods { case Slider slider: shiftNestedObjects(slider, Vector2.Subtract(slider.Position, current.PositionOriginal)); + + var oldPos = new Vector2(slider.Position.X, slider.Position.Y); + moveSliderIntoPlayfield(slider, current); + shiftNestedObjects(slider, Vector2.Subtract(slider.Position, oldPos)); + break; } @@ -151,8 +156,6 @@ namespace osu.Game.Rulesets.Osu.Mods /// private void moveSliderIntoPlayfield(Slider slider, RandomObjectInfo currentObjectInfo) { - var oldPos = new Vector2(slider.Position.X, slider.Position.Y); - // Min. distances from the slider's position to the playfield border var minMargin = new MarginPadding(); @@ -181,10 +184,6 @@ namespace osu.Game.Rulesets.Osu.Mods currentObjectInfo.PositionRandomised = slider.Position; currentObjectInfo.EndPositionRandomised = slider.EndPosition; - - var shift = Vector2.Subtract(slider.Position, oldPos); - - shiftNestedObjects(slider, shift); } /// From d5de5ae6406f85fee9217c8c1649d93e0547e5dc Mon Sep 17 00:00:00 2001 From: Pasi4K5 Date: Wed, 26 May 2021 20:50:31 +0200 Subject: [PATCH 33/33] Add comments explaining the usage of `shiftNestedObjects()` --- osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs index 58ace92905..7b28675511 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs @@ -90,12 +90,15 @@ namespace osu.Game.Rulesets.Osu.Mods switch (hitObject) { case Slider slider: + // Shift nested objects the same distance as the slider got shifted in the randomisation process + // so that moveSliderIntoPlayfield() can determine their relative distances to slider.Position and thus minMargin shiftNestedObjects(slider, Vector2.Subtract(slider.Position, current.PositionOriginal)); var oldPos = new Vector2(slider.Position.X, slider.Position.Y); moveSliderIntoPlayfield(slider, current); + // Shift them again to move them to their final position after the slider got moved into the playfield shiftNestedObjects(slider, Vector2.Subtract(slider.Position, oldPos)); break;