From 323e4ac26b8172865bc6fbede9c39fb05ee5da40 Mon Sep 17 00:00:00 2001 From: Samuel Cattini-Schultz Date: Sun, 21 Feb 2021 18:24:27 +1100 Subject: [PATCH 01/61] Refactor catch Movement skill to not require explicit clockrate usage In catch, rate adjustment mods do not only affect the timings of hitobjects, but also the speed of the player's catcher. This catcher speed change has an impact on difficulty which is currently accounted for by using the clockrate directly in calculations. Semantically this is a bad idea because clockrate adjustments are supposed to be fully accounted for in DifficultyHitObjects, but passing clockrate here for the purpose of being used as catcher speed doesn't make much sense, especially since it is copied in every DifficultyHitObject despite being the same value. It makes more sense to account for this catch specific impact by handling rate adjustment mods in a catch specific way, or more specifically in a Movement skill specific way. --- .../Preprocessing/CatchDifficultyHitObject.cs | 3 --- .../Difficulty/Skills/Movement.cs | 15 +++++++++++++-- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Difficulty/Preprocessing/CatchDifficultyHitObject.cs b/osu.Game.Rulesets.Catch/Difficulty/Preprocessing/CatchDifficultyHitObject.cs index d936ef97ac..e19098c580 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/Preprocessing/CatchDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/Preprocessing/CatchDifficultyHitObject.cs @@ -24,8 +24,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Preprocessing /// public readonly double StrainTime; - public readonly double ClockRate; - public CatchDifficultyHitObject(HitObject hitObject, HitObject lastObject, double clockRate, float halfCatcherWidth) : base(hitObject, lastObject, clockRate) { @@ -37,7 +35,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Preprocessing // Every strain interval is hard capped at the equivalent of 375 BPM streaming speed as a safety measure StrainTime = Math.Max(40, DeltaTime); - ClockRate = clockRate; } } } diff --git a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs index 9ad719be1a..7d61be7bb1 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Linq; using osu.Game.Rulesets.Catch.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; @@ -26,10 +27,20 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills private float lastDistanceMoved; private double lastStrainTime; + /// + /// The speed multiplier applied to the player's catcher. + /// + private readonly double catcherSpeedMultiplier; + public Movement(Mod[] mods, float halfCatcherWidth) : base(mods) { HalfCatcherWidth = halfCatcherWidth; + + // In catch, rate adjustment mods do not only affect the timings of hitobjects, + // but also the speed of the player's catcher, which has an impact on difficulty + var rateAdjustMod = mods.FirstOrDefault(m => m is ModRateAdjust); + catcherSpeedMultiplier = (rateAdjustMod as ModRateAdjust)?.SpeedChange.Value ?? 1; } protected override double StrainValueOf(DifficultyHitObject current) @@ -46,7 +57,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills float distanceMoved = playerPosition - lastPlayerPosition.Value; - double weightedStrainTime = catchCurrent.StrainTime + 13 + (3 / catchCurrent.ClockRate); + double weightedStrainTime = catchCurrent.StrainTime + 13 + (3 / catcherSpeedMultiplier); double distanceAddition = (Math.Pow(Math.Abs(distanceMoved), 1.3) / 510); double sqrtStrain = Math.Sqrt(weightedStrainTime); @@ -79,7 +90,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills playerPosition = catchCurrent.NormalizedPosition; } - distanceAddition *= 1.0 + edgeDashBonus * ((20 - catchCurrent.LastObject.DistanceToHyperDash) / 20) * Math.Pow((Math.Min(catchCurrent.StrainTime * catchCurrent.ClockRate, 265) / 265), 1.5); // Edge Dashes are easier at lower ms values + distanceAddition *= 1.0 + edgeDashBonus * ((20 - catchCurrent.LastObject.DistanceToHyperDash) / 20) * Math.Pow((Math.Min(catchCurrent.StrainTime * catcherSpeedMultiplier, 265) / 265), 1.5); // Edge Dashes are easier at lower ms values } lastPlayerPosition = playerPosition; From 34429a02e7fac260f7c247d5b28b6744cefff5ec Mon Sep 17 00:00:00 2001 From: ilsubyeega Date: Sun, 28 Mar 2021 03:13:05 +0900 Subject: [PATCH 02/61] Use avatar_url from user first instead of a.ppy.sh in DrawableAvatar --- osu.Game/Users/Drawables/DrawableAvatar.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/osu.Game/Users/Drawables/DrawableAvatar.cs b/osu.Game/Users/Drawables/DrawableAvatar.cs index 3dae3afe3f..15ef7d2df0 100644 --- a/osu.Game/Users/Drawables/DrawableAvatar.cs +++ b/osu.Game/Users/Drawables/DrawableAvatar.cs @@ -5,6 +5,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; +using osu.Game.Online.API; namespace osu.Game.Users.Drawables { @@ -13,6 +14,9 @@ namespace osu.Game.Users.Drawables { private readonly User user; + [Resolved(CanBeNull=true)] + private IAPIProvider api { get; set; } + /// /// A simple, non-interactable avatar sprite for the specified user. /// @@ -30,9 +34,11 @@ namespace osu.Game.Users.Drawables [BackgroundDependencyLoader] private void load(LargeTextureStore textures) { - if (user != null && user.Id > 1) + if (api != null && user?.AvatarUrl != null) + Texture = textures.Get(user.AvatarUrl.StartsWith('/') ? $"{api.WebsiteRootUrl}{user.AvatarUrl}" : user.AvatarUrl); + else if (user != null && user.Id > 1) Texture = textures.Get($@"https://a.ppy.sh/{user.Id}"); - + Texture ??= textures.Get(@"Online/avatar-guest"); } From be08460bea4c10391dd980b2d437a5ee4364e248 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 27 Mar 2021 19:59:33 +0100 Subject: [PATCH 03/61] Fix formatting issues --- osu.Game/Users/Drawables/DrawableAvatar.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Users/Drawables/DrawableAvatar.cs b/osu.Game/Users/Drawables/DrawableAvatar.cs index 15ef7d2df0..079d4a932c 100644 --- a/osu.Game/Users/Drawables/DrawableAvatar.cs +++ b/osu.Game/Users/Drawables/DrawableAvatar.cs @@ -14,7 +14,7 @@ namespace osu.Game.Users.Drawables { private readonly User user; - [Resolved(CanBeNull=true)] + [Resolved(CanBeNull = true)] private IAPIProvider api { get; set; } /// @@ -38,7 +38,7 @@ namespace osu.Game.Users.Drawables Texture = textures.Get(user.AvatarUrl.StartsWith('/') ? $"{api.WebsiteRootUrl}{user.AvatarUrl}" : user.AvatarUrl); else if (user != null && user.Id > 1) Texture = textures.Get($@"https://a.ppy.sh/{user.Id}"); - + Texture ??= textures.Get(@"Online/avatar-guest"); } From 32df02084db91ecb7ab419a04df4f60d61f30ca4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 27 Mar 2021 20:00:49 +0100 Subject: [PATCH 04/61] Extract variable to avoid redundant accesses --- osu.Game/Users/Drawables/DrawableAvatar.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Users/Drawables/DrawableAvatar.cs b/osu.Game/Users/Drawables/DrawableAvatar.cs index 079d4a932c..c672c6fa1d 100644 --- a/osu.Game/Users/Drawables/DrawableAvatar.cs +++ b/osu.Game/Users/Drawables/DrawableAvatar.cs @@ -34,8 +34,9 @@ namespace osu.Game.Users.Drawables [BackgroundDependencyLoader] private void load(LargeTextureStore textures) { - if (api != null && user?.AvatarUrl != null) - Texture = textures.Get(user.AvatarUrl.StartsWith('/') ? $"{api.WebsiteRootUrl}{user.AvatarUrl}" : user.AvatarUrl); + string avatarUrl = user?.AvatarUrl; + if (api != null && avatarUrl != null) + Texture = textures.Get(avatarUrl.StartsWith('/') ? $"{api.WebsiteRootUrl}{avatarUrl}" : avatarUrl); else if (user != null && user.Id > 1) Texture = textures.Get($@"https://a.ppy.sh/{user.Id}"); From b3d3c7ecacf7762c22f5c2f556164c134ce9e8cc Mon Sep 17 00:00:00 2001 From: ilsubyeega Date: Mon, 29 Mar 2021 20:01:20 +0900 Subject: [PATCH 05/61] Revert relative url checking to AvatarUrl --- osu.Game/Users/Drawables/DrawableAvatar.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Users/Drawables/DrawableAvatar.cs b/osu.Game/Users/Drawables/DrawableAvatar.cs index c672c6fa1d..98fc10d7ed 100644 --- a/osu.Game/Users/Drawables/DrawableAvatar.cs +++ b/osu.Game/Users/Drawables/DrawableAvatar.cs @@ -36,7 +36,7 @@ namespace osu.Game.Users.Drawables { string avatarUrl = user?.AvatarUrl; if (api != null && avatarUrl != null) - Texture = textures.Get(avatarUrl.StartsWith('/') ? $"{api.WebsiteRootUrl}{avatarUrl}" : avatarUrl); + Texture = textures.Get(avatarUrl); else if (user != null && user.Id > 1) Texture = textures.Get($@"https://a.ppy.sh/{user.Id}"); From dc3af1e0f7634b373c43957188ec1bfec7f0a3f3 Mon Sep 17 00:00:00 2001 From: ilsubyeega Date: Mon, 29 Mar 2021 20:05:24 +0900 Subject: [PATCH 06/61] Remove IAPIProvider since its not required at DrawableAvatar --- osu.Game/Users/Drawables/DrawableAvatar.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/osu.Game/Users/Drawables/DrawableAvatar.cs b/osu.Game/Users/Drawables/DrawableAvatar.cs index 98fc10d7ed..930f1c07a1 100644 --- a/osu.Game/Users/Drawables/DrawableAvatar.cs +++ b/osu.Game/Users/Drawables/DrawableAvatar.cs @@ -5,7 +5,6 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; -using osu.Game.Online.API; namespace osu.Game.Users.Drawables { @@ -14,9 +13,6 @@ namespace osu.Game.Users.Drawables { private readonly User user; - [Resolved(CanBeNull = true)] - private IAPIProvider api { get; set; } - /// /// A simple, non-interactable avatar sprite for the specified user. /// @@ -35,7 +31,7 @@ namespace osu.Game.Users.Drawables private void load(LargeTextureStore textures) { string avatarUrl = user?.AvatarUrl; - if (api != null && avatarUrl != null) + if (avatarUrl != null) Texture = textures.Get(avatarUrl); else if (user != null && user.Id > 1) Texture = textures.Get($@"https://a.ppy.sh/{user.Id}"); From 0c1f624b56f662bedaee8b530fa9584e9753b59b Mon Sep 17 00:00:00 2001 From: ilsubyeega Date: Tue, 30 Mar 2021 17:03:01 +0900 Subject: [PATCH 07/61] Simply code under assuming that avatarUrl always non-null --- osu.Game/Users/Drawables/DrawableAvatar.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/osu.Game/Users/Drawables/DrawableAvatar.cs b/osu.Game/Users/Drawables/DrawableAvatar.cs index 930f1c07a1..ef074813a5 100644 --- a/osu.Game/Users/Drawables/DrawableAvatar.cs +++ b/osu.Game/Users/Drawables/DrawableAvatar.cs @@ -30,11 +30,8 @@ namespace osu.Game.Users.Drawables [BackgroundDependencyLoader] private void load(LargeTextureStore textures) { - string avatarUrl = user?.AvatarUrl; - if (avatarUrl != null) - Texture = textures.Get(avatarUrl); - else if (user != null && user.Id > 1) - Texture = textures.Get($@"https://a.ppy.sh/{user.Id}"); + if (user != null && user.Id > 1) + Texture = textures.Get(user.AvatarUrl); Texture ??= textures.Get(@"Online/avatar-guest"); } From 961bd1177c9bfbeb3203aab22a4602fee3d3f6b2 Mon Sep 17 00:00:00 2001 From: Pasi4K5 Date: Sun, 25 Apr 2021 00:39:36 +0200 Subject: [PATCH 08/61] 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 09/61] 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 10/61] 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 11/61] 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 12/61] 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 13/61] 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 14/61] 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 15/61] 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 16/61] 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 17/61] 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 18/61] 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 19/61] 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 20/61] 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 21/61] 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 22/61] 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 23/61] 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 24/61] 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 25/61] 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 26/61] 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 27/61] 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 28/61] 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 29/61] 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 30/61] 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 31/61] 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 a3c78674a12022506f8cd42d2407e7dfe8ad176a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 25 May 2021 18:09:24 +0900 Subject: [PATCH 32/61] Add new interface for autoplay mods --- .../Mods/EmptyFreeformModAutoplay.cs | 3 +-- .../Mods/PippidonModAutoplay.cs | 3 +-- .../Mods/EmptyScrollingModAutoplay.cs | 3 +-- .../Mods/PippidonModAutoplay.cs | 3 +-- osu.Game.Rulesets.Catch/Mods/CatchModAutoplay.cs | 3 +-- osu.Game.Rulesets.Mania/Mods/ManiaModAutoplay.cs | 3 +-- osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs | 3 +-- osu.Game.Rulesets.Taiko/Mods/TaikoModAutoplay.cs | 3 +-- osu.Game/Rulesets/Mods/ICreateReplay.cs | 14 ++++++++++++++ osu.Game/Rulesets/Mods/ModAutoplay.cs | 13 +------------ 10 files changed, 23 insertions(+), 28 deletions(-) create mode 100644 osu.Game/Rulesets/Mods/ICreateReplay.cs diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Mods/EmptyFreeformModAutoplay.cs b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Mods/EmptyFreeformModAutoplay.cs index d5c1e9bd15..f705009d18 100644 --- a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Mods/EmptyFreeformModAutoplay.cs +++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Mods/EmptyFreeformModAutoplay.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using osu.Game.Beatmaps; -using osu.Game.Rulesets.EmptyFreeform.Objects; using osu.Game.Rulesets.EmptyFreeform.Replays; using osu.Game.Rulesets.Mods; using osu.Game.Scoring; @@ -11,7 +10,7 @@ using osu.Game.Users; namespace osu.Game.Rulesets.EmptyFreeform.Mods { - public class EmptyFreeformModAutoplay : ModAutoplay + public class EmptyFreeformModAutoplay : ModAutoplay { public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score { diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Mods/PippidonModAutoplay.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Mods/PippidonModAutoplay.cs index 8ea334c99c..4565c97d1a 100644 --- a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Mods/PippidonModAutoplay.cs +++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Mods/PippidonModAutoplay.cs @@ -4,14 +4,13 @@ using System.Collections.Generic; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Pippidon.Objects; using osu.Game.Rulesets.Pippidon.Replays; using osu.Game.Scoring; using osu.Game.Users; namespace osu.Game.Rulesets.Pippidon.Mods { - public class PippidonModAutoplay : ModAutoplay + public class PippidonModAutoplay : ModAutoplay { public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score { diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Mods/EmptyScrollingModAutoplay.cs b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Mods/EmptyScrollingModAutoplay.cs index 6dad1ff43b..431994e098 100644 --- a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Mods/EmptyScrollingModAutoplay.cs +++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Mods/EmptyScrollingModAutoplay.cs @@ -3,7 +3,6 @@ using osu.Game.Beatmaps; using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.EmptyScrolling.Objects; using osu.Game.Rulesets.EmptyScrolling.Replays; using osu.Game.Scoring; using osu.Game.Users; @@ -11,7 +10,7 @@ using System.Collections.Generic; namespace osu.Game.Rulesets.EmptyScrolling.Mods { - public class EmptyScrollingModAutoplay : ModAutoplay + public class EmptyScrollingModAutoplay : ModAutoplay { public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score { diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Mods/PippidonModAutoplay.cs b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Mods/PippidonModAutoplay.cs index 8ea334c99c..4565c97d1a 100644 --- a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Mods/PippidonModAutoplay.cs +++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Mods/PippidonModAutoplay.cs @@ -4,14 +4,13 @@ using System.Collections.Generic; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Pippidon.Objects; using osu.Game.Rulesets.Pippidon.Replays; using osu.Game.Scoring; using osu.Game.Users; namespace osu.Game.Rulesets.Pippidon.Mods { - public class PippidonModAutoplay : ModAutoplay + public class PippidonModAutoplay : ModAutoplay { public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score { diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModAutoplay.cs b/osu.Game.Rulesets.Catch/Mods/CatchModAutoplay.cs index e1eceea606..f1b51e51d0 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModAutoplay.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModAutoplay.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using osu.Game.Beatmaps; -using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Replays; using osu.Game.Rulesets.Mods; using osu.Game.Scoring; @@ -11,7 +10,7 @@ using osu.Game.Users; namespace osu.Game.Rulesets.Catch.Mods { - public class CatchModAutoplay : ModAutoplay + public class CatchModAutoplay : ModAutoplay { public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score { diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModAutoplay.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModAutoplay.cs index 105d88129c..6ae854e7f3 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModAutoplay.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModAutoplay.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps; -using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Replays; using osu.Game.Rulesets.Mods; using osu.Game.Scoring; @@ -12,7 +11,7 @@ using osu.Game.Users; namespace osu.Game.Rulesets.Mania.Mods { - public class ManiaModAutoplay : ModAutoplay + public class ManiaModAutoplay : ModAutoplay { public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score { diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs index 3b1f271d41..652da7123e 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs @@ -6,14 +6,13 @@ using System.Collections.Generic; using System.Linq; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Replays; using osu.Game.Scoring; using osu.Game.Users; namespace osu.Game.Rulesets.Osu.Mods { - public class OsuModAutoplay : ModAutoplay + public class OsuModAutoplay : ModAutoplay { public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModAutopilot)).Append(typeof(OsuModSpunOut)).ToArray(); diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModAutoplay.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModAutoplay.cs index 64e59b64d0..31d9abf8b2 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModAutoplay.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModAutoplay.cs @@ -4,14 +4,13 @@ using System.Collections.Generic; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Replays; using osu.Game.Scoring; using osu.Game.Users; namespace osu.Game.Rulesets.Taiko.Mods { - public class TaikoModAutoplay : ModAutoplay + public class TaikoModAutoplay : ModAutoplay { public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score { diff --git a/osu.Game/Rulesets/Mods/ICreateReplay.cs b/osu.Game/Rulesets/Mods/ICreateReplay.cs new file mode 100644 index 0000000000..098bd8799a --- /dev/null +++ b/osu.Game/Rulesets/Mods/ICreateReplay.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 System.Collections.Generic; +using osu.Game.Beatmaps; +using osu.Game.Scoring; + +namespace osu.Game.Rulesets.Mods +{ + public interface ICreateReplay + { + public Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods); + } +} diff --git a/osu.Game/Rulesets/Mods/ModAutoplay.cs b/osu.Game/Rulesets/Mods/ModAutoplay.cs index d6e1d46b06..b84b5671e1 100644 --- a/osu.Game/Rulesets/Mods/ModAutoplay.cs +++ b/osu.Game/Rulesets/Mods/ModAutoplay.cs @@ -7,22 +7,11 @@ using osu.Framework.Graphics.Sprites; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Replays; -using osu.Game.Rulesets.Objects; -using osu.Game.Rulesets.UI; using osu.Game.Scoring; namespace osu.Game.Rulesets.Mods { - public abstract class ModAutoplay : ModAutoplay, IApplicableToDrawableRuleset - where T : HitObject - { - public virtual void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) - { - drawableRuleset.SetReplayScore(CreateReplayScore(drawableRuleset.Beatmap, drawableRuleset.Mods)); - } - } - - public abstract class ModAutoplay : Mod, IApplicableFailOverride + public abstract class ModAutoplay : Mod, IApplicableFailOverride, ICreateReplay { public override string Name => "Autoplay"; public override string Acronym => "AT"; From c2b938a29f5fef339a3adeb9ab62fa647a06dc12 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 25 May 2021 18:09:37 +0900 Subject: [PATCH 33/61] Remove autoplay consideration from `Player` --- osu.Game/Screens/Play/Player.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 39f9e2d388..b818dbea08 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -161,9 +161,7 @@ namespace osu.Game.Screens.Play if (!LoadedBeatmapSuccessfully) return; - // replays should never be recorded or played back when autoplay is enabled - if (!Mods.Value.Any(m => m is ModAutoplay)) - PrepareReplay(); + PrepareReplay(); gameActive.BindValueChanged(_ => updatePauseOnFocusLostState(), true); } From 7f9318d97658d1dcbce49162c1fa3d7d9bdde4db Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 25 May 2021 18:36:47 +0900 Subject: [PATCH 34/61] Expose `GameplayBeatmap` to derived `Player` classes --- osu.Game/Screens/Play/Player.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index b818dbea08..8e9c2fadcd 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -145,7 +145,7 @@ namespace osu.Game.Screens.Play Configuration = configuration ?? new PlayerConfiguration(); } - private GameplayBeatmap gameplayBeatmap; + protected GameplayBeatmap GameplayBeatmap { get; private set; } private ScreenSuspensionHandler screenSuspension; @@ -221,10 +221,10 @@ namespace osu.Game.Screens.Play InternalChild = GameplayClockContainer = CreateGameplayClockContainer(Beatmap.Value, DrawableRuleset.GameplayStartTime); - AddInternal(gameplayBeatmap = new GameplayBeatmap(playableBeatmap)); + AddInternal(GameplayBeatmap = new GameplayBeatmap(playableBeatmap)); AddInternal(screenSuspension = new ScreenSuspensionHandler(GameplayClockContainer)); - dependencies.CacheAs(gameplayBeatmap); + dependencies.CacheAs(GameplayBeatmap); var beatmapSkinProvider = new BeatmapSkinProvidingContainer(Beatmap.Value.Skin); @@ -282,7 +282,7 @@ namespace osu.Game.Screens.Play { HealthProcessor.ApplyResult(r); ScoreProcessor.ApplyResult(r); - gameplayBeatmap.ApplyResult(r); + GameplayBeatmap.ApplyResult(r); }; DrawableRuleset.RevertResult += r => @@ -946,7 +946,7 @@ namespace osu.Game.Screens.Play using (var stream = new MemoryStream()) { - new LegacyScoreEncoder(score, gameplayBeatmap.PlayableBeatmap).Encode(stream); + new LegacyScoreEncoder(score, GameplayBeatmap.PlayableBeatmap).Encode(stream); replayReader = new LegacyByteArrayReader(stream.ToArray(), "replay.osr"); } From 7c89dbcd3502e68639476f9d99ab92e63205c870 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 25 May 2021 18:37:04 +0900 Subject: [PATCH 35/61] Externalise autoplay generation --- osu.Game/Screens/Play/ReplayPlayer.cs | 21 ++++++++++++++++-- osu.Game/Screens/Select/PlaySongSelect.cs | 26 +++++++++++++++-------- 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index e23cc22929..07c3d197da 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.cs @@ -1,9 +1,13 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using System.Collections.Generic; using System.Threading.Tasks; +using osu.Framework.Allocation; using osu.Framework.Input.Bindings; using osu.Game.Input.Bindings; +using osu.Game.Rulesets.Mods; using osu.Game.Scoring; using osu.Game.Screens.Ranking; @@ -11,15 +15,28 @@ namespace osu.Game.Screens.Play { public class ReplayPlayer : Player, IKeyBindingHandler { - protected readonly Score Score; + protected Score Score { get; private set; } + + private readonly Func, Score> createScore; // Disallow replays from failing. (see https://github.com/ppy/osu/issues/6108) protected override bool CheckModsAllowFailure() => false; public ReplayPlayer(Score score, PlayerConfiguration configuration = null) + : this((_, __) => score, configuration) + { + } + + public ReplayPlayer(Func, Score> createScore, PlayerConfiguration configuration = null) : base(configuration) { - Score = score; + this.createScore = createScore; + } + + [BackgroundDependencyLoader] + private void load() + { + Score = createScore(GameplayBeatmap, Mods.Value); } protected override void PrepareReplay() diff --git a/osu.Game/Screens/Select/PlaySongSelect.cs b/osu.Game/Screens/Select/PlaySongSelect.cs index dfb4b59060..357222c109 100644 --- a/osu.Game/Screens/Select/PlaySongSelect.cs +++ b/osu.Game/Screens/Select/PlaySongSelect.cs @@ -21,7 +21,7 @@ namespace osu.Game.Screens.Select public class PlaySongSelect : SongSelect { private bool removeAutoModOnResume; - private OsuScreen player; + private OsuScreen playerLoader; [Resolved(CanBeNull = true)] private NotificationOverlay notifications { get; set; } @@ -49,7 +49,7 @@ namespace osu.Game.Screens.Select { base.OnResuming(last); - player = null; + playerLoader = null; if (removeAutoModOnResume) { @@ -79,14 +79,14 @@ namespace osu.Game.Screens.Select protected override bool OnStart() { - if (player != null) return false; + if (playerLoader != null) return false; // Ctrl+Enter should start map with autoplay enabled. if (GetContainingInputManager().CurrentState?.Keyboard.ControlPressed == true) { - var autoplayMod = getAutoplayMod(); + var autoInstance = getAutoplayMod(); - if (autoplayMod == null) + if (autoInstance == null) { notifications?.Post(new SimpleNotification { @@ -97,18 +97,26 @@ namespace osu.Game.Screens.Select var mods = Mods.Value; - if (mods.All(m => m.GetType() != autoplayMod.GetType())) + if (mods.All(m => m.GetType() != autoInstance.GetType())) { - Mods.Value = mods.Append(autoplayMod).ToArray(); + Mods.Value = mods.Append(autoInstance).ToArray(); removeAutoModOnResume = true; } } SampleConfirm?.Play(); - this.Push(player = new PlayerLoader(() => new SoloPlayer())); - + this.Push(playerLoader = new PlayerLoader(createPlayer)); return true; + + Player createPlayer() + { + var autoplayMod = Mods.Value.OfType().FirstOrDefault(); + if (autoplayMod != null) + return new ReplayPlayer((beatmap, mods) => autoplayMod.CreateReplayScore(beatmap, mods)); + + return new SoloPlayer(); + } } } } From bdbd64c88d6a9cace3862fcdd45ccb6109d38cda Mon Sep 17 00:00:00 2001 From: Pasi4K5 Date: Tue, 25 May 2021 21:32:18 +0200 Subject: [PATCH 36/61] 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 37/61] 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 38/61] 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 39/61] 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 40/61] 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 41/61] 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 42/61] 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 43/61] 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 44/61] 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; From 9221213fe527bdc4e33188a1a9a7642edd240ab4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Jun 2021 14:19:13 +0900 Subject: [PATCH 45/61] Fix potential nullref is beatmap load failed --- osu.Game/Screens/Play/ReplayPlayer.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index 07c3d197da..f26675cc64 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.cs @@ -36,6 +36,8 @@ namespace osu.Game.Screens.Play [BackgroundDependencyLoader] private void load() { + if (!LoadedBeatmapSuccessfully) return; + Score = createScore(GameplayBeatmap, Mods.Value); } From cbf3ef5400bbc7ea2e75f532845175f99a002341 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Jun 2021 14:22:16 +0900 Subject: [PATCH 46/61] Create replay via the `ICreateReplay` interface instead of explicitly `ModAutoplay` --- osu.Game/Screens/Select/PlaySongSelect.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Select/PlaySongSelect.cs b/osu.Game/Screens/Select/PlaySongSelect.cs index 357222c109..418cf23ce7 100644 --- a/osu.Game/Screens/Select/PlaySongSelect.cs +++ b/osu.Game/Screens/Select/PlaySongSelect.cs @@ -111,9 +111,9 @@ namespace osu.Game.Screens.Select Player createPlayer() { - var autoplayMod = Mods.Value.OfType().FirstOrDefault(); - if (autoplayMod != null) - return new ReplayPlayer((beatmap, mods) => autoplayMod.CreateReplayScore(beatmap, mods)); + var replayGeneratingMod = Mods.Value.OfType().FirstOrDefault(); + if (replayGeneratingMod != null) + return new ReplayPlayer((beatmap, mods) => replayGeneratingMod.CreateReplayScore(beatmap, mods)); return new SoloPlayer(); } From 240f7facba9c365d1d3850f314ce2c4e34a24254 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Jun 2021 15:39:02 +0900 Subject: [PATCH 47/61] Add local concessions for autoplay test handling --- osu.Game/Tests/Visual/TestPlayer.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/osu.Game/Tests/Visual/TestPlayer.cs b/osu.Game/Tests/Visual/TestPlayer.cs index 0addc9de75..99308f8d75 100644 --- a/osu.Game/Tests/Visual/TestPlayer.cs +++ b/osu.Game/Tests/Visual/TestPlayer.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Rulesets.Judgements; @@ -48,6 +49,20 @@ namespace osu.Game.Tests.Visual PauseOnFocusLost = pauseOnFocusLost; } + protected override void PrepareReplay() + { + var replayGeneratingMod = Mods.Value.OfType().FirstOrDefault(); + + if (replayGeneratingMod != null) + { + // This logic should really not exist (and tests should be instantiating a ReplayPlayer), but a lot of base work is required to make that happen. + DrawableRuleset?.SetReplayScore(replayGeneratingMod.CreateReplayScore(GameplayBeatmap.PlayableBeatmap, Mods.Value)); + return; + } + + base.PrepareReplay(); + } + [BackgroundDependencyLoader] private void load() { From 3ba0d29108fc250e9c29ea37ec14a9a900741ebf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Jun 2021 15:44:24 +0900 Subject: [PATCH 48/61] Fix incorrect beatmap being parsed down for autoplay generation --- osu.Game/Screens/Play/ReplayPlayer.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index f26675cc64..91236ad607 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Input.Bindings; +using osu.Game.Beatmaps; using osu.Game.Input.Bindings; using osu.Game.Rulesets.Mods; using osu.Game.Scoring; @@ -17,7 +18,7 @@ namespace osu.Game.Screens.Play { protected Score Score { get; private set; } - private readonly Func, Score> createScore; + private readonly Func, Score> createScore; // Disallow replays from failing. (see https://github.com/ppy/osu/issues/6108) protected override bool CheckModsAllowFailure() => false; @@ -27,7 +28,7 @@ namespace osu.Game.Screens.Play { } - public ReplayPlayer(Func, Score> createScore, PlayerConfiguration configuration = null) + public ReplayPlayer(Func, Score> createScore, PlayerConfiguration configuration = null) : base(configuration) { this.createScore = createScore; @@ -38,7 +39,7 @@ namespace osu.Game.Screens.Play { if (!LoadedBeatmapSuccessfully) return; - Score = createScore(GameplayBeatmap, Mods.Value); + Score = createScore(GameplayBeatmap.PlayableBeatmap, Mods.Value); } protected override void PrepareReplay() From 790f1dacc9839d0267797821bb59042c884b549d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Jun 2021 16:24:38 +0900 Subject: [PATCH 49/61] Ensure `ScoreProcessor` is still hooked up in special case --- osu.Game/Tests/Visual/TestPlayer.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/osu.Game/Tests/Visual/TestPlayer.cs b/osu.Game/Tests/Visual/TestPlayer.cs index 99308f8d75..b7e1c68c89 100644 --- a/osu.Game/Tests/Visual/TestPlayer.cs +++ b/osu.Game/Tests/Visual/TestPlayer.cs @@ -51,12 +51,16 @@ namespace osu.Game.Tests.Visual protected override void PrepareReplay() { - var replayGeneratingMod = Mods.Value.OfType().FirstOrDefault(); + var autoplayMod = Mods.Value.OfType().FirstOrDefault(); - if (replayGeneratingMod != null) + // This logic should really not exist (and tests should be instantiating a ReplayPlayer), but a lot of base work is required to make that happen. + if (autoplayMod != null) { - // This logic should really not exist (and tests should be instantiating a ReplayPlayer), but a lot of base work is required to make that happen. - DrawableRuleset?.SetReplayScore(replayGeneratingMod.CreateReplayScore(GameplayBeatmap.PlayableBeatmap, Mods.Value)); + var replayScore = autoplayMod.CreateReplayScore(GameplayBeatmap.PlayableBeatmap, Mods.Value); + + DrawableRuleset?.SetReplayScore(replayScore); + + ScoreProcessor.NewJudgement += result => ScoreProcessor.PopulateScore(replayScore.ScoreInfo); return; } From 6e861a9b7fcb107e2f12887ee3f4b93e253beedd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Jun 2021 16:24:38 +0900 Subject: [PATCH 50/61] Revert incorrect `ScoreProcessor` change --- osu.Game/Tests/Visual/TestPlayer.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/osu.Game/Tests/Visual/TestPlayer.cs b/osu.Game/Tests/Visual/TestPlayer.cs index b7e1c68c89..ceb886f9c4 100644 --- a/osu.Game/Tests/Visual/TestPlayer.cs +++ b/osu.Game/Tests/Visual/TestPlayer.cs @@ -56,11 +56,7 @@ namespace osu.Game.Tests.Visual // This logic should really not exist (and tests should be instantiating a ReplayPlayer), but a lot of base work is required to make that happen. if (autoplayMod != null) { - var replayScore = autoplayMod.CreateReplayScore(GameplayBeatmap.PlayableBeatmap, Mods.Value); - - DrawableRuleset?.SetReplayScore(replayScore); - - ScoreProcessor.NewJudgement += result => ScoreProcessor.PopulateScore(replayScore.ScoreInfo); + DrawableRuleset?.SetReplayScore(autoplayMod.CreateReplayScore(GameplayBeatmap.PlayableBeatmap, Mods.Value)); return; } From 7a71cc1e821e0346e1d9c19fc4506388928b9ee3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Jun 2021 16:54:29 +0900 Subject: [PATCH 51/61] Fix actually incorrect navigation test (can no longer retry from autoplay results) --- osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index 253e448bb4..dd05ce9b7e 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -95,7 +95,7 @@ namespace osu.Game.Tests.Visual.Navigation AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); - AddStep("set autoplay", () => Game.SelectedMods.Value = new[] { new OsuModAutoplay() }); + AddStep("set nofail", () => Game.SelectedMods.Value = new[] { new OsuModNoFail() }); AddStep("press enter", () => InputManager.Key(Key.Enter)); AddUntilStep("wait for player", () => (player = Game.ScreenStack.CurrentScreen as Player) != null); From f14c0eae9956641f8c77179f165b059abc98914d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Jun 2021 17:28:41 +0900 Subject: [PATCH 52/61] Fix editor no longer creating autoplay frames --- osu.Game/Rulesets/UI/DrawableRuleset.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index a2dade2627..0667145ffb 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -349,6 +349,9 @@ namespace osu.Game.Rulesets.UI foreach (var mod in mods.OfType>()) mod.ApplyToDrawableRuleset(this); + foreach (var mod in mods.OfType()) + SetReplayScore(mod.CreateReplayScore(Beatmap, mods)); + foreach (var mod in mods.OfType()) mod.ReadFromConfig(config); } From 8a76d97b63a65fe4e303525e4bd09d809cafa2f1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 2 Jun 2021 11:06:30 +0900 Subject: [PATCH 53/61] Remove replay logic from `DrawableRuleset` (and implement in `DrawableEditorRulesetWrapper`) --- .../Rulesets/Edit/DrawableEditorRulesetWrapper.cs | 14 +++++++++++--- osu.Game/Rulesets/UI/DrawableRuleset.cs | 12 +----------- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/osu.Game/Rulesets/Edit/DrawableEditorRulesetWrapper.cs b/osu.Game/Rulesets/Edit/DrawableEditorRulesetWrapper.cs index 8166e6b8ce..071f01ca00 100644 --- a/osu.Game/Rulesets/Edit/DrawableEditorRulesetWrapper.cs +++ b/osu.Game/Rulesets/Edit/DrawableEditorRulesetWrapper.cs @@ -1,9 +1,11 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.UI; using osu.Game.Screens.Edit; @@ -52,15 +54,21 @@ namespace osu.Game.Rulesets.Edit if (changeHandler != null) { // for now only regenerate replay on a finalised state change, not HitObjectUpdated. - changeHandler.OnStateChange += updateReplay; + changeHandler.OnStateChange += () => Scheduler.AddOnce(regenerateAutoplay); } else { - beatmap.HitObjectUpdated += _ => updateReplay(); + beatmap.HitObjectUpdated += _ => Scheduler.AddOnce(regenerateAutoplay); } + + Scheduler.AddOnce(regenerateAutoplay); } - private void updateReplay() => Scheduler.AddOnce(drawableRuleset.RegenerateAutoplay); + private void regenerateAutoplay() + { + var autoplayMod = drawableRuleset.Mods.OfType().Single(); + drawableRuleset.SetReplayScore(autoplayMod.CreateReplayScore(drawableRuleset.Beatmap, drawableRuleset.Mods)); + } private void addHitObject(HitObject hitObject) { diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index 0667145ffb..16a411d478 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -182,18 +182,11 @@ namespace osu.Game.Rulesets.UI .WithChild(ResumeOverlay))); } - RegenerateAutoplay(); + applyRulesetMods(Mods, config); loadObjects(cancellationToken ?? default); } - public void RegenerateAutoplay() - { - // for now this is applying mods which aren't just autoplay. - // we'll need to reconsider this flow in the future. - applyRulesetMods(Mods, config); - } - /// /// Creates and adds drawable representations of hit objects to the play field. /// @@ -349,9 +342,6 @@ namespace osu.Game.Rulesets.UI foreach (var mod in mods.OfType>()) mod.ApplyToDrawableRuleset(this); - foreach (var mod in mods.OfType()) - SetReplayScore(mod.CreateReplayScore(Beatmap, mods)); - foreach (var mod in mods.OfType()) mod.ReadFromConfig(config); } From 911256603bf0ba6d83c05694c6d20ecdbafa041b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 2 Jun 2021 11:10:02 +0900 Subject: [PATCH 54/61] Rewrite comment to hopefully be more informative --- osu.Game/Tests/Visual/TestPlayer.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game/Tests/Visual/TestPlayer.cs b/osu.Game/Tests/Visual/TestPlayer.cs index ceb886f9c4..09da4db952 100644 --- a/osu.Game/Tests/Visual/TestPlayer.cs +++ b/osu.Game/Tests/Visual/TestPlayer.cs @@ -51,9 +51,15 @@ namespace osu.Game.Tests.Visual protected override void PrepareReplay() { + // Generally, replay generation is handled by whatever is constructing the player. + // This is implemented locally here to ease migration of test scenes that have some executions + // running with autoplay and some not, but are not written in a way that lends to instantiating + // different `Player` types. + // + // Eventually we will want to remove this and update all test usages which rely on autoplay to use + // a `TestReplayPlayer`. var autoplayMod = Mods.Value.OfType().FirstOrDefault(); - // This logic should really not exist (and tests should be instantiating a ReplayPlayer), but a lot of base work is required to make that happen. if (autoplayMod != null) { DrawableRuleset?.SetReplayScore(autoplayMod.CreateReplayScore(GameplayBeatmap.PlayableBeatmap, Mods.Value)); From 45984f035b731ff0d39510b22df7375455fafc39 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 2 Jun 2021 11:33:43 +0900 Subject: [PATCH 55/61] Make autoplay tests test via the `ReplayPlayer` code path --- .../Visual/Gameplay/TestSceneAutoplay.cs | 4 +- osu.Game/Tests/Visual/TestReplayPlayer.cs | 75 +++++++++++++++++++ 2 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 osu.Game/Tests/Visual/TestReplayPlayer.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs index e47c782bca..bc7cf8eee2 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs @@ -18,12 +18,12 @@ namespace osu.Game.Tests.Visual.Gameplay [Description("Player instantiated with an autoplay mod.")] public class TestSceneAutoplay : TestSceneAllRulesetPlayers { - protected new TestPlayer Player => (TestPlayer)base.Player; + protected new TestReplayPlayer Player => (TestReplayPlayer)base.Player; protected override Player CreatePlayer(Ruleset ruleset) { SelectedMods.Value = new[] { ruleset.GetAutoplayMod() }; - return new TestPlayer(false); + return new TestReplayPlayer(false); } protected override void AddCheckSteps() diff --git a/osu.Game/Tests/Visual/TestReplayPlayer.cs b/osu.Game/Tests/Visual/TestReplayPlayer.cs new file mode 100644 index 0000000000..ac47d186eb --- /dev/null +++ b/osu.Game/Tests/Visual/TestReplayPlayer.cs @@ -0,0 +1,75 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.UI; +using osu.Game.Scoring; +using osu.Game.Screens.Play; + +namespace osu.Game.Tests.Visual +{ + /// + /// A player that exposes many components that would otherwise not be available, for testing purposes. + /// + public class TestReplayPlayer : ReplayPlayer + { + protected override bool PauseOnFocusLost { get; } + + public new DrawableRuleset DrawableRuleset => base.DrawableRuleset; + + /// + /// Mods from *player* (not OsuScreen). + /// + public new Bindable> Mods => base.Mods; + + public new HUDOverlay HUDOverlay => base.HUDOverlay; + + public new GameplayClockContainer GameplayClockContainer => base.GameplayClockContainer; + + public new ScoreProcessor ScoreProcessor => base.ScoreProcessor; + + public new HealthProcessor HealthProcessor => base.HealthProcessor; + + public new bool PauseCooldownActive => base.PauseCooldownActive; + + public readonly List Results = new List(); + + /// + /// Instantiate a replay player that renders an autoplay mod. + /// + public TestReplayPlayer(bool allowPause = true, bool showResults = true, bool pauseOnFocusLost = false) + : base((beatmap, mods) => mods.OfType().First().CreateReplayScore(beatmap, mods), new PlayerConfiguration + { + AllowPause = allowPause, + ShowResults = showResults + }) + { + PauseOnFocusLost = pauseOnFocusLost; + } + + /// + /// Instantiate a replay player that renders the provided replay. + /// + public TestReplayPlayer(Score score, bool allowPause = true, bool showResults = true, bool pauseOnFocusLost = false) + : base(score, new PlayerConfiguration + { + AllowPause = allowPause, + ShowResults = showResults + }) + { + PauseOnFocusLost = pauseOnFocusLost; + } + + [BackgroundDependencyLoader] + private void load() + { + ScoreProcessor.NewJudgement += r => Results.Add(r); + } + } +} From a15cac6f5386651adf0af91d990f94525da98b99 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 2 Jun 2021 15:44:04 +0900 Subject: [PATCH 56/61] Change the way `Score` is initialised in `Player` to better lend to population of metadata --- .../Spectate/MultiSpectatorPlayer.cs | 4 +- .../OnlinePlay/Playlists/PlaylistsPlayer.cs | 8 +- osu.Game/Screens/Play/Player.cs | 73 ++++++++----------- osu.Game/Screens/Play/ReplayPlayer.cs | 24 +----- osu.Game/Screens/Play/SpectatorPlayer.cs | 12 +-- 5 files changed, 45 insertions(+), 76 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayer.cs index 0fe9e01d9d..2c157b0564 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayer.cs @@ -17,7 +17,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate public class MultiSpectatorPlayer : SpectatorPlayer { private readonly Bindable waitingOnFrames = new Bindable(true); - private readonly Score score; private readonly ISpectatorPlayerClock spectatorPlayerClock; /// @@ -28,7 +27,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate public MultiSpectatorPlayer([NotNull] Score score, [NotNull] ISpectatorPlayerClock spectatorPlayerClock) : base(score) { - this.score = score; this.spectatorPlayerClock = spectatorPlayerClock; } @@ -43,7 +41,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate base.UpdateAfterChildren(); // This is required because the frame stable clock is set to WaitingOnFrames = false for one frame. - waitingOnFrames.Value = DrawableRuleset.FrameStableClock.WaitingOnFrames.Value || score.Replay.Frames.Count == 0; + waitingOnFrames.Value = DrawableRuleset.FrameStableClock.WaitingOnFrames.Value || Score.Replay.Frames.Count == 0; } protected override GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs index 260d4961ff..a2ef715367 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs @@ -54,11 +54,11 @@ namespace osu.Game.Screens.OnlinePlay.Playlists return new PlaylistsResultsScreen(score, RoomId.Value.Value, PlaylistItem, true); } - protected override Score CreateScore() + protected override void PrepareScoreForResults() { - var score = base.CreateScore(); - score.ScoreInfo.TotalScore = (int)Math.Round(ScoreProcessor.GetStandardisedScore()); - return score; + base.PrepareScoreForResults(); + + Score.ScoreInfo.TotalScore = (int)Math.Round(ScoreProcessor.GetStandardisedScore()); } protected override void Dispose(bool isDisposing) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 99570f6c5e..0d1ebd30fc 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; -using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; @@ -24,10 +23,8 @@ using osu.Game.IO.Archives; using osu.Game.Online.API; using osu.Game.Online.Spectator; using osu.Game.Overlays; -using osu.Game.Replays; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; using osu.Game.Scoring; @@ -137,6 +134,8 @@ namespace osu.Game.Screens.Play public readonly PlayerConfiguration Configuration; + protected Score Score { get; private set; } + /// /// Create a new player instance. /// @@ -161,22 +160,32 @@ namespace osu.Game.Screens.Play if (!LoadedBeatmapSuccessfully) return; + Score = CreateScore(); + + // ensure the score is in a consistent state with the current player. + Score.ScoreInfo.Beatmap = Beatmap.Value.BeatmapInfo; + Score.ScoreInfo.Ruleset = rulesetInfo; + Score.ScoreInfo.Mods = Mods.Value.ToArray(); + PrepareReplay(); + ScoreProcessor.NewJudgement += result => ScoreProcessor.PopulateScore(Score.ScoreInfo); + gameActive.BindValueChanged(_ => updatePauseOnFocusLostState(), true); } - [CanBeNull] - private Score recordingScore; - /// /// Run any recording / playback setup for replays. /// protected virtual void PrepareReplay() { - DrawableRuleset.SetRecordTarget(recordingScore = new Score()); + DrawableRuleset.SetRecordTarget(Score); + } - ScoreProcessor.NewJudgement += result => ScoreProcessor.PopulateScore(recordingScore.ScoreInfo); + protected virtual void PrepareScoreForResults() + { + // perform one final population to ensure everything is up-to-date. + ScoreProcessor.PopulateScore(Score.ScoreInfo); } [BackgroundDependencyLoader(true)] @@ -631,11 +640,11 @@ namespace osu.Game.Screens.Play prepareScoreForDisplayTask ??= Task.Run(async () => { - var score = CreateScore(); + PrepareScoreForResults(); try { - await PrepareScoreForResultsAsync(score).ConfigureAwait(false); + await PrepareScoreForResultsAsync(Score).ConfigureAwait(false); } catch (Exception ex) { @@ -644,14 +653,14 @@ namespace osu.Game.Screens.Play try { - await ImportScore(score).ConfigureAwait(false); + await ImportScore(Score).ConfigureAwait(false); } catch (Exception ex) { Logger.Error(ex, "Score import failed!"); } - return score.ScoreInfo; + return Score.ScoreInfo; }); if (skipStoryboardOutro) @@ -903,41 +912,19 @@ namespace osu.Game.Screens.Play } /// - /// Creates the player's . + /// Creates the player's . /// - /// The . - protected virtual Score CreateScore() - { - var score = new Score + /// The . + protected virtual Score CreateScore() => + new Score { - ScoreInfo = new ScoreInfo - { - Beatmap = Beatmap.Value.BeatmapInfo, - Ruleset = rulesetInfo, - Mods = Mods.Value.ToArray(), - } + ScoreInfo = new ScoreInfo { User = api.LocalUser.Value }, }; - if (DrawableRuleset.ReplayScore != null) - { - score.ScoreInfo.User = DrawableRuleset.ReplayScore.ScoreInfo?.User ?? new GuestUser(); - score.Replay = DrawableRuleset.ReplayScore.Replay; - } - else - { - score.ScoreInfo.User = api.LocalUser.Value; - score.Replay = new Replay { Frames = recordingScore?.Replay.Frames.ToList() ?? new List() }; - } - - ScoreProcessor.PopulateScore(score.ScoreInfo); - - return score; - } - /// - /// Imports the player's to the local database. + /// Imports the player's to the local database. /// - /// The to import. + /// The to import. /// The imported score. protected virtual async Task ImportScore(Score score) { @@ -968,9 +955,9 @@ namespace osu.Game.Screens.Play } /// - /// Prepare the for display at results. + /// Prepare the for display at results. /// - /// The to prepare. + /// The to prepare. /// A task that prepares the provided score. On completion, the score is assumed to be ready for display. protected virtual Task PrepareScoreForResultsAsync(Score score) => Task.CompletedTask; diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index 91236ad607..e440e7d34e 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; -using osu.Framework.Allocation; using osu.Framework.Input.Bindings; using osu.Game.Beatmaps; using osu.Game.Input.Bindings; @@ -16,8 +15,6 @@ namespace osu.Game.Screens.Play { public class ReplayPlayer : Player, IKeyBindingHandler { - protected Score Score { get; private set; } - private readonly Func, Score> createScore; // Disallow replays from failing. (see https://github.com/ppy/osu/issues/6108) @@ -34,28 +31,15 @@ namespace osu.Game.Screens.Play this.createScore = createScore; } - [BackgroundDependencyLoader] - private void load() - { - if (!LoadedBeatmapSuccessfully) return; - - Score = createScore(GameplayBeatmap.PlayableBeatmap, Mods.Value); - } - protected override void PrepareReplay() { DrawableRuleset?.SetReplayScore(Score); + + // todo: move to base class along with Score? + ScoreProcessor.NewJudgement += result => ScoreProcessor.PopulateScore(Score.ScoreInfo); } - protected override Score CreateScore() - { - var baseScore = base.CreateScore(); - - // Since the replay score doesn't contain statistics, we'll pass them through here. - Score.ScoreInfo.HitEvents = baseScore.ScoreInfo.HitEvents; - - return Score; - } + protected override Score CreateScore() => createScore(GameplayBeatmap.PlayableBeatmap, Mods.Value); // Don't re-import replay scores as they're already present in the database. protected override Task ImportScore(Score score) => Task.CompletedTask; diff --git a/osu.Game/Screens/Play/SpectatorPlayer.cs b/osu.Game/Screens/Play/SpectatorPlayer.cs index a8125dfded..67471dff90 100644 --- a/osu.Game/Screens/Play/SpectatorPlayer.cs +++ b/osu.Game/Screens/Play/SpectatorPlayer.cs @@ -18,6 +18,9 @@ namespace osu.Game.Screens.Play { private readonly Score score; + [Resolved] + private SpectatorClient spectatorClient { get; set; } + protected override bool CheckModsAllowFailure() => false; // todo: better support starting mid-way through beatmap public SpectatorPlayer(Score score) @@ -25,13 +28,10 @@ namespace osu.Game.Screens.Play this.score = score; } - protected override ResultsScreen CreateResults(ScoreInfo score) - { - return new SpectatorResultsScreen(score); - } + protected override Score CreateScore() => score; - [Resolved] - private SpectatorClient spectatorClient { get; set; } + protected override ResultsScreen CreateResults(ScoreInfo score) + => new SpectatorResultsScreen(score); [BackgroundDependencyLoader] private void load() From e0eb0adb0af1aa7b30b875fa7aa8e5df756af96d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 2 Jun 2021 18:32:14 +0900 Subject: [PATCH 57/61] Remove unnecessary bind in `ReplayPlayer` --- osu.Game/Screens/Play/ReplayPlayer.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index e440e7d34e..f70c05c2ff 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.cs @@ -34,9 +34,6 @@ namespace osu.Game.Screens.Play protected override void PrepareReplay() { DrawableRuleset?.SetReplayScore(Score); - - // todo: move to base class along with Score? - ScoreProcessor.NewJudgement += result => ScoreProcessor.PopulateScore(Score.ScoreInfo); } protected override Score CreateScore() => createScore(GameplayBeatmap.PlayableBeatmap, Mods.Value); From cde8de154d10e5470dac1f974938e861d2b8db72 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 2 Jun 2021 19:11:09 +0900 Subject: [PATCH 58/61] Remove unused test property for now --- osu.Game/Tests/Visual/TestReplayPlayer.cs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/osu.Game/Tests/Visual/TestReplayPlayer.cs b/osu.Game/Tests/Visual/TestReplayPlayer.cs index ac47d186eb..da302d018d 100644 --- a/osu.Game/Tests/Visual/TestReplayPlayer.cs +++ b/osu.Game/Tests/Visual/TestReplayPlayer.cs @@ -3,9 +3,7 @@ using System.Collections.Generic; using System.Linq; -using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; @@ -38,8 +36,6 @@ namespace osu.Game.Tests.Visual public new bool PauseCooldownActive => base.PauseCooldownActive; - public readonly List Results = new List(); - /// /// Instantiate a replay player that renders an autoplay mod. /// @@ -65,11 +61,5 @@ namespace osu.Game.Tests.Visual { PauseOnFocusLost = pauseOnFocusLost; } - - [BackgroundDependencyLoader] - private void load() - { - ScoreProcessor.NewJudgement += r => Results.Add(r); - } } } From 66dd7b77055cfe297273148bf308372a025f17a8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 3 Jun 2021 14:37:38 +0900 Subject: [PATCH 59/61] Update test logic to allow gameplay to properly continue --- .../Visual/Navigation/TestSceneScreenNavigation.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index dd05ce9b7e..0308d74aa4 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -12,6 +12,7 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; using osu.Game.Overlays.Mods; using osu.Game.Overlays.Toolbar; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Screens.Play; using osu.Game.Screens.Ranking; @@ -95,11 +96,12 @@ namespace osu.Game.Tests.Visual.Navigation AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); - AddStep("set nofail", () => Game.SelectedMods.Value = new[] { new OsuModNoFail() }); + AddStep("set mods", () => Game.SelectedMods.Value = new Mod[] { new OsuModNoFail(), new OsuModDoubleTime { SpeedChange = { Value = 2 } } }); AddStep("press enter", () => InputManager.Key(Key.Enter)); AddUntilStep("wait for player", () => (player = Game.ScreenStack.CurrentScreen as Player) != null); - AddStep("seek to end", () => player.ChildrenOfType().First().Seek(beatmap().Track.Length)); + AddUntilStep("wait for track playing", () => beatmap().Track.IsRunning); + AddStep("seek to near end", () => player.ChildrenOfType().First().Seek(beatmap().Beatmap.HitObjects[^1].StartTime - 1000)); AddUntilStep("wait for pass", () => (results = Game.ScreenStack.CurrentScreen as ResultsScreen) != null && results.IsLoaded); AddStep("attempt to retry", () => results.ChildrenOfType().First().Action()); AddUntilStep("wait for player", () => Game.ScreenStack.CurrentScreen != player && Game.ScreenStack.CurrentScreen is Player); From 94701b77cb261963a66106c0b4270a59de6dfc24 Mon Sep 17 00:00:00 2001 From: Samuel Cattini-Schultz Date: Thu, 3 Jun 2021 15:44:28 +1000 Subject: [PATCH 60/61] Add TODO for variable clockrate support in catch difficulty calculator --- osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs index 7d61be7bb1..83db9216ed 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs @@ -39,6 +39,8 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills // In catch, rate adjustment mods do not only affect the timings of hitobjects, // but also the speed of the player's catcher, which has an impact on difficulty + // TODO: Support variable clockrates caused by mods such as ModTimeRamp + // (perhaps by using IApplicableToRate within the CatchDifficultyHitObject constructor to set a catcher speed for each object before processing) var rateAdjustMod = mods.FirstOrDefault(m => m is ModRateAdjust); catcherSpeedMultiplier = (rateAdjustMod as ModRateAdjust)?.SpeedChange.Value ?? 1; } From f51413ead9a0c1afba047526ff9fae183ff1c9db Mon Sep 17 00:00:00 2001 From: Samuel Cattini-Schultz Date: Thu, 3 Jun 2021 16:09:37 +1000 Subject: [PATCH 61/61] Refactor to pass clockrate in constructor rather than deriving from mods --- .../EmptyFreeformDifficultyCalculator.cs | 2 +- .../PippidonDifficultyCalculator.cs | 2 +- .../EmptyScrollingDifficultyCalculator.cs | 2 +- .../PippidonDifficultyCalculator.cs | 2 +- .../Difficulty/CatchDifficultyCalculator.cs | 4 ++-- osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs | 8 +++----- .../Difficulty/ManiaDifficultyCalculator.cs | 2 +- .../Difficulty/OsuDifficultyCalculator.cs | 2 +- .../Difficulty/TaikoDifficultyCalculator.cs | 2 +- .../NonVisual/DifficultyAdjustmentModCombinationsTest.cs | 2 +- osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs | 5 +++-- 11 files changed, 16 insertions(+), 17 deletions(-) diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/EmptyFreeformDifficultyCalculator.cs b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/EmptyFreeformDifficultyCalculator.cs index 59a68245a6..a80f1178b6 100644 --- a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/EmptyFreeformDifficultyCalculator.cs +++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/EmptyFreeformDifficultyCalculator.cs @@ -25,6 +25,6 @@ namespace osu.Game.Rulesets.EmptyFreeform protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty(); - protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => new Skill[0]; + protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => new Skill[0]; } } diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/PippidonDifficultyCalculator.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/PippidonDifficultyCalculator.cs index f6340f6c25..290148d14b 100644 --- a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/PippidonDifficultyCalculator.cs +++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/PippidonDifficultyCalculator.cs @@ -25,6 +25,6 @@ namespace osu.Game.Rulesets.Pippidon protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty(); - protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => new Skill[0]; + protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => new Skill[0]; } } diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/EmptyScrollingDifficultyCalculator.cs b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/EmptyScrollingDifficultyCalculator.cs index 7f29c4e712..f557a4c754 100644 --- a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/EmptyScrollingDifficultyCalculator.cs +++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/EmptyScrollingDifficultyCalculator.cs @@ -25,6 +25,6 @@ namespace osu.Game.Rulesets.EmptyScrolling protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty(); - protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => new Skill[0]; + protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => new Skill[0]; } } diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/PippidonDifficultyCalculator.cs b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/PippidonDifficultyCalculator.cs index f6340f6c25..290148d14b 100644 --- a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/PippidonDifficultyCalculator.cs +++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/PippidonDifficultyCalculator.cs @@ -25,6 +25,6 @@ namespace osu.Game.Rulesets.Pippidon protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty(); - protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => new Skill[0]; + protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => new Skill[0]; } } diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs index f5cce47186..9feaa55051 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs @@ -67,7 +67,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty } } - protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) + protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) { halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.BeatmapInfo.BaseDifficulty) * 0.5f; @@ -76,7 +76,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty return new Skill[] { - new Movement(mods, halfCatcherWidth), + new Movement(mods, halfCatcherWidth, clockRate), }; } diff --git a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs index 849af75228..4372ed938c 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Linq; using osu.Game.Rulesets.Catch.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; @@ -34,17 +33,16 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills /// private readonly double catcherSpeedMultiplier; - public Movement(Mod[] mods, float halfCatcherWidth) + public Movement(Mod[] mods, float halfCatcherWidth, double clockRate) : base(mods) { HalfCatcherWidth = halfCatcherWidth; - // In catch, rate adjustment mods do not only affect the timings of hitobjects, + // In catch, clockrate adjustments do not only affect the timings of hitobjects, // but also the speed of the player's catcher, which has an impact on difficulty // TODO: Support variable clockrates caused by mods such as ModTimeRamp // (perhaps by using IApplicableToRate within the CatchDifficultyHitObject constructor to set a catcher speed for each object before processing) - var rateAdjustMod = mods.FirstOrDefault(m => m is ModRateAdjust); - catcherSpeedMultiplier = (rateAdjustMod as ModRateAdjust)?.SpeedChange.Value ?? 1; + catcherSpeedMultiplier = clockRate; } protected override double StrainValueOf(DifficultyHitObject current) diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs index 8c0b9ed8b7..a7a6677b68 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs @@ -68,7 +68,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty // Sorting is done in CreateDifficultyHitObjects, since the full list of hitobjects is required. protected override IEnumerable SortObjects(IEnumerable input) => input; - protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => new Skill[] + protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => new Skill[] { new Strain(mods, ((ManiaBeatmap)beatmap).TotalColumns) }; diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index 75d6786d95..e47f82fb39 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -79,7 +79,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty } } - protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => new Skill[] + protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => new Skill[] { new Aim(mods), new Speed(mods) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 6b3e31c5d5..18d06c069f 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty { } - protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => new Skill[] + protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => new Skill[] { new Colour(mods), new Rhythm(mods), diff --git a/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs b/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs index 16c1004f37..e458e66ab7 100644 --- a/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs +++ b/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs @@ -217,7 +217,7 @@ namespace osu.Game.Tests.NonVisual throw new NotImplementedException(); } - protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) + protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) { throw new NotImplementedException(); } diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs index 5780fe39fa..3cc69bd85b 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs @@ -59,7 +59,7 @@ namespace osu.Game.Rulesets.Difficulty private DifficultyAttributes calculate(IBeatmap beatmap, Mod[] mods, double clockRate) { - var skills = CreateSkills(beatmap, mods); + var skills = CreateSkills(beatmap, mods, clockRate); if (!beatmap.HitObjects.Any()) return CreateDifficultyAttributes(beatmap, mods, skills, clockRate); @@ -180,7 +180,8 @@ namespace osu.Game.Rulesets.Difficulty /// /// The whose difficulty will be calculated. /// Mods to calculate difficulty with. + /// Clockrate to calculate difficulty with. /// The s. - protected abstract Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods); + protected abstract Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate); } }