From 00250972c34caba4c5f1ab8dbdd29fa0ba80c697 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Thu, 6 Jul 2023 02:31:12 -0400 Subject: [PATCH 001/119] skip frames after a negative frame until the negative time is "paid back" --- osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs index c6461840aa..f91c96efb6 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs @@ -269,6 +269,13 @@ namespace osu.Game.Scoring.Legacy float lastTime = beatmapOffset; ReplayFrame currentFrame = null; + // the negative time amount that must be "paid back" by positive frames before we start including frames again. + // When a negative frame occurs in a replay, all future frames are skipped until the sum total of their times + // is equal to or greater than the time of that negative frame. + // This value will be negative if we are in a time deficit, ie we have a negative frame that must be paid back. + // Otherwise it will be 0. + float timeDeficit = 0; + string[] frames = reader.ReadToEnd().Split(','); for (int i = 0; i < frames.Length; i++) @@ -296,9 +303,13 @@ namespace osu.Game.Scoring.Legacy // ignore these frames as they serve no real purpose (and can even mislead ruleset-specific handlers - see mania) continue; + timeDeficit += diff; + timeDeficit = Math.Min(0, timeDeficit); + + // still paying back the deficit from a negative frame. Skip this frame. // Todo: At some point we probably want to rewind and play back the negative-time frames // but for now we'll achieve equal playback to stable by skipping negative frames - if (diff < 0) + if (timeDeficit < 0) continue; currentFrame = convertFrame(new LegacyReplayFrame(lastTime, From cc6646c82b13e021514a0465b118383d8e96ba7f Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Thu, 6 Jul 2023 17:13:33 -0400 Subject: [PATCH 002/119] properly handle negative frame before a break this was causing replay data before the skip to be...skipped. --- osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs index f91c96efb6..63465652e8 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs @@ -267,6 +267,7 @@ namespace osu.Game.Scoring.Legacy private void readLegacyReplay(Replay replay, StreamReader reader) { float lastTime = beatmapOffset; + bool skipFramesPresent = false; ReplayFrame currentFrame = null; // the negative time amount that must be "paid back" by positive frames before we start including frames again. @@ -298,18 +299,31 @@ namespace osu.Game.Scoring.Legacy lastTime += diff; if (i < 2 && mouseX == 256 && mouseY == -500) + { // at the start of the replay, stable places two replay frames, at time 0 and SkipBoundary - 1, respectively. // both frames use a position of (256, -500). // ignore these frames as they serve no real purpose (and can even mislead ruleset-specific handlers - see mania) + skipFramesPresent = true; continue; + } - timeDeficit += diff; - timeDeficit = Math.Min(0, timeDeficit); + // if the skip frames inserted by stable are present, the third frame will have a large negative time + // roughly equal to SkipBoundary. We don't want this to count towards the deficit: doing so would cause + // the replay data before the skip to be, well, skipped. + // In other words, this frame, if present, is a different kind of negative frame. It sets the "offset" + // for the beginning of the replay. This is the only negative frame to be handled in such a way. + bool isNegativeBreakFrame = i == 2 && skipFramesPresent && diff < 0; + + if (!isNegativeBreakFrame) + { + timeDeficit += diff; + timeDeficit = Math.Min(0, timeDeficit); + } // still paying back the deficit from a negative frame. Skip this frame. // Todo: At some point we probably want to rewind and play back the negative-time frames // but for now we'll achieve equal playback to stable by skipping negative frames - if (timeDeficit < 0) + if (timeDeficit < 0 || isNegativeBreakFrame) continue; currentFrame = convertFrame(new LegacyReplayFrame(lastTime, From 217b07810fb497f571fadfeb4d015394a43075d0 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Thu, 27 Jul 2023 02:12:21 -0400 Subject: [PATCH 003/119] don't skip the negative break frame investigation reveals this frame is played back by stable --- osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs index 63465652e8..8b1b24ce95 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs @@ -1,4 +1,4 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. #nullable disable @@ -323,7 +323,7 @@ namespace osu.Game.Scoring.Legacy // still paying back the deficit from a negative frame. Skip this frame. // Todo: At some point we probably want to rewind and play back the negative-time frames // but for now we'll achieve equal playback to stable by skipping negative frames - if (timeDeficit < 0 || isNegativeBreakFrame) + if (timeDeficit < 0) continue; currentFrame = convertFrame(new LegacyReplayFrame(lastTime, From a93561cab05807be032e963b37092ff349f12fc1 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Thu, 27 Jul 2023 02:12:43 -0400 Subject: [PATCH 004/119] remove resolved comment --- osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs index 8b1b24ce95..79224b7d4f 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs @@ -321,8 +321,6 @@ namespace osu.Game.Scoring.Legacy } // still paying back the deficit from a negative frame. Skip this frame. - // Todo: At some point we probably want to rewind and play back the negative-time frames - // but for now we'll achieve equal playback to stable by skipping negative frames if (timeDeficit < 0) continue; From 61760f614a9900e5cd71a298a8979ca69d9b8eb0 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Thu, 27 Jul 2023 16:34:18 -0400 Subject: [PATCH 005/119] fix legacy score decode tests for negative frame --- .../Beatmaps/Formats/LegacyScoreDecoderTest.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs index 93cda34ef7..89b6d76e54 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs @@ -92,17 +92,20 @@ namespace osu.Game.Tests.Beatmaps.Formats [TestCase(LegacyBeatmapDecoder.LATEST_VERSION, false)] public void TestLegacyBeatmapReplayOffsetsDecode(int beatmapVersion, bool offsetApplied) { - const double first_frame_time = 48; - const double second_frame_time = 65; + const double first_frame_time = 31; + const double second_frame_time = 48; + const double third_frame_time = 65; var decoder = new TestLegacyScoreDecoder(beatmapVersion); using (var resourceStream = TestResources.OpenResource("Replays/mania-replay.osr")) { var score = decoder.Parse(resourceStream); + int offset = offsetApplied ? LegacyBeatmapDecoder.EARLY_VERSION_TIMING_OFFSET : 0; - Assert.That(score.Replay.Frames[0].Time, Is.EqualTo(first_frame_time + (offsetApplied ? LegacyBeatmapDecoder.EARLY_VERSION_TIMING_OFFSET : 0))); - Assert.That(score.Replay.Frames[1].Time, Is.EqualTo(second_frame_time + (offsetApplied ? LegacyBeatmapDecoder.EARLY_VERSION_TIMING_OFFSET : 0))); + Assert.That(score.Replay.Frames[0].Time, Is.EqualTo(first_frame_time + offset)); + Assert.That(score.Replay.Frames[1].Time, Is.EqualTo(second_frame_time + offset)); + Assert.That(score.Replay.Frames[2].Time, Is.EqualTo(third_frame_time + offset)); } } From 7d174dd8bb4998ac264463067b798fae14541f0c Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Thu, 27 Jul 2023 17:20:54 -0400 Subject: [PATCH 006/119] dont count any of first three frames towards time deficit --- osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs index 79224b7d4f..eceaada399 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs @@ -267,7 +267,6 @@ namespace osu.Game.Scoring.Legacy private void readLegacyReplay(Replay replay, StreamReader reader) { float lastTime = beatmapOffset; - bool skipFramesPresent = false; ReplayFrame currentFrame = null; // the negative time amount that must be "paid back" by positive frames before we start including frames again. @@ -299,22 +298,20 @@ namespace osu.Game.Scoring.Legacy lastTime += diff; if (i < 2 && mouseX == 256 && mouseY == -500) - { // at the start of the replay, stable places two replay frames, at time 0 and SkipBoundary - 1, respectively. // both frames use a position of (256, -500). // ignore these frames as they serve no real purpose (and can even mislead ruleset-specific handlers - see mania) - skipFramesPresent = true; continue; - } - // if the skip frames inserted by stable are present, the third frame will have a large negative time - // roughly equal to SkipBoundary. We don't want this to count towards the deficit: doing so would cause - // the replay data before the skip to be, well, skipped. - // In other words, this frame, if present, is a different kind of negative frame. It sets the "offset" - // for the beginning of the replay. This is the only negative frame to be handled in such a way. - bool isNegativeBreakFrame = i == 2 && skipFramesPresent && diff < 0; - - if (!isNegativeBreakFrame) + // negative frames are only counted towards the deficit after the very beginning of the replay. + // When the two skip frames are present (see directly above), the third frame will have a large + // negative time roughly equal to SkipBoundary. This shouldn't be counted towards the deficit, otherwise + // any replay data before the skip would be, well, skipped. + // + // On testing against stable it appears that stable ignores the negative time of *any* of the first + // three frames, regardless of if the skip frames are present. Hence the condition here. + // But this may be incorrect and need to be revisited later. + if (i > 2) { timeDeficit += diff; timeDeficit = Math.Min(0, timeDeficit); From 04ef04b9026dfe84c323e3d08204f5a722fb5d74 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Thu, 27 Jul 2023 21:12:08 -0400 Subject: [PATCH 007/119] only ignore the first negative frame among the first 3 replay frames --- osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs index eceaada399..fdeda24c75 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs @@ -267,6 +267,7 @@ namespace osu.Game.Scoring.Legacy private void readLegacyReplay(Replay replay, StreamReader reader) { float lastTime = beatmapOffset; + bool negativeFrameEncounted = false; ReplayFrame currentFrame = null; // the negative time amount that must be "paid back" by positive frames before we start including frames again. @@ -308,15 +309,19 @@ namespace osu.Game.Scoring.Legacy // negative time roughly equal to SkipBoundary. This shouldn't be counted towards the deficit, otherwise // any replay data before the skip would be, well, skipped. // - // On testing against stable it appears that stable ignores the negative time of *any* of the first - // three frames, regardless of if the skip frames are present. Hence the condition here. - // But this may be incorrect and need to be revisited later. - if (i > 2) + // On testing against stable, it appears that stable ignores the negative time of only the first + // negative frame of the first three replay frames, regardless of if the skip frames are present. + // Hence the condition here. + // But there is a possibility this is incorrect and may need to be revisited later. + if (i > 2 || negativeFrameEncounted) { timeDeficit += diff; timeDeficit = Math.Min(0, timeDeficit); } + if (diff < 0) + negativeFrameEncounted = true; + // still paying back the deficit from a negative frame. Skip this frame. if (timeDeficit < 0) continue; From 5f7028b5741f88e5c68e05cbd878ecc110d32eec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 21 Mar 2024 17:45:56 +0100 Subject: [PATCH 008/119] Add failing tests --- .../Formats/LegacyScoreDecoderTest.cs | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs index 5dae86d9e9..050259c2fa 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs @@ -31,6 +31,7 @@ using osu.Game.Rulesets.Taiko; using osu.Game.Scoring; using osu.Game.Scoring.Legacy; using osu.Game.Tests.Resources; +using osuTK; namespace osu.Game.Tests.Beatmaps.Formats { @@ -178,6 +179,94 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.That(decodedAfterEncode.Replay.Frames[1].Time, Is.EqualTo(second_frame_time)); } + [Test] + public void TestNegativeFrameSkipped() + { + var ruleset = new OsuRuleset().RulesetInfo; + var scoreInfo = TestResources.CreateTestScoreInfo(ruleset); + var beatmap = new TestBeatmap(ruleset); + + var score = new Score + { + ScoreInfo = scoreInfo, + Replay = new Replay + { + Frames = new List + { + new OsuReplayFrame(0, new Vector2()), + new OsuReplayFrame(1000, OsuPlayfield.BASE_SIZE), + new OsuReplayFrame(500, OsuPlayfield.BASE_SIZE / 2), + new OsuReplayFrame(2000, OsuPlayfield.BASE_SIZE), + } + } + }; + + var decodedAfterEncode = encodeThenDecode(LegacyScoreEncoder.LATEST_VERSION, score, beatmap); + + Assert.That(decodedAfterEncode.Replay.Frames, Has.Count.EqualTo(3)); + Assert.That(decodedAfterEncode.Replay.Frames[0].Time, Is.EqualTo(0)); + Assert.That(decodedAfterEncode.Replay.Frames[1].Time, Is.EqualTo(1000)); + Assert.That(decodedAfterEncode.Replay.Frames[2].Time, Is.EqualTo(2000)); + } + + [Test] + public void FirstTwoFramesSwappedIfInWrongOrder() + { + var ruleset = new OsuRuleset().RulesetInfo; + var scoreInfo = TestResources.CreateTestScoreInfo(ruleset); + var beatmap = new TestBeatmap(ruleset); + + var score = new Score + { + ScoreInfo = scoreInfo, + Replay = new Replay + { + Frames = new List + { + new OsuReplayFrame(100, new Vector2()), + new OsuReplayFrame(50, OsuPlayfield.BASE_SIZE / 2), + new OsuReplayFrame(1000, OsuPlayfield.BASE_SIZE), + } + } + }; + + var decodedAfterEncode = encodeThenDecode(LegacyScoreEncoder.LATEST_VERSION, score, beatmap); + + Assert.That(decodedAfterEncode.Replay.Frames, Has.Count.EqualTo(3)); + Assert.That(decodedAfterEncode.Replay.Frames[0].Time, Is.EqualTo(0)); + Assert.That(decodedAfterEncode.Replay.Frames[1].Time, Is.EqualTo(100)); + Assert.That(decodedAfterEncode.Replay.Frames[2].Time, Is.EqualTo(1000)); + } + + [Test] + public void FirstTwoFramesPulledTowardThirdIfTheyAreAfterIt() + { + var ruleset = new OsuRuleset().RulesetInfo; + var scoreInfo = TestResources.CreateTestScoreInfo(ruleset); + var beatmap = new TestBeatmap(ruleset); + + var score = new Score + { + ScoreInfo = scoreInfo, + Replay = new Replay + { + Frames = new List + { + new OsuReplayFrame(0, new Vector2()), + new OsuReplayFrame(500, OsuPlayfield.BASE_SIZE / 2), + new OsuReplayFrame(-1500, OsuPlayfield.BASE_SIZE), + } + } + }; + + var decodedAfterEncode = encodeThenDecode(LegacyScoreEncoder.LATEST_VERSION, score, beatmap); + + Assert.That(decodedAfterEncode.Replay.Frames, Has.Count.EqualTo(3)); + Assert.That(decodedAfterEncode.Replay.Frames[0].Time, Is.EqualTo(-1500)); + Assert.That(decodedAfterEncode.Replay.Frames[1].Time, Is.EqualTo(-1500)); + Assert.That(decodedAfterEncode.Replay.Frames[2].Time, Is.EqualTo(-1500)); + } + [Test] public void TestCultureInvariance() { From 990a07af0eb7070b2e92ed37c033bf183721e299 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 21 Mar 2024 21:01:51 +0100 Subject: [PATCH 009/119] Rewrite handling of legacy replay frame quirks to match stable closer --- osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs | 77 +++++++++---------- 1 file changed, 37 insertions(+), 40 deletions(-) diff --git a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs index b16cdffe82..af514a4b59 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs @@ -21,6 +21,7 @@ using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Scoring; +using osuTK; using SharpCompress.Compressors.LZMA; namespace osu.Game.Scoring.Legacy @@ -240,15 +241,7 @@ namespace osu.Game.Scoring.Legacy private void readLegacyReplay(Replay replay, StreamReader reader) { float lastTime = beatmapOffset; - bool negativeFrameEncounted = false; - ReplayFrame currentFrame = null; - - // the negative time amount that must be "paid back" by positive frames before we start including frames again. - // When a negative frame occurs in a replay, all future frames are skipped until the sum total of their times - // is equal to or greater than the time of that negative frame. - // This value will be negative if we are in a time deficit, ie we have a negative frame that must be paid back. - // Otherwise it will be 0. - float timeDeficit = 0; + var legacyFrames = new List(); string[] frames = reader.ReadToEnd().Split(','); @@ -271,40 +264,44 @@ namespace osu.Game.Scoring.Legacy lastTime += diff; - if (i < 2 && mouseX == 256 && mouseY == -500) - // at the start of the replay, stable places two replay frames, at time 0 and SkipBoundary - 1, respectively. - // both frames use a position of (256, -500). - // ignore these frames as they serve no real purpose (and can even mislead ruleset-specific handlers - see mania) - continue; - - // negative frames are only counted towards the deficit after the very beginning of the replay. - // When the two skip frames are present (see directly above), the third frame will have a large - // negative time roughly equal to SkipBoundary. This shouldn't be counted towards the deficit, otherwise - // any replay data before the skip would be, well, skipped. - // - // On testing against stable, it appears that stable ignores the negative time of only the first - // negative frame of the first three replay frames, regardless of if the skip frames are present. - // Hence the condition here. - // But there is a possibility this is incorrect and may need to be revisited later. - if (i > 2 || negativeFrameEncounted) - { - timeDeficit += diff; - timeDeficit = Math.Min(0, timeDeficit); - } - - if (diff < 0) - negativeFrameEncounted = true; - - // still paying back the deficit from a negative frame. Skip this frame. - if (timeDeficit < 0) - continue; - - currentFrame = convertFrame(new LegacyReplayFrame(lastTime, + legacyFrames.Add(new LegacyReplayFrame(lastTime, mouseX, mouseY, - (ReplayButtonState)Parsing.ParseInt(split[3])), currentFrame); + (ReplayButtonState)Parsing.ParseInt(split[3]))); + } - replay.Frames.Add(currentFrame); + // https://github.com/peppy/osu-stable-reference/blob/e53980dd76857ee899f66ce519ba1597e7874f28/osu!/GameModes/Play/ReplayWatcher.cs#L62-L67 + if (legacyFrames.Count >= 2 && legacyFrames[1].Time < legacyFrames[0].Time) + { + legacyFrames[1].Time = legacyFrames[0].Time; + legacyFrames[0].Time = 0; + } + + // https://github.com/peppy/osu-stable-reference/blob/e53980dd76857ee899f66ce519ba1597e7874f28/osu!/GameModes/Play/ReplayWatcher.cs#L69-L71 + if (legacyFrames.Count >= 3 && legacyFrames[0].Time > legacyFrames[2].Time) + legacyFrames[0].Time = legacyFrames[1].Time = legacyFrames[2].Time; + + // at the start of the replay, stable places two replay frames, at time 0 and SkipBoundary - 1, respectively. + // both frames use a position of (256, -500). + // ignore these frames as they serve no real purpose (and can even mislead ruleset-specific handlers - see mania) + if (legacyFrames.Count >= 2 && legacyFrames[1].Position == new Vector2(256, -500)) + legacyFrames.RemoveAt(1); + + if (legacyFrames.Count >= 1 && legacyFrames[0].Position == new Vector2(256, -500)) + legacyFrames.RemoveAt(0); + + ReplayFrame currentFrame = null; + + foreach (var legacyFrame in legacyFrames) + { + // never allow backwards time traversal in relation to the current frame. + // this handles frames with negative delta. + // this doesn't match stable 100% as stable will do something similar to adding an interpolated "intermediate frame" + // at the point wherein time flow changes from backwards to forwards, but it'll do for now. + if (currentFrame != null && legacyFrame.Time < currentFrame.Time) + continue; + + replay.Frames.Add(currentFrame = convertFrame(legacyFrame, currentFrame)); } } From beee76d64ab5b7604787603a2c9c8f559910d604 Mon Sep 17 00:00:00 2001 From: DavidBeh <67109172+DavidBeh@users.noreply.github.com> Date: Mon, 22 Apr 2024 20:25:43 +0200 Subject: [PATCH 010/119] enabled and fixed judgements for the magnetised mod --- osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs | 13 ++++++++++--- osu.Game.Rulesets.Osu/Objects/Slider.cs | 9 +++++++++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs b/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs index b49fb931d1..860f96965a 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs @@ -13,7 +13,6 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; -using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.UI; using osuTK; @@ -37,12 +36,16 @@ namespace osu.Game.Rulesets.Osu.Mods MaxValue = 1.0f, }; + // Bindable Setting for Show Judgements + [SettingSource("Show Judgements", "Whether to show judgements or not.")] + public BindableBool ShowJudgements { get; } = new BindableBool(true); + public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) { // Hide judgment displays and follow points as they won't make any sense. // Judgements can potentially be turned on in a future where they display at a position relative to their drawable counterpart. - drawableRuleset.Playfield.DisplayJudgements.Value = false; - (drawableRuleset.Playfield as OsuPlayfield)?.FollowPoints.Hide(); + drawableRuleset.Playfield.DisplayJudgements.Value = ShowJudgements.Value; + //(drawableRuleset.Playfield as OsuPlayfield)?.FollowPoints.Hide(); } public void Update(Playfield playfield) @@ -78,6 +81,10 @@ namespace osu.Game.Rulesets.Osu.Mods float x = (float)Interpolation.DampContinuously(hitObject.X, destination.X, dampLength, clock.ElapsedFrameTime); float y = (float)Interpolation.DampContinuously(hitObject.Y, destination.Y, dampLength, clock.ElapsedFrameTime); + // I added these two lines + if (hitObject is DrawableOsuHitObject h) + h.HitObject.Position = new Vector2(x, y); + hitObject.Position = new Vector2(x, y); } } diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index cc3ffd376e..2660933a70 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -8,6 +8,7 @@ using osu.Game.Rulesets.Objects.Types; using System.Collections.Generic; using osu.Game.Rulesets.Objects; using System.Linq; +using System.Runtime.CompilerServices; using System.Threading; using Newtonsoft.Json; using osu.Framework.Bindables; @@ -24,6 +25,8 @@ namespace osu.Game.Rulesets.Osu.Objects { public class Slider : OsuHitObject, IHasPathWithRepeats, IHasSliderVelocity, IHasGenerateTicks { + private static readonly ConditionalWeakTable> SliderProgress = new ConditionalWeakTable>(); + public double EndTime => StartTime + this.SpanCount() * Path.Distance / Velocity; [JsonIgnore] @@ -201,6 +204,7 @@ namespace osu.Game.Rulesets.Osu.Objects Position = Position + Path.PositionAt(e.PathProgress), StackHeight = StackHeight, }); + SliderProgress.AddOrUpdate(NestedHitObjects.Last(), new StrongBox(e.PathProgress)); break; case SliderEventType.Head: @@ -232,6 +236,7 @@ namespace osu.Game.Rulesets.Osu.Objects Position = Position + Path.PositionAt(e.PathProgress), StackHeight = StackHeight, }); + SliderProgress.Add(NestedHitObjects.Last(), new StrongBox(e.PathProgress)); break; } } @@ -248,6 +253,10 @@ namespace osu.Game.Rulesets.Osu.Objects if (TailCircle != null) TailCircle.Position = EndPosition; + + foreach (var hitObject in NestedHitObjects) + if (hitObject is SliderTick or SliderRepeat) + ((OsuHitObject)hitObject).Position = Position + Path.PositionAt(SliderProgress.TryGetValue(hitObject, out var progress) ? progress?.Value ?? 0 : 0); } protected void UpdateNestedSamples() From 331f1f31b08c508d4d51008a402df2b9d6b46238 Mon Sep 17 00:00:00 2001 From: DavidBeh <67109172+DavidBeh@users.noreply.github.com> Date: Tue, 23 Apr 2024 19:19:11 +0200 Subject: [PATCH 011/119] Attempt to position DrawableOsuJudgement based on its DrawableOsuHitObject instead of DrawableOsuHitObject.HitObject --- osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs | 4 ++-- .../Objects/Drawables/DrawableOsuJudgement.cs | 9 ++++++--- .../Objects/Drawables/DrawableSliderTick.cs | 2 +- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs b/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs index 860f96965a..f0ad284019 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs @@ -82,9 +82,9 @@ namespace osu.Game.Rulesets.Osu.Mods float y = (float)Interpolation.DampContinuously(hitObject.Y, destination.Y, dampLength, clock.ElapsedFrameTime); // I added these two lines - if (hitObject is DrawableOsuHitObject h) + /*if (hitObject is DrawableOsuHitObject h) h.HitObject.Position = new Vector2(x, y); - +*/ hitObject.Position = new Vector2(x, y); } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs index 76ae7340ff..0960748320 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs @@ -39,10 +39,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Lighting.ResetAnimation(); Lighting.SetColourFrom(JudgedObject, Result); - if (JudgedObject?.HitObject is OsuHitObject osuObject) + if (JudgedObject is DrawableOsuHitObject osuObject) { - Position = osuObject.StackedEndPosition; - Scale = new Vector2(osuObject.Scale); + Position = osuObject.ToSpaceOfOtherDrawable(Vector2.Zero, Parent); + // Works only for normal hit circles, also with magnetised: + // Position = osuObject.Position; + + Scale = new Vector2(osuObject.HitObject.Scale); } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs index 73c061afbd..0c7ba180f2 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public const float DEFAULT_TICK_SIZE = 16; - protected DrawableSlider DrawableSlider => (DrawableSlider)ParentHitObject; + public DrawableSlider DrawableSlider => (DrawableSlider)ParentHitObject; private SkinnableDrawable scaleContainer; From 3a914b9337ce15cff315162431fef5769903ba8e Mon Sep 17 00:00:00 2001 From: DavidBeh <67109172+DavidBeh@users.noreply.github.com> Date: Tue, 23 Apr 2024 23:24:51 +0200 Subject: [PATCH 012/119] Fixed judgements with MG mod without causing side effects --- osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs | 4 ---- .../Objects/Drawables/DrawableOsuJudgement.cs | 5 +---- osu.Game.Rulesets.Osu/Objects/Slider.cs | 8 +------- 3 files changed, 2 insertions(+), 15 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs b/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs index f0ad284019..c64b5a18bc 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs @@ -81,10 +81,6 @@ namespace osu.Game.Rulesets.Osu.Mods float x = (float)Interpolation.DampContinuously(hitObject.X, destination.X, dampLength, clock.ElapsedFrameTime); float y = (float)Interpolation.DampContinuously(hitObject.Y, destination.Y, dampLength, clock.ElapsedFrameTime); - // I added these two lines - /*if (hitObject is DrawableOsuHitObject h) - h.HitObject.Position = new Vector2(x, y); -*/ hitObject.Position = new Vector2(x, y); } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs index 0960748320..d0270c68f6 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs @@ -41,10 +41,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (JudgedObject is DrawableOsuHitObject osuObject) { - Position = osuObject.ToSpaceOfOtherDrawable(Vector2.Zero, Parent); - // Works only for normal hit circles, also with magnetised: - // Position = osuObject.Position; - + Position = osuObject.ToSpaceOfOtherDrawable(osuObject.OriginPosition, Parent!); Scale = new Vector2(osuObject.HitObject.Scale); } } diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 2660933a70..5b52996e22 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -8,7 +8,6 @@ using osu.Game.Rulesets.Objects.Types; using System.Collections.Generic; using osu.Game.Rulesets.Objects; using System.Linq; -using System.Runtime.CompilerServices; using System.Threading; using Newtonsoft.Json; using osu.Framework.Bindables; @@ -25,7 +24,6 @@ namespace osu.Game.Rulesets.Osu.Objects { public class Slider : OsuHitObject, IHasPathWithRepeats, IHasSliderVelocity, IHasGenerateTicks { - private static readonly ConditionalWeakTable> SliderProgress = new ConditionalWeakTable>(); public double EndTime => StartTime + this.SpanCount() * Path.Distance / Velocity; @@ -204,7 +202,6 @@ namespace osu.Game.Rulesets.Osu.Objects Position = Position + Path.PositionAt(e.PathProgress), StackHeight = StackHeight, }); - SliderProgress.AddOrUpdate(NestedHitObjects.Last(), new StrongBox(e.PathProgress)); break; case SliderEventType.Head: @@ -236,7 +233,6 @@ namespace osu.Game.Rulesets.Osu.Objects Position = Position + Path.PositionAt(e.PathProgress), StackHeight = StackHeight, }); - SliderProgress.Add(NestedHitObjects.Last(), new StrongBox(e.PathProgress)); break; } } @@ -254,9 +250,7 @@ namespace osu.Game.Rulesets.Osu.Objects if (TailCircle != null) TailCircle.Position = EndPosition; - foreach (var hitObject in NestedHitObjects) - if (hitObject is SliderTick or SliderRepeat) - ((OsuHitObject)hitObject).Position = Position + Path.PositionAt(SliderProgress.TryGetValue(hitObject, out var progress) ? progress?.Value ?? 0 : 0); + // Positions of other nested hitobjects are not updated } protected void UpdateNestedSamples() From f863ea30e10fce9dce2d388a0e8b0f7f7532c9f1 Mon Sep 17 00:00:00 2001 From: DavidBeh <67109172+DavidBeh@users.noreply.github.com> Date: Tue, 23 Apr 2024 23:31:23 +0200 Subject: [PATCH 013/119] Made judgements always on and disabled follow paths again --- osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs b/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs index c64b5a18bc..97ec669703 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs @@ -13,6 +13,7 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; +using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.UI; using osuTK; @@ -36,16 +37,11 @@ namespace osu.Game.Rulesets.Osu.Mods MaxValue = 1.0f, }; - // Bindable Setting for Show Judgements - [SettingSource("Show Judgements", "Whether to show judgements or not.")] - public BindableBool ShowJudgements { get; } = new BindableBool(true); - public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) { // Hide judgment displays and follow points as they won't make any sense. // Judgements can potentially be turned on in a future where they display at a position relative to their drawable counterpart. - drawableRuleset.Playfield.DisplayJudgements.Value = ShowJudgements.Value; - //(drawableRuleset.Playfield as OsuPlayfield)?.FollowPoints.Hide(); + (drawableRuleset.Playfield as OsuPlayfield)?.FollowPoints.Hide(); } public void Update(Playfield playfield) From 7dac5afd90bf3fa7194e46c20c9b10e3fe7450e9 Mon Sep 17 00:00:00 2001 From: DavidBeh <67109172+DavidBeh@users.noreply.github.com> Date: Tue, 23 Apr 2024 23:57:27 +0200 Subject: [PATCH 014/119] Enabled judgements for repel (but not in depth). Updated comments in repel, mag, depth --- osu.Game.Rulesets.Osu/Mods/OsuModDepth.cs | 6 ++++-- osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs | 2 +- osu.Game.Rulesets.Osu/Mods/OsuModRepel.cs | 3 +-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDepth.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDepth.cs index a9111eec1f..6d4a621f4d 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModDepth.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModDepth.cs @@ -47,10 +47,12 @@ namespace osu.Game.Rulesets.Osu.Mods public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) { - // Hide judgment displays and follow points as they won't make any sense. + // Hide follow points as they won't make any sense. // Judgements can potentially be turned on in a future where they display at a position relative to their drawable counterpart. - drawableRuleset.Playfield.DisplayJudgements.Value = false; (drawableRuleset.Playfield as OsuPlayfield)?.FollowPoints.Hide(); + // Hide judgements as they don't move with the drawables after appearing, which does look bad. + // They would need to either move with them or disappear sooner. + drawableRuleset.Playfield.DisplayJudgements.Value = false; } private void applyTransform(DrawableHitObject drawable, ArmedState state) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs b/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs index 97ec669703..b2553e295c 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs @@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Osu.Mods public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) { - // Hide judgment displays and follow points as they won't make any sense. + // Hide follow points as they won't make any sense. // Judgements can potentially be turned on in a future where they display at a position relative to their drawable counterpart. (drawableRuleset.Playfield as OsuPlayfield)?.FollowPoints.Hide(); } diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRepel.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRepel.cs index ced98f0cd5..302e17432e 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRepel.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRepel.cs @@ -38,9 +38,8 @@ namespace osu.Game.Rulesets.Osu.Mods public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) { - // Hide judgment displays and follow points as they won't make any sense. + // Hide follow points as they won't make any sense. // Judgements can potentially be turned on in a future where they display at a position relative to their drawable counterpart. - drawableRuleset.Playfield.DisplayJudgements.Value = false; (drawableRuleset.Playfield as OsuPlayfield)?.FollowPoints.Hide(); } From 16190a4ed7efe012de82aa9cf2b25925556e4a3c Mon Sep 17 00:00:00 2001 From: DavidBeh <67109172+DavidBeh@users.noreply.github.com> Date: Wed, 24 Apr 2024 00:23:45 +0200 Subject: [PATCH 015/119] Made judgements follow DrawableOsuHitObjects. Enabled judgements for depth mod --- osu.Game.Rulesets.Osu/Mods/OsuModDepth.cs | 3 --- .../Objects/Drawables/DrawableOsuJudgement.cs | 11 +++++++++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDepth.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDepth.cs index 6d4a621f4d..306dcee839 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModDepth.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModDepth.cs @@ -50,9 +50,6 @@ namespace osu.Game.Rulesets.Osu.Mods // Hide follow points as they won't make any sense. // Judgements can potentially be turned on in a future where they display at a position relative to their drawable counterpart. (drawableRuleset.Playfield as OsuPlayfield)?.FollowPoints.Hide(); - // Hide judgements as they don't move with the drawables after appearing, which does look bad. - // They would need to either move with them or disappear sooner. - drawableRuleset.Playfield.DisplayJudgements.Value = false; } private void applyTransform(DrawableHitObject drawable, ArmedState state) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs index d0270c68f6..64bf25cceb 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs @@ -46,6 +46,17 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables } } + + protected override void Update() + { + base.Update(); + if (JudgedObject is DrawableOsuHitObject osuObject && Parent != null && osuObject.HitObject != null) + { + Position = osuObject.ToSpaceOfOtherDrawable(osuObject.OriginPosition, Parent!); + Scale = new Vector2(osuObject.HitObject.Scale); + } + } + protected override void ApplyHitAnimations() { bool hitLightingEnabled = config.Get(OsuSetting.HitLighting); From 8c2a4eb78ab0b900f552f91641ba7b526d33443f Mon Sep 17 00:00:00 2001 From: DavidBeh <67109172+DavidBeh@users.noreply.github.com> Date: Wed, 24 Apr 2024 12:40:23 +0200 Subject: [PATCH 016/119] Fix formatting --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs | 2 +- osu.Game.Rulesets.Osu/Objects/Slider.cs | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs index 64bf25cceb..ffbf45291f 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs @@ -46,10 +46,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables } } - protected override void Update() { base.Update(); + if (JudgedObject is DrawableOsuHitObject osuObject && Parent != null && osuObject.HitObject != null) { Position = osuObject.ToSpaceOfOtherDrawable(osuObject.OriginPosition, Parent!); diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 5b52996e22..248f40208a 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -24,7 +24,6 @@ namespace osu.Game.Rulesets.Osu.Objects { public class Slider : OsuHitObject, IHasPathWithRepeats, IHasSliderVelocity, IHasGenerateTicks { - public double EndTime => StartTime + this.SpanCount() * Path.Distance / Velocity; [JsonIgnore] From f534c4aadab0a274674bd5b077e4acea797f177e Mon Sep 17 00:00:00 2001 From: Fabian van Oeffelt Date: Thu, 2 May 2024 18:42:35 +0200 Subject: [PATCH 017/119] Initial implementation --- .../Input/Bindings/GlobalActionContainer.cs | 8 ++ .../GlobalActionKeyBindingStrings.cs | 10 ++ osu.Game/Screens/Select/SongSelect.cs | 97 ++++++++++++++++++- 3 files changed, 113 insertions(+), 2 deletions(-) diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index 296232d9ea..ff7d9f0a6d 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -134,6 +134,8 @@ namespace osu.Game.Input.Bindings new KeyBinding(new[] { InputKey.Shift, InputKey.F2 }, GlobalAction.SelectPreviousRandom), new KeyBinding(InputKey.F3, GlobalAction.ToggleBeatmapOptions), new KeyBinding(InputKey.BackSpace, GlobalAction.DeselectAllMods), + new KeyBinding(InputKey.PageUp, GlobalAction.IncreaseSpeed), // Not working with minus and other keys.... + new KeyBinding(InputKey.PageDown, GlobalAction.DecreaseSpeed), }; public IEnumerable AudioControlKeyBindings => new[] @@ -364,5 +366,11 @@ namespace osu.Game.Input.Bindings [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorToggleRotateControl))] EditorToggleRotateControl, + + [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.IncreaseSpeed))] + IncreaseSpeed, + + [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.DecreaseSpeed))] + DecreaseSpeed, } } diff --git a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs index 8356c480dd..40fe4064ed 100644 --- a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs +++ b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs @@ -349,6 +349,16 @@ namespace osu.Game.Localisation /// public static LocalisableString EditorToggleRotateControl => new TranslatableString(getKey(@"editor_toggle_rotate_control"), @"Toggle rotate control"); + /// + /// "Increase Speed" + /// + public static LocalisableString IncreaseSpeed => new TranslatableString(getKey(@"increase_speed"), @"Increase Speed"); + + /// + /// "Decrease Speed" + /// + public static LocalisableString DecreaseSpeed => new TranslatableString(getKey(@"decrease_speed"), @"Decrease Speed"); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 34ee0ae4e8..f2036f017d 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -755,7 +755,95 @@ namespace osu.Game.Screens.Select return false; } - + public void IncreaseSpeed() + { + // find way of grabbing all types of ModSpeedAdjust + var rateAdjustStates = ModSelect.AllAvailableMods.Where(pair => pair.Mod.Acronym == "DT" || pair.Mod.Acronym == "NC" || pair.Mod.Acronym == "HT" || pair.Mod.Acronym == "DC"); + var stateDoubleTime = ModSelect.AllAvailableMods.First(pair => pair.Mod.Acronym == "DT"); + bool oneActive = false; + double newRate = 1.05d; + foreach (var state in rateAdjustStates) + { + ModRateAdjust mod = (ModRateAdjust)state.Mod; + if (state.Active.Value) + { + oneActive = true; + newRate = mod.SpeedChange.Value + 0.05d; + if (mod.Acronym == "DT" || mod.Acronym == "NC") + { + mod.SpeedChange.Value = newRate; + return; + } + else + { + if (newRate == 1.0d) + { + state.Active.Value = false; + } + if (newRate > 1d) + { + state.Active.Value = false; + stateDoubleTime.Active.Value = true; + ((ModDoubleTime)stateDoubleTime.Mod).SpeedChange.Value = newRate; + return; + } + if (newRate < 1d) + { + mod.SpeedChange.Value = newRate; + } + } + } + } + if (!oneActive) + { + stateDoubleTime.Active.Value = true; + ((ModDoubleTime)stateDoubleTime.Mod).SpeedChange.Value = newRate; + } + } + public void DecreaseSpeed() + { + var rateAdjustStates = ModSelect.AllAvailableMods.Where(pair => pair.Mod.Acronym == "DT" || pair.Mod.Acronym == "NC" || pair.Mod.Acronym == "HT" || pair.Mod.Acronym == "DC"); + var stateHalfTime = ModSelect.AllAvailableMods.First(pair => pair.Mod.Acronym == "HT"); + bool oneActive = false; + double newRate = 0.95d; + foreach (var state in rateAdjustStates) + { + ModRateAdjust mod = (ModRateAdjust)state.Mod; + if (state.Active.Value) + { + oneActive = true; + newRate = mod.SpeedChange.Value - 0.05d; + if (mod.Acronym == "HT" || mod.Acronym == "DC") + { + mod.SpeedChange.Value = newRate; + return; + } + else + { + if (newRate == 1.0d) + { + state.Active.Value = false; + } + if (newRate < 1d) + { + state.Active.Value = false; + stateHalfTime.Active.Value = true; + ((ModHalfTime)stateHalfTime.Mod).SpeedChange.Value = newRate; + return; + } + if (newRate > 1d) + { + mod.SpeedChange.Value = newRate; + } + } + } + } + if (!oneActive) + { + stateHalfTime.Active.Value = true; + ((ModHalfTime)stateHalfTime.Mod).SpeedChange.Value = newRate; + } + } protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); @@ -955,12 +1043,17 @@ namespace osu.Game.Screens.Select return false; if (!this.IsCurrentScreen()) return false; - switch (e.Action) { case GlobalAction.Select: FinaliseSelection(); return true; + case GlobalAction.IncreaseSpeed: + IncreaseSpeed(); + return true; + case GlobalAction.DecreaseSpeed: + DecreaseSpeed(); + return true; } return false; From 5c21a0330addcf81db97e7ec157510d894cbd6a0 Mon Sep 17 00:00:00 2001 From: Fabian van Oeffelt Date: Thu, 2 May 2024 19:05:07 +0200 Subject: [PATCH 018/119] F1 also does not work with minus in song select, same behaviour --- osu.Game/Input/Bindings/GlobalActionContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index ff7d9f0a6d..204ecb9061 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -134,7 +134,7 @@ namespace osu.Game.Input.Bindings new KeyBinding(new[] { InputKey.Shift, InputKey.F2 }, GlobalAction.SelectPreviousRandom), new KeyBinding(InputKey.F3, GlobalAction.ToggleBeatmapOptions), new KeyBinding(InputKey.BackSpace, GlobalAction.DeselectAllMods), - new KeyBinding(InputKey.PageUp, GlobalAction.IncreaseSpeed), // Not working with minus and other keys.... + new KeyBinding(InputKey.PageUp, GlobalAction.IncreaseSpeed), new KeyBinding(InputKey.PageDown, GlobalAction.DecreaseSpeed), }; From 7527ddbc6865c35e26a7c4deef850e0d4e93d786 Mon Sep 17 00:00:00 2001 From: Fabian van Oeffelt Date: Thu, 2 May 2024 19:05:43 +0200 Subject: [PATCH 019/119] Comment, make code more readable, functions are now private --- osu.Game/Screens/Select/SongSelect.cs | 133 ++++++++++++-------------- 1 file changed, 60 insertions(+), 73 deletions(-) diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index f2036f017d..5de4a7a9a3 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -755,93 +755,80 @@ namespace osu.Game.Screens.Select return false; } - public void IncreaseSpeed() + private void increaseSpeed() { - // find way of grabbing all types of ModSpeedAdjust - var rateAdjustStates = ModSelect.AllAvailableMods.Where(pair => pair.Mod.Acronym == "DT" || pair.Mod.Acronym == "NC" || pair.Mod.Acronym == "HT" || pair.Mod.Acronym == "DC"); - var stateDoubleTime = ModSelect.AllAvailableMods.First(pair => pair.Mod.Acronym == "DT"); - bool oneActive = false; - double newRate = 1.05d; - foreach (var state in rateAdjustStates) - { - ModRateAdjust mod = (ModRateAdjust)state.Mod; - if (state.Active.Value) - { - oneActive = true; - newRate = mod.SpeedChange.Value + 0.05d; - if (mod.Acronym == "DT" || mod.Acronym == "NC") - { - mod.SpeedChange.Value = newRate; - return; - } - else - { - if (newRate == 1.0d) - { - state.Active.Value = false; - } - if (newRate > 1d) - { - state.Active.Value = false; - stateDoubleTime.Active.Value = true; - ((ModDoubleTime)stateDoubleTime.Mod).SpeedChange.Value = newRate; - return; - } - if (newRate < 1d) - { - mod.SpeedChange.Value = newRate; - } - } - } - } - if (!oneActive) + var rateAdjustStates = ModSelect.AllAvailableMods.Where(pair => pair.Mod is ModRateAdjust); + var stateDoubleTime = ModSelect.AllAvailableMods.First(pair => pair.Mod is ModDoubleTime); + bool rateModActive = ModSelect.AllAvailableMods.Where(pair => pair.Mod is ModRateAdjust && pair.Active.Value).Count() > 0; + double stepSize = 0.05d; + double newRate = 1d + stepSize; + // If no mod rateAdjust mod is currently active activate DoubleTime with speed newRate + if (!rateModActive) { stateDoubleTime.Active.Value = true; ((ModDoubleTime)stateDoubleTime.Mod).SpeedChange.Value = newRate; + return; } - } - public void DecreaseSpeed() - { - var rateAdjustStates = ModSelect.AllAvailableMods.Where(pair => pair.Mod.Acronym == "DT" || pair.Mod.Acronym == "NC" || pair.Mod.Acronym == "HT" || pair.Mod.Acronym == "DC"); - var stateHalfTime = ModSelect.AllAvailableMods.First(pair => pair.Mod.Acronym == "HT"); - bool oneActive = false; - double newRate = 0.95d; + // Find current active rateAdjust mod and modify speed, enable DoubleTime if necessary foreach (var state in rateAdjustStates) { ModRateAdjust mod = (ModRateAdjust)state.Mod; - if (state.Active.Value) + if (!state.Active.Value) continue; + newRate = mod.SpeedChange.Value + stepSize; + if (mod.Acronym == "DT" || mod.Acronym == "NC") + mod.SpeedChange.Value = newRate; + else { - oneActive = true; - newRate = mod.SpeedChange.Value - 0.05d; - if (mod.Acronym == "HT" || mod.Acronym == "DC") + if (newRate == 1.0d) + state.Active.Value = false; + if (newRate > 1d) { + state.Active.Value = false; + stateDoubleTime.Active.Value = true; + ((ModDoubleTime)stateDoubleTime.Mod).SpeedChange.Value = newRate; + break; + } + if (newRate < 1d) mod.SpeedChange.Value = newRate; - return; - } - else - { - if (newRate == 1.0d) - { - state.Active.Value = false; - } - if (newRate < 1d) - { - state.Active.Value = false; - stateHalfTime.Active.Value = true; - ((ModHalfTime)stateHalfTime.Mod).SpeedChange.Value = newRate; - return; - } - if (newRate > 1d) - { - mod.SpeedChange.Value = newRate; - } - } } } - if (!oneActive) + } + private void decreaseSpeed() + { + var rateAdjustStates = ModSelect.AllAvailableMods.Where(pair => pair.Mod is ModRateAdjust); + var stateHalfTime = ModSelect.AllAvailableMods.First(pair => pair.Mod is ModHalfTime); + bool rateModActive = ModSelect.AllAvailableMods.Where(pair => pair.Mod is ModRateAdjust && pair.Active.Value).Count() > 0; + double stepSize = 0.05d; + double newRate = 1d - stepSize; + // If no mod rateAdjust mod is currently active activate HalfTime with speed newRate + if (!rateModActive) { stateHalfTime.Active.Value = true; ((ModHalfTime)stateHalfTime.Mod).SpeedChange.Value = newRate; + return; + } + // Find current active rateAdjust mod and modify speed, enable HalfTime if necessary + foreach (var state in rateAdjustStates) + { + ModRateAdjust mod = (ModRateAdjust)state.Mod; + if (!state.Active.Value) continue; + newRate = mod.SpeedChange.Value - stepSize; + if (mod.Acronym == "HT" || mod.Acronym == "DC") + mod.SpeedChange.Value = newRate; + else + { + if (newRate == 1.0d) + state.Active.Value = false; + if (newRate < 1d) + { + state.Active.Value = false; + stateHalfTime.Active.Value = true; + ((ModHalfTime)stateHalfTime.Mod).SpeedChange.Value = newRate; + break; + } + if (newRate > 1d) + mod.SpeedChange.Value = newRate; + } } } protected override void Dispose(bool isDisposing) @@ -1049,10 +1036,10 @@ namespace osu.Game.Screens.Select FinaliseSelection(); return true; case GlobalAction.IncreaseSpeed: - IncreaseSpeed(); + increaseSpeed(); return true; case GlobalAction.DecreaseSpeed: - DecreaseSpeed(); + decreaseSpeed(); return true; } From 588badf29216782699ae359700d6449e82638187 Mon Sep 17 00:00:00 2001 From: Fabian van Oeffelt Date: Thu, 2 May 2024 19:22:39 +0200 Subject: [PATCH 020/119] Fix Formatting --- osu.Game/Input/Bindings/GlobalActionContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index f6bc002e32..5dacb6db4d 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -416,7 +416,7 @@ namespace osu.Game.Input.Bindings [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.DecreaseSpeed))] DecreaseSpeed, - + [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.IncreaseOffset))] IncreaseOffset, From 4b5ea6bd0bc46cf37612efa0de3c81f3d4fe52bc Mon Sep 17 00:00:00 2001 From: Fabian van Oeffelt Date: Thu, 2 May 2024 19:41:00 +0200 Subject: [PATCH 021/119] Fix Code Inspection --- osu.Game/Screens/Select/SongSelect.cs | 36 +++++++++++++++++++++------ 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 73ce029d3f..de0f24aa90 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -808,13 +808,15 @@ namespace osu.Game.Screens.Select return false; } + private void increaseSpeed() { var rateAdjustStates = ModSelect.AllAvailableMods.Where(pair => pair.Mod is ModRateAdjust); var stateDoubleTime = ModSelect.AllAvailableMods.First(pair => pair.Mod is ModDoubleTime); - bool rateModActive = ModSelect.AllAvailableMods.Where(pair => pair.Mod is ModRateAdjust && pair.Active.Value).Count() > 0; - double stepSize = 0.05d; - double newRate = 1d + stepSize; + bool rateModActive = ModSelect.AllAvailableMods.Count(pair => pair.Mod is ModRateAdjust && pair.Active.Value) > 0; + const double stepsize = 0.05d; + double newRate = 1d + stepsize; + // If no mod rateAdjust mod is currently active activate DoubleTime with speed newRate if (!rateModActive) { @@ -822,18 +824,23 @@ namespace osu.Game.Screens.Select ((ModDoubleTime)stateDoubleTime.Mod).SpeedChange.Value = newRate; return; } + // Find current active rateAdjust mod and modify speed, enable DoubleTime if necessary foreach (var state in rateAdjustStates) { ModRateAdjust mod = (ModRateAdjust)state.Mod; + if (!state.Active.Value) continue; - newRate = mod.SpeedChange.Value + stepSize; + + newRate = mod.SpeedChange.Value + stepsize; + if (mod.Acronym == "DT" || mod.Acronym == "NC") mod.SpeedChange.Value = newRate; else { if (newRate == 1.0d) state.Active.Value = false; + if (newRate > 1d) { state.Active.Value = false; @@ -841,18 +848,21 @@ namespace osu.Game.Screens.Select ((ModDoubleTime)stateDoubleTime.Mod).SpeedChange.Value = newRate; break; } + if (newRate < 1d) mod.SpeedChange.Value = newRate; } } } + private void decreaseSpeed() { var rateAdjustStates = ModSelect.AllAvailableMods.Where(pair => pair.Mod is ModRateAdjust); var stateHalfTime = ModSelect.AllAvailableMods.First(pair => pair.Mod is ModHalfTime); - bool rateModActive = ModSelect.AllAvailableMods.Where(pair => pair.Mod is ModRateAdjust && pair.Active.Value).Count() > 0; - double stepSize = 0.05d; - double newRate = 1d - stepSize; + bool rateModActive = ModSelect.AllAvailableMods.Count(pair => pair.Mod is ModRateAdjust && pair.Active.Value) > 0; + const double stepsize = 0.05d; + double newRate = 1d - stepsize; + // If no mod rateAdjust mod is currently active activate HalfTime with speed newRate if (!rateModActive) { @@ -860,18 +870,23 @@ namespace osu.Game.Screens.Select ((ModHalfTime)stateHalfTime.Mod).SpeedChange.Value = newRate; return; } + // Find current active rateAdjust mod and modify speed, enable HalfTime if necessary foreach (var state in rateAdjustStates) { ModRateAdjust mod = (ModRateAdjust)state.Mod; + if (!state.Active.Value) continue; - newRate = mod.SpeedChange.Value - stepSize; + + newRate = mod.SpeedChange.Value - stepsize; + if (mod.Acronym == "HT" || mod.Acronym == "DC") mod.SpeedChange.Value = newRate; else { if (newRate == 1.0d) state.Active.Value = false; + if (newRate < 1d) { state.Active.Value = false; @@ -879,11 +894,13 @@ namespace osu.Game.Screens.Select ((ModHalfTime)stateHalfTime.Mod).SpeedChange.Value = newRate; break; } + if (newRate > 1d) mod.SpeedChange.Value = newRate; } } } + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); @@ -1086,14 +1103,17 @@ namespace osu.Game.Screens.Select return false; if (!this.IsCurrentScreen()) return false; + switch (e.Action) { case GlobalAction.Select: FinaliseSelection(); return true; + case GlobalAction.IncreaseSpeed: increaseSpeed(); return true; + case GlobalAction.DecreaseSpeed: decreaseSpeed(); return true; From e3afd89dc879d6b21c41abc2df749729d314fedc Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 16 May 2024 04:49:33 +0300 Subject: [PATCH 022/119] Allow specifying height of `ShearedButton`s Also includes a test case in `TestSceneShearedButton`s to ensure the buttons' shear factors don't change on different heights (I've encountered issues with that previously). --- .../UserInterface/TestSceneShearedButtons.cs | 51 +++++++++++++++++-- .../Graphics/UserInterface/ShearedButton.cs | 9 ++-- .../Mods/ModFooterInformationDisplay.cs | 2 +- 3 files changed, 53 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneShearedButtons.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneShearedButtons.cs index 118d32ee70..8db22f2d65 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneShearedButtons.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneShearedButtons.cs @@ -7,11 +7,13 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; +using osuTK; using osuTK.Input; namespace osu.Game.Tests.Visual.UserInterface @@ -35,7 +37,7 @@ namespace osu.Game.Tests.Visual.UserInterface if (bigButton) { - Child = button = new ShearedButton(400) + Child = button = new ShearedButton(400, 80) { LighterColour = Colour4.FromHex("#FFFFFF"), DarkerColour = Colour4.FromHex("#FFCC22"), @@ -44,13 +46,12 @@ namespace osu.Game.Tests.Visual.UserInterface Anchor = Anchor.Centre, Origin = Anchor.Centre, Text = "Let's GO!", - Height = 80, Action = () => actionFired = true, }; } else { - Child = button = new ShearedButton(200) + Child = button = new ShearedButton(200, 80) { LighterColour = Colour4.FromHex("#FF86DD"), DarkerColour = Colour4.FromHex("#DE31AE"), @@ -58,7 +59,6 @@ namespace osu.Game.Tests.Visual.UserInterface Anchor = Anchor.Centre, Origin = Anchor.Centre, Text = "Press me", - Height = 80, Action = () => actionFired = true, }; } @@ -171,5 +171,48 @@ namespace osu.Game.Tests.Visual.UserInterface void setToggleDisabledState(bool disabled) => AddStep($"{(disabled ? "disable" : "enable")} toggle", () => button.Active.Disabled = disabled); } + + [Test] + public void TestButtons() + { + AddStep("create buttons", () => Children = new[] + { + new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Direction = FillDirection.Horizontal, + AutoSizeAxes = Axes.Both, + Scale = new Vector2(2.5f), + Children = new Drawable[] + { + new ShearedButton(120) + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Text = "Test", + Action = () => { }, + Padding = new MarginPadding(), + }, + new ShearedButton(120, 40) + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Text = "Test", + Action = () => { }, + Padding = new MarginPadding { Left = -1f }, + }, + new ShearedButton(120, 70) + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Text = "Test", + Action = () => { }, + Padding = new MarginPadding { Left = 3f }, + }, + } + } + }); + } } } diff --git a/osu.Game/Graphics/UserInterface/ShearedButton.cs b/osu.Game/Graphics/UserInterface/ShearedButton.cs index b1e7066a01..caf1f76d88 100644 --- a/osu.Game/Graphics/UserInterface/ShearedButton.cs +++ b/osu.Game/Graphics/UserInterface/ShearedButton.cs @@ -17,7 +17,7 @@ namespace osu.Game.Graphics.UserInterface { public partial class ShearedButton : OsuClickableContainer { - public const float HEIGHT = 50; + public const float DEFAULT_HEIGHT = 50; public const float CORNER_RADIUS = 7; public const float BORDER_THICKNESS = 2; @@ -85,10 +85,11 @@ namespace osu.Game.Graphics.UserInterface /// If a value is provided (or the argument is omitted entirely), the button will autosize in width to fit the text. /// /// - public ShearedButton(float? width = null) + /// The height of the button. + public ShearedButton(float? width = null, float height = DEFAULT_HEIGHT) { - Height = HEIGHT; - Padding = new MarginPadding { Horizontal = shear * 50 }; + Height = height; + Padding = new MarginPadding { Horizontal = shear * height }; Content.CornerRadius = CORNER_RADIUS; Content.Shear = new Vector2(shear, 0); diff --git a/osu.Game/Overlays/Mods/ModFooterInformationDisplay.cs b/osu.Game/Overlays/Mods/ModFooterInformationDisplay.cs index 7fccf0cc13..8668879850 100644 --- a/osu.Game/Overlays/Mods/ModFooterInformationDisplay.cs +++ b/osu.Game/Overlays/Mods/ModFooterInformationDisplay.cs @@ -36,7 +36,7 @@ namespace osu.Game.Overlays.Mods Origin = Anchor.BottomRight, Anchor = Anchor.BottomRight, AutoSizeAxes = Axes.X, - Height = ShearedButton.HEIGHT, + Height = ShearedButton.DEFAULT_HEIGHT, Shear = new Vector2(ShearedOverlayContainer.SHEAR, 0), CornerRadius = ShearedButton.CORNER_RADIUS, BorderThickness = ShearedButton.BORDER_THICKNESS, From 266f0803624415acbf6bde6eb3a9b63f7ea9b83c Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 16 May 2024 05:01:49 +0300 Subject: [PATCH 023/119] Allow customising content of `ShearedButton`s --- .../Graphics/UserInterface/ShearedButton.cs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/ShearedButton.cs b/osu.Game/Graphics/UserInterface/ShearedButton.cs index caf1f76d88..0fd21502a1 100644 --- a/osu.Game/Graphics/UserInterface/ShearedButton.cs +++ b/osu.Game/Graphics/UserInterface/ShearedButton.cs @@ -75,6 +75,8 @@ namespace osu.Game.Graphics.UserInterface private readonly Container backgroundLayer; private readonly Box flashLayer; + protected readonly Container ButtonContent; + /// /// Creates a new /// @@ -110,12 +112,16 @@ namespace osu.Game.Graphics.UserInterface { RelativeSizeAxes = Axes.Both }, - text = new OsuSpriteText + ButtonContent = new Container { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Font = OsuFont.TorusAlternate.With(size: 17), - Shear = new Vector2(-shear, 0) + AutoSizeAxes = Axes.Both, + Shear = new Vector2(-shear, 0), + Child = text = new OsuSpriteText + { + Font = OsuFont.TorusAlternate.With(size: 17), + } }, } }, @@ -189,7 +195,7 @@ namespace osu.Game.Graphics.UserInterface { var colourDark = darkerColour ?? ColourProvider.Background3; var colourLight = lighterColour ?? ColourProvider.Background1; - var colourText = textColour ?? ColourProvider.Content1; + var colourContent = textColour ?? ColourProvider.Content1; if (!Enabled.Value) { @@ -206,9 +212,9 @@ namespace osu.Game.Graphics.UserInterface backgroundLayer.TransformTo(nameof(BorderColour), ColourInfo.GradientVertical(colourDark, colourLight), 150, Easing.OutQuint); if (!Enabled.Value) - colourText = colourText.Opacity(0.6f); + colourContent = colourContent.Opacity(0.6f); - text.FadeColour(colourText, 150, Easing.OutQuint); + ButtonContent.FadeColour(colourContent, 150, Easing.OutQuint); } } } From 7e8d5a5b0a1d0c58aab8de8afd6dc56094fd4f00 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 16 May 2024 04:50:51 +0300 Subject: [PATCH 024/119] Add new footer back button --- osu.Game/Screens/Footer/ScreenBackButton.cs | 64 +++++++++++++++++++++ osu.Game/Screens/Footer/ScreenFooter.cs | 17 ++++-- 2 files changed, 75 insertions(+), 6 deletions(-) create mode 100644 osu.Game/Screens/Footer/ScreenBackButton.cs diff --git a/osu.Game/Screens/Footer/ScreenBackButton.cs b/osu.Game/Screens/Footer/ScreenBackButton.cs new file mode 100644 index 0000000000..c5e613ea51 --- /dev/null +++ b/osu.Game/Screens/Footer/ScreenBackButton.cs @@ -0,0 +1,64 @@ +// 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.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Localisation; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.Footer +{ + public partial class ScreenBackButton : ShearedButton + { + // todo: see https://github.com/ppy/osu-framework/issues/3271 + private const float torus_scale_factor = 1.2f; + + public const float BUTTON_WIDTH = 240; + + public ScreenBackButton() + : base(BUTTON_WIDTH, 70) + { + } + + [BackgroundDependencyLoader] + private void load() + { + ButtonContent.Child = new FillFlowContainer + { + X = -10f, + RelativeSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(20f, 0f), + Children = new Drawable[] + { + new SpriteIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(20f), + Icon = FontAwesome.Solid.ChevronLeft, + }, + new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Font = OsuFont.TorusAlternate.With(size: 20 * torus_scale_factor), + Text = CommonStrings.Back, + UseFullGlyphHeight = false, + } + } + }; + + DarkerColour = Color4Extensions.FromHex("#DE31AE"); + LighterColour = Color4Extensions.FromHex("#FF86DD"); + TextColour = Color4.White; + } + } +} diff --git a/osu.Game/Screens/Footer/ScreenFooter.cs b/osu.Game/Screens/Footer/ScreenFooter.cs index 01013bb466..4a10a4cdab 100644 --- a/osu.Game/Screens/Footer/ScreenFooter.cs +++ b/osu.Game/Screens/Footer/ScreenFooter.cs @@ -7,7 +7,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; -using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; using osuTK; @@ -15,9 +14,8 @@ namespace osu.Game.Screens.Footer { public partial class ScreenFooter : InputBlockingContainer { - //Should be 60, setting to 50 for now for the sake of matching the current BackButton height. - private const int height = 50; - private const int padding = 80; + private const int height = 60; + private const int padding = 60; private readonly List overlays = new List(); @@ -68,13 +66,20 @@ namespace osu.Game.Screens.Footer }, buttons = new FillFlowContainer { - Position = new Vector2(TwoLayerButton.SIZE_EXTENDED.X + padding, 10), + Position = new Vector2(ScreenBackButton.BUTTON_WIDTH + padding, 10), Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, Direction = FillDirection.Horizontal, Spacing = new Vector2(7, 0), AutoSizeAxes = Axes.Both - } + }, + new ScreenBackButton + { + Margin = new MarginPadding { Bottom = 10f, Left = 12f }, + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Action = () => { }, + }, }; } } From 9446f45acf7da4bea4f042dc68a36e2c7d42db53 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 16 May 2024 05:03:34 +0300 Subject: [PATCH 025/119] Fix shear factor of `ScreenFooterButton`s not matching anything else When shear factors differ between components that are close to each other (`BackButtonV2` and `ScreenFooterButton` in this case), they look completely ugly. --- osu.Game/Screens/Footer/ScreenFooterButton.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Footer/ScreenFooterButton.cs b/osu.Game/Screens/Footer/ScreenFooterButton.cs index b3b3c9a8ec..3a23249ae0 100644 --- a/osu.Game/Screens/Footer/ScreenFooterButton.cs +++ b/osu.Game/Screens/Footer/ScreenFooterButton.cs @@ -25,8 +25,9 @@ namespace osu.Game.Screens.Footer { public partial class ScreenFooterButton : OsuClickableContainer, IKeyBindingHandler { - // This should be 12 by design, but an extra allowance is added due to the corner radius specification. - private const float shear_width = 13.5f; + // if we go by design, both this and ShearedButton should have shear factor as 1/7, + // but ShearedButton uses 1/5 so we must follow suit for the design to stay consistent. + private const float shear = 1f / 5f; protected const int CORNER_RADIUS = 10; protected const int BUTTON_HEIGHT = 90; @@ -34,7 +35,7 @@ namespace osu.Game.Screens.Footer public Bindable OverlayState = new Bindable(); - protected static readonly Vector2 BUTTON_SHEAR = new Vector2(shear_width / BUTTON_HEIGHT, 0); + protected static readonly Vector2 BUTTON_SHEAR = new Vector2(shear, 0); [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; From 7fce4cc4948803047de66133b07060a9991ff426 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 16 May 2024 05:30:19 +0300 Subject: [PATCH 026/119] Make `ScreenFooterButtonMods` share shear factor with base class --- .../SelectV2/Footer/ScreenFooterButtonMods.cs | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/SelectV2/Footer/ScreenFooterButtonMods.cs b/osu.Game/Screens/SelectV2/Footer/ScreenFooterButtonMods.cs index 49961c60f8..f590a19164 100644 --- a/osu.Game/Screens/SelectV2/Footer/ScreenFooterButtonMods.cs +++ b/osu.Game/Screens/SelectV2/Footer/ScreenFooterButtonMods.cs @@ -33,12 +33,9 @@ namespace osu.Game.Screens.SelectV2.Footer { // todo: see https://github.com/ppy/osu-framework/issues/3271 private const float torus_scale_factor = 1.2f; - private const float bar_shear_width = 7f; private const float bar_height = 37f; private const float mod_display_portion = 0.65f; - private static readonly Vector2 bar_shear = new Vector2(bar_shear_width / bar_height, 0); - private readonly BindableWithCurrent> current = new BindableWithCurrent>(Array.Empty()); public Bindable> Current @@ -77,7 +74,7 @@ namespace osu.Game.Screens.SelectV2.Footer Y = -5f, Depth = float.MaxValue, Origin = Anchor.BottomLeft, - Shear = bar_shear, + Shear = BUTTON_SHEAR, CornerRadius = CORNER_RADIUS, Size = new Vector2(BUTTON_WIDTH, bar_height), Masking = true, @@ -107,7 +104,7 @@ namespace osu.Game.Screens.SelectV2.Footer { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Shear = -bar_shear, + Shear = -BUTTON_SHEAR, UseFullGlyphHeight = false, Font = OsuFont.Torus.With(size: 14 * torus_scale_factor, weight: FontWeight.Bold) } @@ -129,7 +126,7 @@ namespace osu.Game.Screens.SelectV2.Footer { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Shear = -bar_shear, + Shear = -BUTTON_SHEAR, Scale = new Vector2(0.6f), Current = { BindTarget = Current }, ExpansionMode = ExpansionMode.AlwaysContracted, @@ -138,7 +135,7 @@ namespace osu.Game.Screens.SelectV2.Footer { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Shear = -bar_shear, + Shear = -BUTTON_SHEAR, Font = OsuFont.Torus.With(size: 14 * torus_scale_factor, weight: FontWeight.Bold), Mods = { BindTarget = Current }, } @@ -304,7 +301,7 @@ namespace osu.Game.Screens.SelectV2.Footer Y = -5f; Depth = float.MaxValue; Origin = Anchor.BottomLeft; - Shear = bar_shear; + Shear = BUTTON_SHEAR; CornerRadius = CORNER_RADIUS; AutoSizeAxes = Axes.X; Height = bar_height; @@ -328,7 +325,7 @@ namespace osu.Game.Screens.SelectV2.Footer { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Shear = -bar_shear, + Shear = -BUTTON_SHEAR, Text = ModSelectOverlayStrings.Unranked.ToUpper(), Margin = new MarginPadding { Horizontal = 15 }, UseFullGlyphHeight = false, From 9265290acfea0c3d746ede43426ae479708e340b Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 16 May 2024 05:30:35 +0300 Subject: [PATCH 027/119] Change shear factor everywhere to 0.15x --- osu.Game/Graphics/UserInterface/ShearedButton.cs | 3 ++- osu.Game/Overlays/Mods/ShearedOverlayContainer.cs | 3 ++- osu.Game/Screens/Footer/ScreenFooterButton.cs | 5 ++--- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/ShearedButton.cs b/osu.Game/Graphics/UserInterface/ShearedButton.cs index 0fd21502a1..d546c18cb8 100644 --- a/osu.Game/Graphics/UserInterface/ShearedButton.cs +++ b/osu.Game/Graphics/UserInterface/ShearedButton.cs @@ -11,6 +11,7 @@ using osu.Framework.Localisation; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Overlays; +using osu.Game.Overlays.Mods; using osuTK; namespace osu.Game.Graphics.UserInterface @@ -66,7 +67,7 @@ namespace osu.Game.Graphics.UserInterface private readonly Box background; private readonly OsuSpriteText text; - private const float shear = 0.2f; + private const float shear = ShearedOverlayContainer.SHEAR; private Colour4? darkerColour; private Colour4? lighterColour; diff --git a/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs b/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs index a372ec70db..893ac89aa4 100644 --- a/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs +++ b/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs @@ -22,7 +22,8 @@ namespace osu.Game.Overlays.Mods { protected const float PADDING = 14; - public const float SHEAR = 0.2f; + // todo: maybe move this to a higher place since it's used for screen footer buttons etc. + public const float SHEAR = 0.15f; [Cached] protected readonly OverlayColourProvider ColourProvider; diff --git a/osu.Game/Screens/Footer/ScreenFooterButton.cs b/osu.Game/Screens/Footer/ScreenFooterButton.cs index 3a23249ae0..e0b019bfa8 100644 --- a/osu.Game/Screens/Footer/ScreenFooterButton.cs +++ b/osu.Game/Screens/Footer/ScreenFooterButton.cs @@ -18,6 +18,7 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Input.Bindings; using osu.Game.Overlays; +using osu.Game.Overlays.Mods; using osuTK; using osuTK.Graphics; @@ -25,9 +26,7 @@ namespace osu.Game.Screens.Footer { public partial class ScreenFooterButton : OsuClickableContainer, IKeyBindingHandler { - // if we go by design, both this and ShearedButton should have shear factor as 1/7, - // but ShearedButton uses 1/5 so we must follow suit for the design to stay consistent. - private const float shear = 1f / 5f; + private const float shear = ShearedOverlayContainer.SHEAR; protected const int CORNER_RADIUS = 10; protected const int BUTTON_HEIGHT = 90; From 310b4d90cc49fc8f26982bc0656285458d713d6c Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 16 May 2024 07:27:54 +0300 Subject: [PATCH 028/119] Move `SHEAR` constant to `OsuGame` and revert back to 0.2x (i.e. master) Discussed in [discord](https://discord.com/channels/188630481301012481/188630652340404224/1240490608934653984). --- osu.Game/Graphics/UserInterface/ShearedButton.cs | 2 +- osu.Game/Graphics/UserInterface/ShearedSearchTextBox.cs | 4 ++-- osu.Game/OsuGame.cs | 5 +++++ osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs | 2 +- osu.Game/Overlays/Mods/ModColumn.cs | 2 +- osu.Game/Overlays/Mods/ModFooterInformationDisplay.cs | 2 +- osu.Game/Overlays/Mods/ModPanel.cs | 2 +- osu.Game/Overlays/Mods/ModSelectColumn.cs | 6 +++--- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 4 ++-- osu.Game/Overlays/Mods/ModSelectPanel.cs | 8 ++++---- osu.Game/Overlays/Mods/RankingInformationDisplay.cs | 6 +++--- osu.Game/Overlays/Mods/ShearedOverlayContainer.cs | 3 --- osu.Game/Screens/Footer/ScreenFooterButton.cs | 2 +- 13 files changed, 25 insertions(+), 23 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/ShearedButton.cs b/osu.Game/Graphics/UserInterface/ShearedButton.cs index d546c18cb8..7eed388707 100644 --- a/osu.Game/Graphics/UserInterface/ShearedButton.cs +++ b/osu.Game/Graphics/UserInterface/ShearedButton.cs @@ -67,7 +67,7 @@ namespace osu.Game.Graphics.UserInterface private readonly Box background; private readonly OsuSpriteText text; - private const float shear = ShearedOverlayContainer.SHEAR; + private const float shear = OsuGame.SHEAR; private Colour4? darkerColour; private Colour4? lighterColour; diff --git a/osu.Game/Graphics/UserInterface/ShearedSearchTextBox.cs b/osu.Game/Graphics/UserInterface/ShearedSearchTextBox.cs index c3a9f8a586..5f3716f36c 100644 --- a/osu.Game/Graphics/UserInterface/ShearedSearchTextBox.cs +++ b/osu.Game/Graphics/UserInterface/ShearedSearchTextBox.cs @@ -53,7 +53,7 @@ namespace osu.Game.Graphics.UserInterface public ShearedSearchTextBox() { Height = 42; - Shear = new Vector2(ShearedOverlayContainer.SHEAR, 0); + Shear = new Vector2(OsuGame.SHEAR, 0); Masking = true; CornerRadius = corner_radius; @@ -116,7 +116,7 @@ namespace osu.Game.Graphics.UserInterface PlaceholderText = CommonStrings.InputSearch; CornerRadius = corner_radius; - TextContainer.Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0); + TextContainer.Shear = new Vector2(-OsuGame.SHEAR, 0); } protected override SpriteText CreatePlaceholder() => new SearchPlaceholder(); diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 7c89314014..af01a1b1ac 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -91,6 +91,11 @@ namespace osu.Game /// protected const float SIDE_OVERLAY_OFFSET_RATIO = 0.05f; + /// + /// A common shear factor applied to most components of the game. + /// + public const float SHEAR = 0.2f; + public Toolbar Toolbar { get; private set; } private ChatOverlay chatOverlay; diff --git a/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs b/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs index c58cf710bd..5b10a2844e 100644 --- a/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs +++ b/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs @@ -66,7 +66,7 @@ namespace osu.Game.Overlays.Mods [BackgroundDependencyLoader] private void load() { - const float shear = ShearedOverlayContainer.SHEAR; + const float shear = OsuGame.SHEAR; LeftContent.AddRange(new Drawable[] { diff --git a/osu.Game/Overlays/Mods/ModColumn.cs b/osu.Game/Overlays/Mods/ModColumn.cs index e9f21338bd..326394a207 100644 --- a/osu.Game/Overlays/Mods/ModColumn.cs +++ b/osu.Game/Overlays/Mods/ModColumn.cs @@ -106,7 +106,7 @@ namespace osu.Game.Overlays.Mods Origin = Anchor.CentreLeft, Scale = new Vector2(0.8f), RelativeSizeAxes = Axes.X, - Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0) + Shear = new Vector2(-OsuGame.SHEAR, 0) }); ItemsFlow.Padding = new MarginPadding { diff --git a/osu.Game/Overlays/Mods/ModFooterInformationDisplay.cs b/osu.Game/Overlays/Mods/ModFooterInformationDisplay.cs index 8668879850..6665a3b8dc 100644 --- a/osu.Game/Overlays/Mods/ModFooterInformationDisplay.cs +++ b/osu.Game/Overlays/Mods/ModFooterInformationDisplay.cs @@ -37,7 +37,7 @@ namespace osu.Game.Overlays.Mods Anchor = Anchor.BottomRight, AutoSizeAxes = Axes.X, Height = ShearedButton.DEFAULT_HEIGHT, - Shear = new Vector2(ShearedOverlayContainer.SHEAR, 0), + Shear = new Vector2(OsuGame.SHEAR, 0), CornerRadius = ShearedButton.CORNER_RADIUS, BorderThickness = ShearedButton.BORDER_THICKNESS, Masking = true, diff --git a/osu.Game/Overlays/Mods/ModPanel.cs b/osu.Game/Overlays/Mods/ModPanel.cs index cf173b0d6a..9f87a704c0 100644 --- a/osu.Game/Overlays/Mods/ModPanel.cs +++ b/osu.Game/Overlays/Mods/ModPanel.cs @@ -36,7 +36,7 @@ namespace osu.Game.Overlays.Mods Anchor = Anchor.Centre, Origin = Anchor.Centre, Active = { BindTarget = Active }, - Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0), + Shear = new Vector2(-OsuGame.SHEAR, 0), Scale = new Vector2(HEIGHT / ModSwitchSmall.DEFAULT_SIZE) }; } diff --git a/osu.Game/Overlays/Mods/ModSelectColumn.cs b/osu.Game/Overlays/Mods/ModSelectColumn.cs index 61b29ef65b..5ffed24e7a 100644 --- a/osu.Game/Overlays/Mods/ModSelectColumn.cs +++ b/osu.Game/Overlays/Mods/ModSelectColumn.cs @@ -70,7 +70,7 @@ namespace osu.Game.Overlays.Mods { Width = WIDTH; RelativeSizeAxes = Axes.Y; - Shear = new Vector2(ShearedOverlayContainer.SHEAR, 0); + Shear = new Vector2(OsuGame.SHEAR, 0); InternalChildren = new Drawable[] { @@ -96,7 +96,7 @@ namespace osu.Game.Overlays.Mods { RelativeSizeAxes = Axes.X, Height = header_height, - Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0), + Shear = new Vector2(-OsuGame.SHEAR, 0), Velocity = 0.7f, ClampAxes = Axes.Y }, @@ -111,7 +111,7 @@ namespace osu.Game.Overlays.Mods AutoSizeAxes = Axes.Y, Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0), + Shear = new Vector2(-OsuGame.SHEAR, 0), Padding = new MarginPadding { Horizontal = 17, diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 25293e8e20..f521b2e38c 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -227,7 +227,7 @@ namespace osu.Game.Overlays.Mods Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, Direction = FillDirection.Horizontal, - Shear = new Vector2(SHEAR, 0), + Shear = new Vector2(OsuGame.SHEAR, 0), RelativeSizeAxes = Axes.Y, AutoSizeAxes = Axes.X, Margin = new MarginPadding { Horizontal = 70 }, @@ -847,7 +847,7 @@ namespace osu.Game.Overlays.Mods // DrawWidth/DrawPosition do not include shear effects, and we want to know the full extents of the columns post-shear, // so we have to manually compensate. var topLeft = column.ToSpaceOfOtherDrawable(Vector2.Zero, ScrollContent); - var bottomRight = column.ToSpaceOfOtherDrawable(new Vector2(column.DrawWidth - column.DrawHeight * SHEAR, 0), ScrollContent); + var bottomRight = column.ToSpaceOfOtherDrawable(new Vector2(column.DrawWidth - column.DrawHeight * OsuGame.SHEAR, 0), ScrollContent); bool isCurrentlyVisible = Precision.AlmostBigger(topLeft.X, leftVisibleBound) && Precision.DefinitelyBigger(rightVisibleBound, bottomRight.X); diff --git a/osu.Game/Overlays/Mods/ModSelectPanel.cs b/osu.Game/Overlays/Mods/ModSelectPanel.cs index 29f4c93e88..284356f37e 100644 --- a/osu.Game/Overlays/Mods/ModSelectPanel.cs +++ b/osu.Game/Overlays/Mods/ModSelectPanel.cs @@ -87,7 +87,7 @@ namespace osu.Game.Overlays.Mods Content.CornerRadius = CORNER_RADIUS; Content.BorderThickness = 2; - Shear = new Vector2(ShearedOverlayContainer.SHEAR, 0); + Shear = new Vector2(OsuGame.SHEAR, 0); Children = new Drawable[] { @@ -128,10 +128,10 @@ namespace osu.Game.Overlays.Mods { Font = OsuFont.TorusAlternate.With(size: 18, weight: FontWeight.SemiBold), RelativeSizeAxes = Axes.X, - Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0), + Shear = new Vector2(-OsuGame.SHEAR, 0), Margin = new MarginPadding { - Left = -18 * ShearedOverlayContainer.SHEAR + Left = -18 * OsuGame.SHEAR }, ShowTooltip = false, // Tooltip is handled by `IncompatibilityDisplayingModPanel`. }, @@ -139,7 +139,7 @@ namespace osu.Game.Overlays.Mods { Font = OsuFont.Default.With(size: 12), RelativeSizeAxes = Axes.X, - Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0), + Shear = new Vector2(-OsuGame.SHEAR, 0), ShowTooltip = false, // Tooltip is handled by `IncompatibilityDisplayingModPanel`. } } diff --git a/osu.Game/Overlays/Mods/RankingInformationDisplay.cs b/osu.Game/Overlays/Mods/RankingInformationDisplay.cs index 494f8a377f..75a8f289d8 100644 --- a/osu.Game/Overlays/Mods/RankingInformationDisplay.cs +++ b/osu.Game/Overlays/Mods/RankingInformationDisplay.cs @@ -52,7 +52,7 @@ namespace osu.Game.Overlays.Mods Anchor = Anchor.BottomRight, Origin = Anchor.BottomRight, RelativeSizeAxes = Axes.Both, - Shear = new Vector2(ShearedOverlayContainer.SHEAR, 0), + Shear = new Vector2(OsuGame.SHEAR, 0), CornerRadius = ShearedButton.CORNER_RADIUS, Masking = true, Children = new Drawable[] @@ -79,7 +79,7 @@ namespace osu.Game.Overlays.Mods { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0), + Shear = new Vector2(-OsuGame.SHEAR, 0), Font = OsuFont.Default.With(size: 17, weight: FontWeight.SemiBold) } } @@ -94,7 +94,7 @@ namespace osu.Game.Overlays.Mods Origin = Anchor.Centre, Child = counter = new EffectCounter { - Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0), + Shear = new Vector2(-OsuGame.SHEAR, 0), Anchor = Anchor.Centre, Origin = Anchor.Centre, Current = { BindTarget = ModMultiplier } diff --git a/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs b/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs index 893ac89aa4..acdd1db728 100644 --- a/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs +++ b/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs @@ -22,9 +22,6 @@ namespace osu.Game.Overlays.Mods { protected const float PADDING = 14; - // todo: maybe move this to a higher place since it's used for screen footer buttons etc. - public const float SHEAR = 0.15f; - [Cached] protected readonly OverlayColourProvider ColourProvider; diff --git a/osu.Game/Screens/Footer/ScreenFooterButton.cs b/osu.Game/Screens/Footer/ScreenFooterButton.cs index e0b019bfa8..40e79ed474 100644 --- a/osu.Game/Screens/Footer/ScreenFooterButton.cs +++ b/osu.Game/Screens/Footer/ScreenFooterButton.cs @@ -26,7 +26,7 @@ namespace osu.Game.Screens.Footer { public partial class ScreenFooterButton : OsuClickableContainer, IKeyBindingHandler { - private const float shear = ShearedOverlayContainer.SHEAR; + private const float shear = OsuGame.SHEAR; protected const int CORNER_RADIUS = 10; protected const int BUTTON_HEIGHT = 90; From 820cfbcb005f759d1e0f1858781d762028ff08e2 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 16 May 2024 07:47:30 +0300 Subject: [PATCH 029/119] Remove unused using directives --- osu.Game/Graphics/UserInterface/ShearedButton.cs | 1 - osu.Game/Graphics/UserInterface/ShearedSearchTextBox.cs | 1 - osu.Game/Screens/Footer/ScreenFooterButton.cs | 1 - 3 files changed, 3 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/ShearedButton.cs b/osu.Game/Graphics/UserInterface/ShearedButton.cs index 7eed388707..87d269ccd4 100644 --- a/osu.Game/Graphics/UserInterface/ShearedButton.cs +++ b/osu.Game/Graphics/UserInterface/ShearedButton.cs @@ -11,7 +11,6 @@ using osu.Framework.Localisation; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Overlays; -using osu.Game.Overlays.Mods; using osuTK; namespace osu.Game.Graphics.UserInterface diff --git a/osu.Game/Graphics/UserInterface/ShearedSearchTextBox.cs b/osu.Game/Graphics/UserInterface/ShearedSearchTextBox.cs index 5f3716f36c..c6565726b5 100644 --- a/osu.Game/Graphics/UserInterface/ShearedSearchTextBox.cs +++ b/osu.Game/Graphics/UserInterface/ShearedSearchTextBox.cs @@ -11,7 +11,6 @@ using osu.Framework.Graphics.UserInterface; using osu.Framework.Localisation; using osu.Game.Graphics.Sprites; using osu.Game.Overlays; -using osu.Game.Overlays.Mods; using osu.Game.Resources.Localisation.Web; using osuTK; diff --git a/osu.Game/Screens/Footer/ScreenFooterButton.cs b/osu.Game/Screens/Footer/ScreenFooterButton.cs index 40e79ed474..8fbd9f6cba 100644 --- a/osu.Game/Screens/Footer/ScreenFooterButton.cs +++ b/osu.Game/Screens/Footer/ScreenFooterButton.cs @@ -18,7 +18,6 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Input.Bindings; using osu.Game.Overlays; -using osu.Game.Overlays.Mods; using osuTK; using osuTK.Graphics; From a12a20e8b503bdb2a5647bebefe05de33b0553be Mon Sep 17 00:00:00 2001 From: Fabian van Oeffelt Date: Sat, 18 May 2024 18:37:44 +0200 Subject: [PATCH 030/119] Change Inputkeys to Ctrl+Up/Ctrl+Down --- osu.Game/Input/Bindings/GlobalActionContainer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index 5dacb6db4d..b0a1684512 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -182,8 +182,8 @@ namespace osu.Game.Input.Bindings new KeyBinding(new[] { InputKey.Shift, InputKey.F2 }, GlobalAction.SelectPreviousRandom), new KeyBinding(InputKey.F3, GlobalAction.ToggleBeatmapOptions), new KeyBinding(InputKey.BackSpace, GlobalAction.DeselectAllMods), - new KeyBinding(InputKey.PageUp, GlobalAction.IncreaseSpeed), - new KeyBinding(InputKey.PageDown, GlobalAction.DecreaseSpeed), + new KeyBinding(new[] { InputKey.Control, InputKey.Up }, GlobalAction.IncreaseSpeed), + new KeyBinding(new[] { InputKey.Control, InputKey.Down }, GlobalAction.DecreaseSpeed), }; private static IEnumerable audioControlKeyBindings => new[] From 80064c4b98d955431ac423578b3a9c55b3f4ae7e Mon Sep 17 00:00:00 2001 From: Fabian van Oeffelt Date: Sat, 18 May 2024 18:38:23 +0200 Subject: [PATCH 031/119] Speedchange now also works in Modselect --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 25293e8e20..572379ea2c 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -27,6 +27,7 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; using osu.Game.Localisation; using osu.Game.Rulesets.Mods; +using osu.Game.Screens.Select; using osu.Game.Utils; using osuTK; using osuTK.Input; @@ -63,6 +64,9 @@ namespace osu.Game.Overlays.Mods private Func isValidMod = _ => true; + [Resolved] + private SongSelect? songSelect { get; set; } + /// /// A function determining whether each mod in the column should be displayed. /// A return value of means that the mod is not filtered and therefore its corresponding panel should be displayed. @@ -752,6 +756,14 @@ namespace osu.Game.Overlays.Mods return true; } + + case GlobalAction.IncreaseSpeed: + songSelect!.ChangeSpeed(0.05); + return true; + + case GlobalAction.DecreaseSpeed: + songSelect!.ChangeSpeed(-0.05); + return true; } return base.OnPressed(e); From 99f30d92c84789670b40ae3989cd52b2dc3a3dc2 Mon Sep 17 00:00:00 2001 From: Fabian van Oeffelt Date: Sat, 18 May 2024 18:38:31 +0200 Subject: [PATCH 032/119] Add Unit Tests --- .../SongSelect/TestScenePlaySongSelect.cs | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index e03ffd48f1..938b858110 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -87,6 +87,84 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("delete all beatmaps", () => manager.Delete()); } + [Test] + public void TestSpeedChange() + { + createSongSelect(); + changeMods(); + + AddStep("decreasing speed without mods", () => songSelect?.ChangeSpeed(-0.05)); + AddAssert("halftime at 0.95", () => songSelect!.Mods.Value.Single() is ModHalfTime mod && mod.SpeedChange.Value == 0.95); + + AddStep("decreasing speed with halftime", () => songSelect?.ChangeSpeed(-0.05)); + AddAssert("halftime at 0.9", () => songSelect!.Mods.Value.Single() is ModHalfTime mod && mod.SpeedChange.Value == 0.9); + + AddStep("increasing speed with halftime", () => songSelect?.ChangeSpeed(+0.05)); + AddAssert("halftime at 0.95", () => songSelect!.Mods.Value.Single() is ModHalfTime mod && mod.SpeedChange.Value == 0.95); + + AddStep("increasing speed with halftime to nomod", () => songSelect?.ChangeSpeed(+0.05)); + AddAssert("no mods selected", () => songSelect!.Mods.Value.Count == 0); + + AddStep("increasing speed without mods", () => songSelect?.ChangeSpeed(+0.05)); + AddAssert("doubletime at 1.05", () => songSelect!.Mods.Value.Single() is ModDoubleTime mod && mod.SpeedChange.Value == 1.05); + + AddStep("increasing speed with doubletime", () => songSelect?.ChangeSpeed(+0.05)); + AddAssert("doubletime at 1.1", () => songSelect!.Mods.Value.Single() is ModDoubleTime mod && mod.SpeedChange.Value == 1.1); + + AddStep("decreasing speed with doubletime", () => songSelect?.ChangeSpeed(-0.05)); + AddAssert("doubletime at 1.05", () => songSelect!.Mods.Value.Single() is ModDoubleTime mod && mod.SpeedChange.Value == 1.05); + + OsuModNightcore nc = new OsuModNightcore + { + SpeedChange = { Value = 1.05 } + }; + changeMods(nc); + AddStep("increasing speed with nightcore", () => songSelect?.ChangeSpeed(+0.05)); + AddAssert("nightcore at 1.1", () => songSelect!.Mods.Value.Single() is ModNightcore mod && mod.SpeedChange.Value == 1.1); + + AddStep("decreasing speed with nightcore", () => songSelect?.ChangeSpeed(-0.05)); + AddAssert("doubletime at 1.05", () => songSelect!.Mods.Value.Single() is ModNightcore mod && mod.SpeedChange.Value == 1.05); + + AddStep("decreasing speed with nightcore to nomod", () => songSelect?.ChangeSpeed(-0.05)); + AddAssert("no mods selected", () => songSelect!.Mods.Value.Count == 0); + + AddStep("decreasing speed nomod, nightcore was selected", () => songSelect?.ChangeSpeed(-0.05)); + AddAssert("daycore at 0.95", () => songSelect!.Mods.Value.Single() is ModDaycore mod && mod.SpeedChange.Value == 0.95); + + AddStep("decreasing speed with daycore", () => songSelect?.ChangeSpeed(-0.05)); + AddAssert("daycore at 0.9", () => songSelect!.Mods.Value.Single() is ModDaycore mod && mod.SpeedChange.Value == 0.9); + + AddStep("increasing speed with daycore", () => songSelect?.ChangeSpeed(0.05)); + AddAssert("daycore at 0.95", () => songSelect!.Mods.Value.Single() is ModDaycore mod && mod.SpeedChange.Value == 0.95); + + OsuModDoubleTime dt = new OsuModDoubleTime + { + SpeedChange = { Value = 1.02 }, + AdjustPitch = { Value = true }, + }; + changeMods(dt); + AddStep("decreasing speed from doubletime 1.02 with adjustpitch enabled", () => songSelect?.ChangeSpeed(-0.05)); + AddAssert("halftime at 0.97 with adjustpitch enabled", () => songSelect!.Mods.Value.Single() is ModHalfTime mod && mod.SpeedChange.Value == 0.97 && mod.AdjustPitch.Value); + + OsuModHalfTime ht = new OsuModHalfTime + { + SpeedChange = { Value = 0.97 }, + AdjustPitch = { Value = true }, + }; + Mod[] modlist = { ht, new OsuModHardRock(), new OsuModHidden() }; + changeMods(modlist); + AddStep("decreasing speed from halftime 0.97 with adjustpitch enabled, HDHR enabled", () => songSelect?.ChangeSpeed(0.05)); + AddAssert("doubletime at 1.02 with adjustpitch enabled, HDHR still enabled", () => songSelect!.Mods.Value.Count(mod => (mod is ModDoubleTime modDt && modDt.AdjustPitch.Value && modDt.SpeedChange.Value == 1.02) || mod is ModHardRock || mod is ModHidden) == 3); + + changeMods(new ModWindUp()); + AddStep("windup active, trying to change speed", () => songSelect?.ChangeSpeed(0.05)); + AddAssert("windup still active", () => songSelect!.Mods.Value.First() is ModWindUp); + + changeMods(new ModAdaptiveSpeed()); + AddStep("adaptivespeed active, trying to change speed", () => songSelect?.ChangeSpeed(0.05)); + AddAssert("adaptivespeed still active", () => songSelect!.Mods.Value.First() is ModAdaptiveSpeed); + } + [Test] public void TestPlaceholderBeatmapPresence() { From 3fdbd735ce063653bb92b7570df716e700a9b529 Mon Sep 17 00:00:00 2001 From: Fabian van Oeffelt Date: Sat, 18 May 2024 18:40:51 +0200 Subject: [PATCH 033/119] change to single Function, Nightcore now switches into Daycore, keep Adjustpitch after change --- osu.Game/Screens/Select/SongSelect.cs | 182 +++++++++++++++----------- 1 file changed, 109 insertions(+), 73 deletions(-) diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index de0f24aa90..e1447b284a 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -98,6 +98,9 @@ namespace osu.Game.Screens.Select new OsuMenuItem(@"Select", MenuItemType.Highlighted, () => FinaliseSelection(getBeatmap())) }; + [Resolved] + private OsuGameBase? game { get; set; } + [Resolved] private Bindable> selectedMods { get; set; } = null!; @@ -144,6 +147,10 @@ namespace osu.Game.Screens.Select private Bindable configBackgroundBlur = null!; + private bool lastPitchState; + + private bool usedPitchMods; + [BackgroundDependencyLoader(true)] private void load(AudioManager audio, OsuColour colours, ManageCollectionsDialog? manageCollectionsDialog, DifficultyRecommender? recommender, OsuConfigManager config) { @@ -809,94 +816,123 @@ namespace osu.Game.Screens.Select return false; } - private void increaseSpeed() + public void ChangeSpeed(double delta) { - var rateAdjustStates = ModSelect.AllAvailableMods.Where(pair => pair.Mod is ModRateAdjust); - var stateDoubleTime = ModSelect.AllAvailableMods.First(pair => pair.Mod is ModDoubleTime); - bool rateModActive = ModSelect.AllAvailableMods.Count(pair => pair.Mod is ModRateAdjust && pair.Active.Value) > 0; - const double stepsize = 0.05d; - double newRate = 1d + stepsize; + // Mod Change from 0.95 DC to 1.0 none to 1.05 DT/NC ? - // If no mod rateAdjust mod is currently active activate DoubleTime with speed newRate - if (!rateModActive) - { - stateDoubleTime.Active.Value = true; - ((ModDoubleTime)stateDoubleTime.Mod).SpeedChange.Value = newRate; + if (game == null) return; + + ModNightcore modNc = (ModNightcore)((MultiMod)game.AvailableMods.Value[ModType.DifficultyIncrease].First(mod => mod is MultiMod multiMod && multiMod.Mods.Count(modType => modType is ModNightcore) > 0)).Mods.First(mod => mod is ModNightcore); + ModDoubleTime modDt = (ModDoubleTime)((MultiMod)game.AvailableMods.Value[ModType.DifficultyIncrease].First(mod => mod is MultiMod multiMod && multiMod.Mods.Count(modType => modType is ModDoubleTime) > 0)).Mods.First(mod => mod is ModDoubleTime); + ModDaycore modDc = (ModDaycore)((MultiMod)game.AvailableMods.Value[ModType.DifficultyReduction].First(mod => mod is MultiMod multiMod && multiMod.Mods.Count(modType => modType is ModDaycore) > 0)).Mods.First(mod => mod is ModDaycore); + ModHalfTime modHt = (ModHalfTime)((MultiMod)game.AvailableMods.Value[ModType.DifficultyReduction].First(mod => mod is MultiMod multiMod && multiMod.Mods.Count(modType => modType is ModHalfTime) > 0)).Mods.First(mod => mod is ModHalfTime); + bool rateModActive = selectedMods.Value.Count(mod => mod is ModRateAdjust) > 0; + bool incompatiableModActive = selectedMods.Value.Count(mod => modDt.IncompatibleMods.Count(incompatibleMod => (mod.GetType().IsSubclassOf(incompatibleMod) || mod.GetType() == incompatibleMod) && incompatibleMod != typeof(ModRateAdjust)) > 0) > 0; + double newRate = 1d + delta; + bool isPositive = delta > 0; + + if (incompatiableModActive) return; - } - // Find current active rateAdjust mod and modify speed, enable DoubleTime if necessary - foreach (var state in rateAdjustStates) + if (rateModActive) { - ModRateAdjust mod = (ModRateAdjust)state.Mod; + ModRateAdjust mod = (ModRateAdjust)selectedMods.Value.First(mod => mod is ModRateAdjust); - if (!state.Active.Value) continue; + // Find current active rateAdjust mod and modify speed, enable HalfTime if necessary + newRate = mod.SpeedChange.Value + delta; - newRate = mod.SpeedChange.Value + stepsize; - - if (mod.Acronym == "DT" || mod.Acronym == "NC") - mod.SpeedChange.Value = newRate; - else + if (newRate == 1.0) { - if (newRate == 1.0d) - state.Active.Value = false; + lastPitchState = false; + usedPitchMods = false; - if (newRate > 1d) - { - state.Active.Value = false; - stateDoubleTime.Active.Value = true; - ((ModDoubleTime)stateDoubleTime.Mod).SpeedChange.Value = newRate; - break; - } + if (mod is ModDoubleTime dtmod && dtmod.AdjustPitch.Value) lastPitchState = true; - if (newRate < 1d) - mod.SpeedChange.Value = newRate; + if (mod is ModHalfTime htmod && htmod.AdjustPitch.Value) lastPitchState = true; + + if (mod is ModNightcore || mod is ModDaycore) usedPitchMods = true; + + //Disable RateAdjustMods + selectedMods.Value = selectedMods.Value.Where(search => search is not ModRateAdjust).ToList(); + return; } - } - } - private void decreaseSpeed() - { - var rateAdjustStates = ModSelect.AllAvailableMods.Where(pair => pair.Mod is ModRateAdjust); - var stateHalfTime = ModSelect.AllAvailableMods.First(pair => pair.Mod is ModHalfTime); - bool rateModActive = ModSelect.AllAvailableMods.Count(pair => pair.Mod is ModRateAdjust && pair.Active.Value) > 0; - const double stepsize = 0.05d; - double newRate = 1d - stepsize; - - // If no mod rateAdjust mod is currently active activate HalfTime with speed newRate - if (!rateModActive) - { - stateHalfTime.Active.Value = true; - ((ModHalfTime)stateHalfTime.Mod).SpeedChange.Value = newRate; - return; - } - - // Find current active rateAdjust mod and modify speed, enable HalfTime if necessary - foreach (var state in rateAdjustStates) - { - ModRateAdjust mod = (ModRateAdjust)state.Mod; - - if (!state.Active.Value) continue; - - newRate = mod.SpeedChange.Value - stepsize; - - if (mod.Acronym == "HT" || mod.Acronym == "DC") - mod.SpeedChange.Value = newRate; - else + if (((mod is ModDoubleTime || mod is ModNightcore) && newRate < mod.SpeedChange.MinValue) + || ((mod is ModHalfTime || mod is ModDaycore) && newRate > mod.SpeedChange.MaxValue)) { - if (newRate == 1.0d) - state.Active.Value = false; + bool adjustPitch = (mod is ModDoubleTime dtmod && dtmod.AdjustPitch.Value) || (mod is ModHalfTime htmod && htmod.AdjustPitch.Value); - if (newRate < 1d) + //Disable RateAdjustMods + selectedMods.Value = selectedMods.Value.Where(search => search is not ModRateAdjust).ToList(); + + ModRateAdjust? oppositeMod = null; + + switch (mod) { - state.Active.Value = false; - stateHalfTime.Active.Value = true; - ((ModHalfTime)stateHalfTime.Mod).SpeedChange.Value = newRate; - break; + case ModDoubleTime: + modHt.AdjustPitch.Value = adjustPitch; + oppositeMod = modHt; + break; + + case ModHalfTime: + modDt.AdjustPitch.Value = adjustPitch; + oppositeMod = modDt; + break; + + case ModNightcore: + oppositeMod = modDc; + break; + + case ModDaycore: + oppositeMod = modNc; + break; } - if (newRate > 1d) - mod.SpeedChange.Value = newRate; + if (oppositeMod == null) return; + + oppositeMod.SpeedChange.Value = newRate; + selectedMods.Value = selectedMods.Value.Append(oppositeMod).ToList(); + return; + } + + if (newRate > mod.SpeedChange.MaxValue && (mod is ModDoubleTime || mod is ModNightcore)) + newRate = mod.SpeedChange.MaxValue; + + if (newRate < mod.SpeedChange.MinValue && (mod is ModHalfTime || mod is ModDaycore)) + newRate = mod.SpeedChange.MinValue; + + mod.SpeedChange.Value = newRate; + } + else + { + // If no ModRateAdjust is actived activate one + if (isPositive) + { + if (!usedPitchMods) + { + modDt.SpeedChange.Value = newRate; + modDt.AdjustPitch.Value = lastPitchState; + selectedMods.Value = selectedMods.Value.Append(modDt).ToList(); + } + else + { + modNc.SpeedChange.Value = newRate; + selectedMods.Value = selectedMods.Value.Append(modNc).ToList(); + } + } + else + { + if (!usedPitchMods) + { + modHt.SpeedChange.Value = newRate; + modHt.AdjustPitch.Value = lastPitchState; + selectedMods.Value = selectedMods.Value.Append(modHt).ToList(); + } + else + { + modDc.SpeedChange.Value = newRate; + selectedMods.Value = selectedMods.Value.Append(modDc).ToList(); + } } } } @@ -1111,11 +1147,11 @@ namespace osu.Game.Screens.Select return true; case GlobalAction.IncreaseSpeed: - increaseSpeed(); + ChangeSpeed(0.05); return true; case GlobalAction.DecreaseSpeed: - decreaseSpeed(); + ChangeSpeed(-0.05); return true; } From 45fcbea182d1076ae9239984f76ed9b36b458c5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 17 May 2024 14:43:06 +0200 Subject: [PATCH 034/119] Compute total score without mods during standardised score conversion This is going to be used by server-side flows. Note that the server-side overload of `UpdateFromLegacy()` was not calling `LegacyScoreDecoder.PopulateTotalScoreWithoutMods()`. Computing the score without mods inline reduces reflection overheads from constructing mod instances, which feels pretty important for server-side flows. There is one weird kink in the treatment of stable scores with score V2 active - they get the *legacy* multipliers unapplied for them because that made the most sense. For all intents and purposes this matters mostly for client-side replays with score V2. I'm not sure whether scores with SV2 ever make it to submission in stable. There may be minute differences in converted score due to rounding shenanigans but I don't think it's worth doing a reverify for this. --- .../StandardisedScoreMigrationTools.cs | 67 ++++++++++--------- 1 file changed, 34 insertions(+), 33 deletions(-) diff --git a/osu.Game/Database/StandardisedScoreMigrationTools.cs b/osu.Game/Database/StandardisedScoreMigrationTools.cs index 7d09ebdb40..db44731bed 100644 --- a/osu.Game/Database/StandardisedScoreMigrationTools.cs +++ b/osu.Game/Database/StandardisedScoreMigrationTools.cs @@ -16,7 +16,6 @@ using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring.Legacy; using osu.Game.Scoring; -using osu.Game.Scoring.Legacy; namespace osu.Game.Database { @@ -248,8 +247,7 @@ namespace osu.Game.Database // warning: ordering is important here - both total score and ranks are dependent on accuracy! score.Accuracy = computeAccuracy(score, scoreProcessor); score.Rank = computeRank(score, scoreProcessor); - score.TotalScore = convertFromLegacyTotalScore(score, ruleset, beatmap); - LegacyScoreDecoder.PopulateTotalScoreWithoutMods(score); + (score.TotalScoreWithoutMods, score.TotalScore) = convertFromLegacyTotalScore(score, ruleset, beatmap); } /// @@ -273,7 +271,7 @@ namespace osu.Game.Database // warning: ordering is important here - both total score and ranks are dependent on accuracy! score.Accuracy = computeAccuracy(score, scoreProcessor); score.Rank = computeRank(score, scoreProcessor); - score.TotalScore = convertFromLegacyTotalScore(score, ruleset, difficulty, attributes); + (score.TotalScoreWithoutMods, score.TotalScore) = convertFromLegacyTotalScore(score, ruleset, difficulty, attributes); } /// @@ -283,17 +281,13 @@ namespace osu.Game.Database /// The in which the score was set. /// The applicable for this score. /// The standardised total score. - private static long convertFromLegacyTotalScore(ScoreInfo score, Ruleset ruleset, WorkingBeatmap beatmap) + private static (long withoutMods, long withMods) convertFromLegacyTotalScore(ScoreInfo score, Ruleset ruleset, WorkingBeatmap beatmap) { if (!score.IsLegacyScore) - return score.TotalScore; + return (score.TotalScoreWithoutMods, score.TotalScore); if (ruleset is not ILegacyRuleset legacyRuleset) - return score.TotalScore; - - var mods = score.Mods; - if (mods.Any(mod => mod is ModScoreV2)) - return score.TotalScore; + return (score.TotalScoreWithoutMods, score.TotalScore); var playableBeatmap = beatmap.GetPlayableBeatmap(ruleset.RulesetInfo, score.Mods); @@ -302,8 +296,13 @@ namespace osu.Game.Database ILegacyScoreSimulator sv1Simulator = legacyRuleset.CreateLegacyScoreSimulator(); LegacyScoreAttributes attributes = sv1Simulator.Simulate(beatmap, playableBeatmap); + var legacyBeatmapConversionDifficultyInfo = LegacyBeatmapConversionDifficultyInfo.FromBeatmap(beatmap.Beatmap); - return convertFromLegacyTotalScore(score, ruleset, LegacyBeatmapConversionDifficultyInfo.FromBeatmap(beatmap.Beatmap), attributes); + var mods = score.Mods; + if (mods.Any(mod => mod is ModScoreV2)) + return ((long)Math.Round(score.TotalScore / sv1Simulator.GetLegacyScoreMultiplier(mods, legacyBeatmapConversionDifficultyInfo)), score.TotalScore); + + return convertFromLegacyTotalScore(score, ruleset, legacyBeatmapConversionDifficultyInfo, attributes); } /// @@ -314,15 +313,15 @@ namespace osu.Game.Database /// The beatmap difficulty. /// The legacy scoring attributes for the beatmap which the score was set on. /// The standardised total score. - private static long convertFromLegacyTotalScore(ScoreInfo score, Ruleset ruleset, LegacyBeatmapConversionDifficultyInfo difficulty, LegacyScoreAttributes attributes) + private static (long withoutMods, long withMods) convertFromLegacyTotalScore(ScoreInfo score, Ruleset ruleset, LegacyBeatmapConversionDifficultyInfo difficulty, LegacyScoreAttributes attributes) { if (!score.IsLegacyScore) - return score.TotalScore; + return (score.TotalScoreWithoutMods, score.TotalScore); Debug.Assert(score.LegacyTotalScore != null); if (ruleset is not ILegacyRuleset legacyRuleset) - return score.TotalScore; + return (score.TotalScoreWithoutMods, score.TotalScore); double legacyModMultiplier = legacyRuleset.CreateLegacyScoreSimulator().GetLegacyScoreMultiplier(score.Mods, difficulty); int maximumLegacyAccuracyScore = attributes.AccuracyScore; @@ -354,17 +353,18 @@ namespace osu.Game.Database double modMultiplier = score.Mods.Select(m => m.ScoreMultiplier).Aggregate(1.0, (c, n) => c * n); - long convertedTotalScore; + long convertedTotalScoreWithoutMods; switch (score.Ruleset.OnlineID) { case 0: if (score.MaxCombo == 0 || score.Accuracy == 0) { - return (long)Math.Round(( + convertedTotalScoreWithoutMods = (long)Math.Round( 0 + 500000 * Math.Pow(score.Accuracy, 5) - + bonusProportion) * modMultiplier); + + bonusProportion); + break; } // see similar check above. @@ -372,10 +372,11 @@ namespace osu.Game.Database // are either pointless or wildly wrong. if (maximumLegacyComboScore + maximumLegacyBonusScore == 0) { - return (long)Math.Round(( + convertedTotalScoreWithoutMods = (long)Math.Round( 500000 * comboProportion // as above, zero if mods result in zero multiplier, one otherwise + 500000 * Math.Pow(score.Accuracy, 5) - + bonusProportion) * modMultiplier); + + bonusProportion); + break; } // Assumptions: @@ -472,17 +473,17 @@ namespace osu.Game.Database double newComboScoreProportion = estimatedComboPortionInStandardisedScore / maximumAchievableComboPortionInStandardisedScore; - convertedTotalScore = (long)Math.Round(( + convertedTotalScoreWithoutMods = (long)Math.Round( 500000 * newComboScoreProportion * score.Accuracy + 500000 * Math.Pow(score.Accuracy, 5) - + bonusProportion) * modMultiplier); + + bonusProportion); break; case 1: - convertedTotalScore = (long)Math.Round(( + convertedTotalScoreWithoutMods = (long)Math.Round( 250000 * comboProportion + 750000 * Math.Pow(score.Accuracy, 3.6) - + bonusProportion) * modMultiplier); + + bonusProportion); break; case 2: @@ -507,28 +508,28 @@ namespace osu.Game.Database ? 0 : (double)score.Statistics.GetValueOrDefault(HitResult.SmallTickHit) / score.MaximumStatistics.GetValueOrDefault(HitResult.SmallTickHit); - convertedTotalScore = (long)Math.Round(( + convertedTotalScoreWithoutMods = (long)Math.Round( comboPortion * estimateComboProportionForCatch(attributes.MaxCombo, score.MaxCombo, score.Statistics.GetValueOrDefault(HitResult.Miss)) + dropletsPortion * dropletsHit - + bonusProportion) * modMultiplier); + + bonusProportion); break; case 3: - convertedTotalScore = (long)Math.Round(( + convertedTotalScoreWithoutMods = (long)Math.Round( 850000 * comboProportion + 150000 * Math.Pow(score.Accuracy, 2 + 2 * score.Accuracy) - + bonusProportion) * modMultiplier); + + bonusProportion); break; default: - convertedTotalScore = score.TotalScore; - break; + return (score.TotalScoreWithoutMods, score.TotalScore); } - if (convertedTotalScore < 0) - throw new InvalidOperationException($"Total score conversion operation returned invalid total of {convertedTotalScore}"); + if (convertedTotalScoreWithoutMods < 0) + throw new InvalidOperationException($"Total score conversion operation returned invalid total of {convertedTotalScoreWithoutMods}"); - return convertedTotalScore; + long convertedTotalScore = (long)Math.Round(convertedTotalScoreWithoutMods * modMultiplier); + return (convertedTotalScoreWithoutMods, convertedTotalScore); } /// From 148afd120127c58655ed35190fb7456ce1f0e973 Mon Sep 17 00:00:00 2001 From: Fabian van Oeffelt Date: Tue, 21 May 2024 14:47:34 +0200 Subject: [PATCH 035/119] Change Speedchange behaviour to keep changing while holding key, Add Toast to nofity user what just happend --- osu.Game/Localisation/ToastStrings.cs | 5 +++++ osu.Game/Overlays/OSD/SpeedChangeToast.cs | 17 +++++++++++++++ osu.Game/Screens/Select/SongSelect.cs | 26 ++++++++++++++++------- 3 files changed, 40 insertions(+), 8 deletions(-) create mode 100644 osu.Game/Overlays/OSD/SpeedChangeToast.cs diff --git a/osu.Game/Localisation/ToastStrings.cs b/osu.Game/Localisation/ToastStrings.cs index da798a3937..33027966dd 100644 --- a/osu.Game/Localisation/ToastStrings.cs +++ b/osu.Game/Localisation/ToastStrings.cs @@ -49,6 +49,11 @@ namespace osu.Game.Localisation /// public static LocalisableString UrlCopied => new TranslatableString(getKey(@"url_copied"), @"URL copied"); + /// + /// "Speed Changed" + /// + public static LocalisableString SpeedChanged => new TranslatableString(getKey(@"speed_changed"), @"Speed Changed"); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Overlays/OSD/SpeedChangeToast.cs b/osu.Game/Overlays/OSD/SpeedChangeToast.cs new file mode 100644 index 0000000000..73ba23622b --- /dev/null +++ b/osu.Game/Overlays/OSD/SpeedChangeToast.cs @@ -0,0 +1,17 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Configuration; +using osu.Game.Input.Bindings; +using osu.Game.Localisation; + +namespace osu.Game.Overlays.OSD +{ + public partial class SpeedChangeToast : Toast + { + public SpeedChangeToast(OsuConfigManager config, double delta) + : base(CommonStrings.Beatmaps, ToastStrings.SpeedChanged, config.LookupKeyBindings(GlobalAction.IncreaseSpeed) + " / " + config.LookupKeyBindings(GlobalAction.DecreaseSpeed)) + { + } + } +} diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index e1447b284a..7eb2be9100 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -30,6 +30,7 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; using osu.Game.Overlays; using osu.Game.Overlays.Mods; +using osu.Game.Overlays.OSD; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Screens.Backgrounds; @@ -151,6 +152,12 @@ namespace osu.Game.Screens.Select private bool usedPitchMods; + [Resolved] + private OnScreenDisplay? onScreenDisplay { get; set; } + + [Resolved] + private OsuConfigManager? config { get; set; } + [BackgroundDependencyLoader(true)] private void load(AudioManager audio, OsuColour colours, ManageCollectionsDialog? manageCollectionsDialog, DifficultyRecommender? recommender, OsuConfigManager config) { @@ -819,7 +826,7 @@ namespace osu.Game.Screens.Select public void ChangeSpeed(double delta) { // Mod Change from 0.95 DC to 1.0 none to 1.05 DT/NC ? - + onScreenDisplay?.Display(new SpeedChangeToast(config!, delta)); if (game == null) return; ModNightcore modNc = (ModNightcore)((MultiMod)game.AvailableMods.Value[ModType.DifficultyIncrease].First(mod => mod is MultiMod multiMod && multiMod.Mods.Count(modType => modType is ModNightcore) > 0)).Mods.First(mod => mod is ModNightcore); @@ -1135,17 +1142,10 @@ namespace osu.Game.Screens.Select public virtual bool OnPressed(KeyBindingPressEvent e) { - if (e.Repeat) - return false; - if (!this.IsCurrentScreen()) return false; switch (e.Action) { - case GlobalAction.Select: - FinaliseSelection(); - return true; - case GlobalAction.IncreaseSpeed: ChangeSpeed(0.05); return true; @@ -1155,6 +1155,16 @@ namespace osu.Game.Screens.Select return true; } + if (e.Repeat) + return false; + + switch (e.Action) + { + case GlobalAction.Select: + FinaliseSelection(); + return true; + } + return false; } From 3403789c6fef04827679b27715b653778b7d0aed Mon Sep 17 00:00:00 2001 From: Fabian van Oeffelt Date: Tue, 21 May 2024 16:11:20 +0200 Subject: [PATCH 036/119] Toast now only shows when speed is actually changed --- osu.Game/Screens/Select/SongSelect.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 7eb2be9100..b78134392b 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -825,8 +825,6 @@ namespace osu.Game.Screens.Select public void ChangeSpeed(double delta) { - // Mod Change from 0.95 DC to 1.0 none to 1.05 DT/NC ? - onScreenDisplay?.Display(new SpeedChangeToast(config!, delta)); if (game == null) return; ModNightcore modNc = (ModNightcore)((MultiMod)game.AvailableMods.Value[ModType.DifficultyIncrease].First(mod => mod is MultiMod multiMod && multiMod.Mods.Count(modType => modType is ModNightcore) > 0)).Mods.First(mod => mod is ModNightcore); @@ -841,6 +839,8 @@ namespace osu.Game.Screens.Select if (incompatiableModActive) return; + onScreenDisplay?.Display(new SpeedChangeToast(config!, delta)); + if (rateModActive) { ModRateAdjust mod = (ModRateAdjust)selectedMods.Value.First(mod => mod is ModRateAdjust); From 99d99cede03993796aa4f8fe5f1d344a9cfe9472 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 22 May 2024 11:59:34 +0800 Subject: [PATCH 037/119] Basic cleanup Before I gave up on attempting to fix the method. --- osu.Game/Screens/Select/SongSelect.cs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index b78134392b..18d5799bae 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -100,7 +100,7 @@ namespace osu.Game.Screens.Select }; [Resolved] - private OsuGameBase? game { get; set; } + private OsuGameBase game { get; set; } = null!; [Resolved] private Bindable> selectedMods { get; set; } = null!; @@ -156,7 +156,7 @@ namespace osu.Game.Screens.Select private OnScreenDisplay? onScreenDisplay { get; set; } [Resolved] - private OsuConfigManager? config { get; set; } + private OsuConfigManager config { get; set; } = null!; [BackgroundDependencyLoader(true)] private void load(AudioManager audio, OsuColour colours, ManageCollectionsDialog? manageCollectionsDialog, DifficultyRecommender? recommender, OsuConfigManager config) @@ -825,21 +825,19 @@ namespace osu.Game.Screens.Select public void ChangeSpeed(double delta) { - if (game == null) return; - ModNightcore modNc = (ModNightcore)((MultiMod)game.AvailableMods.Value[ModType.DifficultyIncrease].First(mod => mod is MultiMod multiMod && multiMod.Mods.Count(modType => modType is ModNightcore) > 0)).Mods.First(mod => mod is ModNightcore); ModDoubleTime modDt = (ModDoubleTime)((MultiMod)game.AvailableMods.Value[ModType.DifficultyIncrease].First(mod => mod is MultiMod multiMod && multiMod.Mods.Count(modType => modType is ModDoubleTime) > 0)).Mods.First(mod => mod is ModDoubleTime); ModDaycore modDc = (ModDaycore)((MultiMod)game.AvailableMods.Value[ModType.DifficultyReduction].First(mod => mod is MultiMod multiMod && multiMod.Mods.Count(modType => modType is ModDaycore) > 0)).Mods.First(mod => mod is ModDaycore); ModHalfTime modHt = (ModHalfTime)((MultiMod)game.AvailableMods.Value[ModType.DifficultyReduction].First(mod => mod is MultiMod multiMod && multiMod.Mods.Count(modType => modType is ModHalfTime) > 0)).Mods.First(mod => mod is ModHalfTime); bool rateModActive = selectedMods.Value.Count(mod => mod is ModRateAdjust) > 0; - bool incompatiableModActive = selectedMods.Value.Count(mod => modDt.IncompatibleMods.Count(incompatibleMod => (mod.GetType().IsSubclassOf(incompatibleMod) || mod.GetType() == incompatibleMod) && incompatibleMod != typeof(ModRateAdjust)) > 0) > 0; + bool incompatibleModActive = selectedMods.Value.Count(mod => modDt.IncompatibleMods.Count(incompatibleMod => (mod.GetType().IsSubclassOf(incompatibleMod) || mod.GetType() == incompatibleMod) && incompatibleMod != typeof(ModRateAdjust)) > 0) > 0; double newRate = 1d + delta; bool isPositive = delta > 0; - if (incompatiableModActive) + if (incompatibleModActive) return; - onScreenDisplay?.Display(new SpeedChangeToast(config!, delta)); + onScreenDisplay?.Display(new SpeedChangeToast(config, delta)); if (rateModActive) { @@ -912,7 +910,7 @@ namespace osu.Game.Screens.Select } else { - // If no ModRateAdjust is actived activate one + // If no ModRateAdjust is active, activate one if (isPositive) { if (!usedPitchMods) From 02a388cba6493207a170728abd607d80bfdecb3a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 22 May 2024 12:03:48 +0800 Subject: [PATCH 038/119] Fix enum not being at end (and adjust naming) --- osu.Game/Input/Bindings/GlobalActionContainer.cs | 16 ++++++++-------- .../GlobalActionKeyBindingStrings.cs | 8 ++++---- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 4 ++-- osu.Game/Overlays/OSD/SpeedChangeToast.cs | 2 +- osu.Game/Screens/Select/SongSelect.cs | 4 ++-- 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index b0a1684512..09db7461d6 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -182,8 +182,8 @@ namespace osu.Game.Input.Bindings new KeyBinding(new[] { InputKey.Shift, InputKey.F2 }, GlobalAction.SelectPreviousRandom), new KeyBinding(InputKey.F3, GlobalAction.ToggleBeatmapOptions), new KeyBinding(InputKey.BackSpace, GlobalAction.DeselectAllMods), - new KeyBinding(new[] { InputKey.Control, InputKey.Up }, GlobalAction.IncreaseSpeed), - new KeyBinding(new[] { InputKey.Control, InputKey.Down }, GlobalAction.DecreaseSpeed), + new KeyBinding(new[] { InputKey.Control, InputKey.Up }, GlobalAction.IncreaseModSpeed), + new KeyBinding(new[] { InputKey.Control, InputKey.Down }, GlobalAction.DecreaseModSpeed), }; private static IEnumerable audioControlKeyBindings => new[] @@ -411,12 +411,6 @@ namespace osu.Game.Input.Bindings [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorToggleRotateControl))] EditorToggleRotateControl, - [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.IncreaseSpeed))] - IncreaseSpeed, - - [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.DecreaseSpeed))] - DecreaseSpeed, - [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.IncreaseOffset))] IncreaseOffset, @@ -428,6 +422,12 @@ namespace osu.Game.Input.Bindings [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.StepReplayBackward))] StepReplayBackward, + + [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.IncreaseModSpeed))] + IncreaseModSpeed, + + [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.DecreaseModSpeed))] + DecreaseModSpeed, } public enum GlobalActionCategory diff --git a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs index d0cbf52f07..18a1d3e4fe 100644 --- a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs +++ b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs @@ -370,14 +370,14 @@ namespace osu.Game.Localisation public static LocalisableString EditorToggleRotateControl => new TranslatableString(getKey(@"editor_toggle_rotate_control"), @"Toggle rotate control"); /// - /// "Increase Speed" + /// "Increase mod speed" /// - public static LocalisableString IncreaseSpeed => new TranslatableString(getKey(@"increase_speed"), @"Increase Speed"); + public static LocalisableString IncreaseModSpeed => new TranslatableString(getKey(@"increase_mod_speed"), @"Increase mod speed"); /// - /// "Decrease Speed" + /// "Decrease mod speed" /// - public static LocalisableString DecreaseSpeed => new TranslatableString(getKey(@"decrease_speed"), @"Decrease Speed"); + public static LocalisableString DecreaseModSpeed => new TranslatableString(getKey(@"decrease_mod_speed"), @"Decrease mod speed"); private static string getKey(string key) => $@"{prefix}:{key}"; } diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 572379ea2c..3b8090a4b2 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -757,11 +757,11 @@ namespace osu.Game.Overlays.Mods return true; } - case GlobalAction.IncreaseSpeed: + case GlobalAction.IncreaseModSpeed: songSelect!.ChangeSpeed(0.05); return true; - case GlobalAction.DecreaseSpeed: + case GlobalAction.DecreaseModSpeed: songSelect!.ChangeSpeed(-0.05); return true; } diff --git a/osu.Game/Overlays/OSD/SpeedChangeToast.cs b/osu.Game/Overlays/OSD/SpeedChangeToast.cs index 73ba23622b..231ef86526 100644 --- a/osu.Game/Overlays/OSD/SpeedChangeToast.cs +++ b/osu.Game/Overlays/OSD/SpeedChangeToast.cs @@ -10,7 +10,7 @@ namespace osu.Game.Overlays.OSD public partial class SpeedChangeToast : Toast { public SpeedChangeToast(OsuConfigManager config, double delta) - : base(CommonStrings.Beatmaps, ToastStrings.SpeedChanged, config.LookupKeyBindings(GlobalAction.IncreaseSpeed) + " / " + config.LookupKeyBindings(GlobalAction.DecreaseSpeed)) + : base(CommonStrings.Beatmaps, ToastStrings.SpeedChanged, config.LookupKeyBindings(GlobalAction.IncreaseModSpeed) + " / " + config.LookupKeyBindings(GlobalAction.DecreaseModSpeed)) { } } diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 18d5799bae..257f6583a4 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -1144,11 +1144,11 @@ namespace osu.Game.Screens.Select switch (e.Action) { - case GlobalAction.IncreaseSpeed: + case GlobalAction.IncreaseModSpeed: ChangeSpeed(0.05); return true; - case GlobalAction.DecreaseSpeed: + case GlobalAction.DecreaseModSpeed: ChangeSpeed(-0.05); return true; } From f979200712aa0336de04e30fb2b3bebd714b5920 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 22 May 2024 12:06:51 +0800 Subject: [PATCH 039/119] Use null conditional rather than implicit not-null --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 3b8090a4b2..ad589e8fa9 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -758,11 +758,11 @@ namespace osu.Game.Overlays.Mods } case GlobalAction.IncreaseModSpeed: - songSelect!.ChangeSpeed(0.05); + songSelect?.ChangeSpeed(0.05); return true; case GlobalAction.DecreaseModSpeed: - songSelect!.ChangeSpeed(-0.05); + songSelect?.ChangeSpeed(-0.05); return true; } From 57da4229ff621a12a43ae704bbd21884a9039d74 Mon Sep 17 00:00:00 2001 From: Fabian van Oeffelt Date: Wed, 22 May 2024 13:58:59 +0200 Subject: [PATCH 040/119] Add speed value to Toast --- osu.Game/Localisation/ToastStrings.cs | 4 ++-- osu.Game/Overlays/OSD/SpeedChangeToast.cs | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Game/Localisation/ToastStrings.cs b/osu.Game/Localisation/ToastStrings.cs index 33027966dd..25899153f8 100644 --- a/osu.Game/Localisation/ToastStrings.cs +++ b/osu.Game/Localisation/ToastStrings.cs @@ -50,9 +50,9 @@ namespace osu.Game.Localisation public static LocalisableString UrlCopied => new TranslatableString(getKey(@"url_copied"), @"URL copied"); /// - /// "Speed Changed" + /// "Speed changed to" /// - public static LocalisableString SpeedChanged => new TranslatableString(getKey(@"speed_changed"), @"Speed Changed"); + public static LocalisableString SpeedChangedTo => new TranslatableString(getKey(@"speed_changed"), @"Speed changed to"); private static string getKey(string key) => $@"{prefix}:{key}"; } diff --git a/osu.Game/Overlays/OSD/SpeedChangeToast.cs b/osu.Game/Overlays/OSD/SpeedChangeToast.cs index 231ef86526..df4f825541 100644 --- a/osu.Game/Overlays/OSD/SpeedChangeToast.cs +++ b/osu.Game/Overlays/OSD/SpeedChangeToast.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Threading; using osu.Game.Configuration; using osu.Game.Input.Bindings; using osu.Game.Localisation; @@ -9,8 +10,8 @@ namespace osu.Game.Overlays.OSD { public partial class SpeedChangeToast : Toast { - public SpeedChangeToast(OsuConfigManager config, double delta) - : base(CommonStrings.Beatmaps, ToastStrings.SpeedChanged, config.LookupKeyBindings(GlobalAction.IncreaseModSpeed) + " / " + config.LookupKeyBindings(GlobalAction.DecreaseModSpeed)) + public SpeedChangeToast(OsuConfigManager config, double newSpeed) + : base(CommonStrings.Beatmaps, ToastStrings.SpeedChangedTo + " " + newSpeed.ToString(Thread.CurrentThread.CurrentCulture), config.LookupKeyBindings(GlobalAction.IncreaseModSpeed) + " / " + config.LookupKeyBindings(GlobalAction.DecreaseModSpeed)) { } } From abc67ebbaccfdc3d36ef1853f766829685e1308e Mon Sep 17 00:00:00 2001 From: Fabian van Oeffelt Date: Wed, 22 May 2024 13:59:26 +0200 Subject: [PATCH 041/119] Fix test not running due to floating point number inaccuacy --- .../SongSelect/TestScenePlaySongSelect.cs | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index 938b858110..af8b2a7760 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -94,25 +94,25 @@ namespace osu.Game.Tests.Visual.SongSelect changeMods(); AddStep("decreasing speed without mods", () => songSelect?.ChangeSpeed(-0.05)); - AddAssert("halftime at 0.95", () => songSelect!.Mods.Value.Single() is ModHalfTime mod && mod.SpeedChange.Value == 0.95); + AddAssert("halftime at 0.95", () => songSelect!.Mods.Value.Single() is ModHalfTime mod && Math.Round(mod.SpeedChange.Value, 2) == 0.95); AddStep("decreasing speed with halftime", () => songSelect?.ChangeSpeed(-0.05)); - AddAssert("halftime at 0.9", () => songSelect!.Mods.Value.Single() is ModHalfTime mod && mod.SpeedChange.Value == 0.9); + AddAssert("halftime at 0.9", () => songSelect!.Mods.Value.Single() is ModHalfTime mod && Math.Round(mod.SpeedChange.Value, 2) == 0.9); AddStep("increasing speed with halftime", () => songSelect?.ChangeSpeed(+0.05)); - AddAssert("halftime at 0.95", () => songSelect!.Mods.Value.Single() is ModHalfTime mod && mod.SpeedChange.Value == 0.95); + AddAssert("halftime at 0.95", () => songSelect!.Mods.Value.Single() is ModHalfTime mod && Math.Round(mod.SpeedChange.Value, 2) == 0.95); AddStep("increasing speed with halftime to nomod", () => songSelect?.ChangeSpeed(+0.05)); AddAssert("no mods selected", () => songSelect!.Mods.Value.Count == 0); AddStep("increasing speed without mods", () => songSelect?.ChangeSpeed(+0.05)); - AddAssert("doubletime at 1.05", () => songSelect!.Mods.Value.Single() is ModDoubleTime mod && mod.SpeedChange.Value == 1.05); + AddAssert("doubletime at 1.05", () => songSelect!.Mods.Value.Single() is ModDoubleTime mod && Math.Round(mod.SpeedChange.Value, 2) == 1.05); AddStep("increasing speed with doubletime", () => songSelect?.ChangeSpeed(+0.05)); - AddAssert("doubletime at 1.1", () => songSelect!.Mods.Value.Single() is ModDoubleTime mod && mod.SpeedChange.Value == 1.1); + AddAssert("doubletime at 1.1", () => songSelect!.Mods.Value.Single() is ModDoubleTime mod && Math.Round(mod.SpeedChange.Value, 2) == 1.1); AddStep("decreasing speed with doubletime", () => songSelect?.ChangeSpeed(-0.05)); - AddAssert("doubletime at 1.05", () => songSelect!.Mods.Value.Single() is ModDoubleTime mod && mod.SpeedChange.Value == 1.05); + AddAssert("doubletime at 1.05", () => songSelect!.Mods.Value.Single() is ModDoubleTime mod && Math.Round(mod.SpeedChange.Value, 2) == 1.05); OsuModNightcore nc = new OsuModNightcore { @@ -120,22 +120,22 @@ namespace osu.Game.Tests.Visual.SongSelect }; changeMods(nc); AddStep("increasing speed with nightcore", () => songSelect?.ChangeSpeed(+0.05)); - AddAssert("nightcore at 1.1", () => songSelect!.Mods.Value.Single() is ModNightcore mod && mod.SpeedChange.Value == 1.1); + AddAssert("nightcore at 1.1", () => songSelect!.Mods.Value.Single() is ModNightcore mod && Math.Round(mod.SpeedChange.Value, 2) == 1.1); AddStep("decreasing speed with nightcore", () => songSelect?.ChangeSpeed(-0.05)); - AddAssert("doubletime at 1.05", () => songSelect!.Mods.Value.Single() is ModNightcore mod && mod.SpeedChange.Value == 1.05); + AddAssert("doubletime at 1.05", () => songSelect!.Mods.Value.Single() is ModNightcore mod && Math.Round(mod.SpeedChange.Value, 2) == 1.05); AddStep("decreasing speed with nightcore to nomod", () => songSelect?.ChangeSpeed(-0.05)); AddAssert("no mods selected", () => songSelect!.Mods.Value.Count == 0); AddStep("decreasing speed nomod, nightcore was selected", () => songSelect?.ChangeSpeed(-0.05)); - AddAssert("daycore at 0.95", () => songSelect!.Mods.Value.Single() is ModDaycore mod && mod.SpeedChange.Value == 0.95); + AddAssert("daycore at 0.95", () => songSelect!.Mods.Value.Single() is ModDaycore mod && Math.Round(mod.SpeedChange.Value, 2) == 0.95); AddStep("decreasing speed with daycore", () => songSelect?.ChangeSpeed(-0.05)); - AddAssert("daycore at 0.9", () => songSelect!.Mods.Value.Single() is ModDaycore mod && mod.SpeedChange.Value == 0.9); + AddAssert("daycore at 0.9", () => songSelect!.Mods.Value.Single() is ModDaycore mod && Math.Round(mod.SpeedChange.Value, 2) == 0.9); AddStep("increasing speed with daycore", () => songSelect?.ChangeSpeed(0.05)); - AddAssert("daycore at 0.95", () => songSelect!.Mods.Value.Single() is ModDaycore mod && mod.SpeedChange.Value == 0.95); + AddAssert("daycore at 0.95", () => songSelect!.Mods.Value.Single() is ModDaycore mod && Math.Round(mod.SpeedChange.Value, 2) == 0.95); OsuModDoubleTime dt = new OsuModDoubleTime { @@ -144,7 +144,7 @@ namespace osu.Game.Tests.Visual.SongSelect }; changeMods(dt); AddStep("decreasing speed from doubletime 1.02 with adjustpitch enabled", () => songSelect?.ChangeSpeed(-0.05)); - AddAssert("halftime at 0.97 with adjustpitch enabled", () => songSelect!.Mods.Value.Single() is ModHalfTime mod && mod.SpeedChange.Value == 0.97 && mod.AdjustPitch.Value); + AddAssert("halftime at 0.97 with adjustpitch enabled", () => songSelect!.Mods.Value.Single() is ModHalfTime mod && Math.Round(mod.SpeedChange.Value, 2) == 0.97 && mod.AdjustPitch.Value); OsuModHalfTime ht = new OsuModHalfTime { @@ -154,7 +154,7 @@ namespace osu.Game.Tests.Visual.SongSelect Mod[] modlist = { ht, new OsuModHardRock(), new OsuModHidden() }; changeMods(modlist); AddStep("decreasing speed from halftime 0.97 with adjustpitch enabled, HDHR enabled", () => songSelect?.ChangeSpeed(0.05)); - AddAssert("doubletime at 1.02 with adjustpitch enabled, HDHR still enabled", () => songSelect!.Mods.Value.Count(mod => (mod is ModDoubleTime modDt && modDt.AdjustPitch.Value && modDt.SpeedChange.Value == 1.02) || mod is ModHardRock || mod is ModHidden) == 3); + AddAssert("doubletime at 1.02 with adjustpitch enabled, HDHR still enabled", () => songSelect!.Mods.Value.Count(mod => (mod is ModDoubleTime modDt && modDt.AdjustPitch.Value && Math.Round(modDt.SpeedChange.Value, 2) == 1.02) || mod is ModHardRock || mod is ModHidden) == 3); changeMods(new ModWindUp()); AddStep("windup active, trying to change speed", () => songSelect?.ChangeSpeed(0.05)); From 0df634574fc5b680e168d59966a2d537a2baa160 Mon Sep 17 00:00:00 2001 From: Fabian van Oeffelt Date: Wed, 22 May 2024 13:59:33 +0200 Subject: [PATCH 042/119] Improve readability --- osu.Game/Screens/Select/SongSelect.cs | 223 ++++++++++++++------------ 1 file changed, 119 insertions(+), 104 deletions(-) diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 257f6583a4..b3823d7a0f 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -823,123 +823,138 @@ namespace osu.Game.Screens.Select return false; } + private Mod getRateMod(ModType modType, Type type) + { + var modList = game.AvailableMods.Value[modType]; + var multiMod = (MultiMod)modList.First(mod => mod is MultiMod multiMod && multiMod.Mods.Count(mod2 => mod2.GetType().IsSubclassOf(type)) > 0); + var mod = multiMod.Mods.First(mod => mod.GetType().IsSubclassOf(type)); + return mod; + } + public void ChangeSpeed(double delta) { - ModNightcore modNc = (ModNightcore)((MultiMod)game.AvailableMods.Value[ModType.DifficultyIncrease].First(mod => mod is MultiMod multiMod && multiMod.Mods.Count(modType => modType is ModNightcore) > 0)).Mods.First(mod => mod is ModNightcore); - ModDoubleTime modDt = (ModDoubleTime)((MultiMod)game.AvailableMods.Value[ModType.DifficultyIncrease].First(mod => mod is MultiMod multiMod && multiMod.Mods.Count(modType => modType is ModDoubleTime) > 0)).Mods.First(mod => mod is ModDoubleTime); - ModDaycore modDc = (ModDaycore)((MultiMod)game.AvailableMods.Value[ModType.DifficultyReduction].First(mod => mod is MultiMod multiMod && multiMod.Mods.Count(modType => modType is ModDaycore) > 0)).Mods.First(mod => mod is ModDaycore); - ModHalfTime modHt = (ModHalfTime)((MultiMod)game.AvailableMods.Value[ModType.DifficultyReduction].First(mod => mod is MultiMod multiMod && multiMod.Mods.Count(modType => modType is ModHalfTime) > 0)).Mods.First(mod => mod is ModHalfTime); + ModNightcore modNc = (ModNightcore)getRateMod(ModType.DifficultyIncrease, typeof(ModNightcore)); + ModDoubleTime modDt = (ModDoubleTime)getRateMod(ModType.DifficultyIncrease, typeof(ModDoubleTime)); + ModDaycore modDc = (ModDaycore)getRateMod(ModType.DifficultyReduction, typeof(ModDaycore)); + ModHalfTime modHt = (ModHalfTime)getRateMod(ModType.DifficultyReduction, typeof(ModHalfTime)); bool rateModActive = selectedMods.Value.Count(mod => mod is ModRateAdjust) > 0; bool incompatibleModActive = selectedMods.Value.Count(mod => modDt.IncompatibleMods.Count(incompatibleMod => (mod.GetType().IsSubclassOf(incompatibleMod) || mod.GetType() == incompatibleMod) && incompatibleMod != typeof(ModRateAdjust)) > 0) > 0; - double newRate = 1d + delta; + double newRate = Math.Round(1d + delta, 2); bool isPositive = delta > 0; if (incompatibleModActive) return; - onScreenDisplay?.Display(new SpeedChangeToast(config, delta)); - - if (rateModActive) + if (!rateModActive) { - ModRateAdjust mod = (ModRateAdjust)selectedMods.Value.First(mod => mod is ModRateAdjust); + onScreenDisplay?.Display(new SpeedChangeToast(config, newRate)); - // Find current active rateAdjust mod and modify speed, enable HalfTime if necessary - newRate = mod.SpeedChange.Value + delta; - - if (newRate == 1.0) - { - lastPitchState = false; - usedPitchMods = false; - - if (mod is ModDoubleTime dtmod && dtmod.AdjustPitch.Value) lastPitchState = true; - - if (mod is ModHalfTime htmod && htmod.AdjustPitch.Value) lastPitchState = true; - - if (mod is ModNightcore || mod is ModDaycore) usedPitchMods = true; - - //Disable RateAdjustMods - selectedMods.Value = selectedMods.Value.Where(search => search is not ModRateAdjust).ToList(); - return; - } - - if (((mod is ModDoubleTime || mod is ModNightcore) && newRate < mod.SpeedChange.MinValue) - || ((mod is ModHalfTime || mod is ModDaycore) && newRate > mod.SpeedChange.MaxValue)) - { - bool adjustPitch = (mod is ModDoubleTime dtmod && dtmod.AdjustPitch.Value) || (mod is ModHalfTime htmod && htmod.AdjustPitch.Value); - - //Disable RateAdjustMods - selectedMods.Value = selectedMods.Value.Where(search => search is not ModRateAdjust).ToList(); - - ModRateAdjust? oppositeMod = null; - - switch (mod) - { - case ModDoubleTime: - modHt.AdjustPitch.Value = adjustPitch; - oppositeMod = modHt; - break; - - case ModHalfTime: - modDt.AdjustPitch.Value = adjustPitch; - oppositeMod = modDt; - break; - - case ModNightcore: - oppositeMod = modDc; - break; - - case ModDaycore: - oppositeMod = modNc; - break; - } - - if (oppositeMod == null) return; - - oppositeMod.SpeedChange.Value = newRate; - selectedMods.Value = selectedMods.Value.Append(oppositeMod).ToList(); - return; - } - - if (newRate > mod.SpeedChange.MaxValue && (mod is ModDoubleTime || mod is ModNightcore)) - newRate = mod.SpeedChange.MaxValue; - - if (newRate < mod.SpeedChange.MinValue && (mod is ModHalfTime || mod is ModDaycore)) - newRate = mod.SpeedChange.MinValue; - - mod.SpeedChange.Value = newRate; - } - else - { // If no ModRateAdjust is active, activate one - if (isPositive) - { - if (!usedPitchMods) - { - modDt.SpeedChange.Value = newRate; - modDt.AdjustPitch.Value = lastPitchState; - selectedMods.Value = selectedMods.Value.Append(modDt).ToList(); - } - else - { - modNc.SpeedChange.Value = newRate; - selectedMods.Value = selectedMods.Value.Append(modNc).ToList(); - } - } - else - { - if (!usedPitchMods) - { - modHt.SpeedChange.Value = newRate; - modHt.AdjustPitch.Value = lastPitchState; - selectedMods.Value = selectedMods.Value.Append(modHt).ToList(); - } - else - { - modDc.SpeedChange.Value = newRate; - selectedMods.Value = selectedMods.Value.Append(modDc).ToList(); - } - } + ModRateAdjust? newMod = null; + + if (isPositive && !usedPitchMods) + newMod = modDt; + + if (isPositive && usedPitchMods) + newMod = modNc; + + if (!isPositive && !usedPitchMods) + newMod = modHt; + + if (!isPositive && usedPitchMods) + newMod = modDc; + + if (!usedPitchMods && newMod is ModDoubleTime newModDt) + newModDt.AdjustPitch.Value = lastPitchState; + + if (!usedPitchMods && newMod is ModHalfTime newModHt) + newModHt.AdjustPitch.Value = lastPitchState; + + newMod!.SpeedChange.Value = newRate; + selectedMods.Value = selectedMods.Value.Append(newMod).ToList(); + return; } + + ModRateAdjust mod = (ModRateAdjust)selectedMods.Value.First(mod => mod is ModRateAdjust); + newRate = Math.Round(mod.SpeedChange.Value + delta, 2); + + // Disable RateAdjustMods if newRate is 1 + if (newRate == 1.0) + { + lastPitchState = false; + usedPitchMods = false; + + if (mod is ModDoubleTime dtmod && dtmod.AdjustPitch.Value) + lastPitchState = true; + + if (mod is ModHalfTime htmod && htmod.AdjustPitch.Value) + lastPitchState = true; + + if (mod is ModNightcore || mod is ModDaycore) + usedPitchMods = true; + + //Disable RateAdjustMods + selectedMods.Value = selectedMods.Value.Where(search => search is not ModRateAdjust).ToList(); + + onScreenDisplay?.Display(new SpeedChangeToast(config, newRate)); + + return; + } + + bool overMaxRateLimit = (mod is ModHalfTime || mod is ModDaycore) && newRate > mod.SpeedChange.MaxValue; + bool underMinRateLimit = (mod is ModDoubleTime || mod is ModNightcore) && newRate < mod.SpeedChange.MinValue; + + // Swap mod to opposite mod if newRate exceeds max/min speed values + if (overMaxRateLimit || underMinRateLimit) + { + bool adjustPitch = (mod is ModDoubleTime dtmod && dtmod.AdjustPitch.Value) || (mod is ModHalfTime htmod && htmod.AdjustPitch.Value); + + //Disable RateAdjustMods + selectedMods.Value = selectedMods.Value.Where(search => search is not ModRateAdjust).ToList(); + + ModRateAdjust? oppositeMod = null; + + switch (mod) + { + case ModDoubleTime: + modHt.AdjustPitch.Value = adjustPitch; + oppositeMod = modHt; + break; + + case ModHalfTime: + modDt.AdjustPitch.Value = adjustPitch; + oppositeMod = modDt; + break; + + case ModNightcore: + oppositeMod = modDc; + break; + + case ModDaycore: + oppositeMod = modNc; + break; + } + + if (oppositeMod == null) return; + + oppositeMod.SpeedChange.Value = newRate; + selectedMods.Value = selectedMods.Value.Append(oppositeMod).ToList(); + + onScreenDisplay?.Display(new SpeedChangeToast(config, newRate)); + + return; + } + + // Cap newRate to max/min values and change rate of current active mod + if (newRate > mod.SpeedChange.MaxValue && (mod is ModDoubleTime || mod is ModNightcore)) + newRate = mod.SpeedChange.MaxValue; + + if (newRate < mod.SpeedChange.MinValue && (mod is ModHalfTime || mod is ModDaycore)) + newRate = mod.SpeedChange.MinValue; + + mod.SpeedChange.Value = newRate; + + onScreenDisplay?.Display(new SpeedChangeToast(config, newRate)); } protected override void Dispose(bool isDisposing) From d47c4cb47946877f7ba00bfe0d497137638f8230 Mon Sep 17 00:00:00 2001 From: Aurelian Date: Fri, 24 May 2024 06:28:19 +0200 Subject: [PATCH 043/119] Test for scaling slider flat --- .../Editor/TestSliderScaling.cs | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSliderScaling.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSliderScaling.cs index 021fdba225..157a08df46 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSliderScaling.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSliderScaling.cs @@ -68,6 +68,119 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddAssert("slider length shrunk", () => slider.Path.Distance < distanceBefore); } + [Test] + [Timeout(4000)] //Catches crashes in other threads, but not ideal. Hopefully there is a improvement to this. + public void TestScalingSliderFlat( + [Values(0, 1, 2, 3)] int type_int + ) + { + Slider slider = null; + + switch (type_int) + { + case 0: + AddStep("Add linear slider", () => + { + slider = new Slider { StartTime = EditorClock.CurrentTime, Position = new Vector2(300) }; + + PathControlPoint[] points = + { + new PathControlPoint(new Vector2(0), PathType.LINEAR), + new PathControlPoint(new Vector2(50, 100)), + }; + + slider.Path = new SliderPath(points); + EditorBeatmap.Add(slider); + }); + break; + case 1: + AddStep("Add perfect curve slider", () => + { + slider = new Slider { StartTime = EditorClock.CurrentTime, Position = new Vector2(300) }; + + PathControlPoint[] points = + { + new PathControlPoint(new Vector2(0), PathType.PERFECT_CURVE), + new PathControlPoint(new Vector2(50, 25)), + new PathControlPoint(new Vector2(25, 100)), + }; + + slider.Path = new SliderPath(points); + EditorBeatmap.Add(slider); + }); + break; + case 2: + AddStep("Add bezier slider", () => + { + slider = new Slider { StartTime = EditorClock.CurrentTime, Position = new Vector2(300) }; + + PathControlPoint[] points = + { + new PathControlPoint(new Vector2(0), PathType.BEZIER), + new PathControlPoint(new Vector2(50, 25)), + new PathControlPoint(new Vector2(25, 80)), + new PathControlPoint(new Vector2(40, 100)), + }; + + slider.Path = new SliderPath(points); + EditorBeatmap.Add(slider); + }); + break; + AddStep("Add perfect curve slider", () => + { + slider = new Slider { StartTime = EditorClock.CurrentTime, Position = new Vector2(300) }; + + PathControlPoint[] points = + { + new PathControlPoint(new Vector2(0), PathType.PERFECT_CURVE), + new PathControlPoint(new Vector2(50, 25)), + new PathControlPoint(new Vector2(25, 100)), + }; + + slider.Path = new SliderPath(points); + EditorBeatmap.Add(slider); + }); + break; + case 3: + AddStep("Add catmull slider", () => + { + slider = new Slider { StartTime = EditorClock.CurrentTime, Position = new Vector2(300) }; + + PathControlPoint[] points = + { + new PathControlPoint(new Vector2(0), PathType.CATMULL), + new PathControlPoint(new Vector2(50, 25)), + new PathControlPoint(new Vector2(25, 80)), + new PathControlPoint(new Vector2(40, 100)), + }; + + slider.Path = new SliderPath(points); + EditorBeatmap.Add(slider); + }); + break; + } + + AddAssert("ensure object placed", () => EditorBeatmap.HitObjects.Count == 1); + + moveMouse(new Vector2(300)); + AddStep("select slider", () => InputManager.Click(MouseButton.Left)); + AddStep("slider is valid", () => slider.Path.GetSegmentEnds()); //To run ensureValid(); + + SelectionBoxDragHandle dragHandle = null!; + AddStep("store drag handle", () => dragHandle = Editor.ChildrenOfType().Skip(1).First()); + AddAssert("is dragHandle not null", () => dragHandle != null); + + AddStep("move mouse to handle", () => InputManager.MoveMouseTo(dragHandle)); + AddStep("begin drag", () => InputManager.PressButton(MouseButton.Left)); + moveMouse(new Vector2(0, 300)); + AddStep("end drag", () => InputManager.ReleaseButton(MouseButton.Left)); + + AddStep("move mouse to handle", () => InputManager.MoveMouseTo(dragHandle)); + AddStep("begin drag", () => InputManager.PressButton(MouseButton.Left)); + moveMouse(new Vector2(0, 300)); //Should crash here if broken, although doesn't count as failed... + AddStep("end drag", () => InputManager.ReleaseButton(MouseButton.Left)); + } + private void moveMouse(Vector2 pos) => AddStep($"move mouse to {pos}", () => InputManager.MoveMouseTo(playfield.ToScreenSpace(pos))); } From d948e0fc5c3af124d56b6d8bb80d2d904f73a704 Mon Sep 17 00:00:00 2001 From: Aurelian Date: Fri, 24 May 2024 08:26:17 +0200 Subject: [PATCH 044/119] Nearly straight sliders are treated as linear --- osu.Game/Rulesets/Objects/SliderPath.cs | 35 +++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/osu.Game/Rulesets/Objects/SliderPath.cs b/osu.Game/Rulesets/Objects/SliderPath.cs index e8e769e3fa..b0a5d02e71 100644 --- a/osu.Game/Rulesets/Objects/SliderPath.cs +++ b/osu.Game/Rulesets/Objects/SliderPath.cs @@ -269,6 +269,37 @@ namespace osu.Game.Rulesets.Objects pathCache.Validate(); } + /// + /// Checks if the array of vectors is almost straight. + /// + /// + /// The angle is first obtained based on the farthest vector from the first, + /// then we find the angle of each vector from the first, + /// and calculate the distance between the two angle vectors. + /// We than scale this distance to the distance from the first vector + /// (or by 10 if the distance is smaller), + /// and if it is greater than acceptableDifference, we return false. + /// + private static bool isAlmostStraight(Vector2[] vectors, float acceptableDifference = 0.1f) + { + if (vectors.Length <= 2 || vectors.All(x => x == vectors.First())) return true; + + Vector2 first = vectors.First(); + Vector2 farthest = vectors.MaxBy(x => Vector2.Distance(first, x)); + + Vector2 angle = Vector2.Normalize(farthest - first); + foreach (Vector2 vector in vectors) + { + if (vector == first) + continue; + + if (Math.Max(10.0f, Vector2.Distance(vector, first)) * Vector2.Distance(Vector2.Normalize(vector - first), angle) > acceptableDifference) + return false; + } + + return true; + } + private void calculatePath() { calculatedPath.Clear(); @@ -293,6 +324,10 @@ namespace osu.Game.Rulesets.Objects var segmentVertices = vertices.AsSpan().Slice(start, i - start + 1); var segmentType = ControlPoints[start].Type ?? PathType.LINEAR; + //If a segment is almost straight, treat it as linear. + if (segmentType != PathType.LINEAR && isAlmostStraight(segmentVertices.ToArray())) + segmentType = PathType.LINEAR; + // No need to calculate path when there is only 1 vertex if (segmentVertices.Length == 1) calculatedPath.Add(segmentVertices[0]); From 481883801fbdb8df3303d2cbec05fbe52cfdbdc9 Mon Sep 17 00:00:00 2001 From: Aurelian Date: Fri, 24 May 2024 08:55:10 +0200 Subject: [PATCH 045/119] Removed duplicate unreachable code --- .../Editor/TestSliderScaling.cs | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSliderScaling.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSliderScaling.cs index 157a08df46..44f85837dc 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSliderScaling.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSliderScaling.cs @@ -126,21 +126,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor EditorBeatmap.Add(slider); }); break; - AddStep("Add perfect curve slider", () => - { - slider = new Slider { StartTime = EditorClock.CurrentTime, Position = new Vector2(300) }; - - PathControlPoint[] points = - { - new PathControlPoint(new Vector2(0), PathType.PERFECT_CURVE), - new PathControlPoint(new Vector2(50, 25)), - new PathControlPoint(new Vector2(25, 100)), - }; - - slider.Path = new SliderPath(points); - EditorBeatmap.Add(slider); - }); - break; case 3: AddStep("Add catmull slider", () => { From fff52be59a9bc076aa30d7c9045dcf3acd1aff58 Mon Sep 17 00:00:00 2001 From: Aurelian Date: Fri, 24 May 2024 09:30:24 +0200 Subject: [PATCH 046/119] Addressed code quality issues --- .../Editor/TestSliderScaling.cs | 21 +++++++++++-------- osu.Game/Rulesets/Objects/SliderPath.cs | 5 +++-- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSliderScaling.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSliderScaling.cs index 44f85837dc..99694f82d1 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSliderScaling.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSliderScaling.cs @@ -71,12 +71,12 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor [Test] [Timeout(4000)] //Catches crashes in other threads, but not ideal. Hopefully there is a improvement to this. public void TestScalingSliderFlat( - [Values(0, 1, 2, 3)] int type_int + [Values(0, 1, 2, 3)] int typeInt ) { - Slider slider = null; + Slider slider = null!; - switch (type_int) + switch (typeInt) { case 0: AddStep("Add linear slider", () => @@ -93,6 +93,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor EditorBeatmap.Add(slider); }); break; + case 1: AddStep("Add perfect curve slider", () => { @@ -109,14 +110,15 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor EditorBeatmap.Add(slider); }); break; - case 2: - AddStep("Add bezier slider", () => + + case 3: + AddStep("Add catmull slider", () => { slider = new Slider { StartTime = EditorClock.CurrentTime, Position = new Vector2(300) }; PathControlPoint[] points = { - new PathControlPoint(new Vector2(0), PathType.BEZIER), + new PathControlPoint(new Vector2(0), PathType.CATMULL), new PathControlPoint(new Vector2(50, 25)), new PathControlPoint(new Vector2(25, 80)), new PathControlPoint(new Vector2(40, 100)), @@ -126,14 +128,15 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor EditorBeatmap.Add(slider); }); break; - case 3: - AddStep("Add catmull slider", () => + + default: + AddStep("Add bezier slider", () => { slider = new Slider { StartTime = EditorClock.CurrentTime, Position = new Vector2(300) }; PathControlPoint[] points = { - new PathControlPoint(new Vector2(0), PathType.CATMULL), + new PathControlPoint(new Vector2(0), PathType.BEZIER), new PathControlPoint(new Vector2(50, 25)), new PathControlPoint(new Vector2(25, 80)), new PathControlPoint(new Vector2(40, 100)), diff --git a/osu.Game/Rulesets/Objects/SliderPath.cs b/osu.Game/Rulesets/Objects/SliderPath.cs index b0a5d02e71..ddea23034c 100644 --- a/osu.Game/Rulesets/Objects/SliderPath.cs +++ b/osu.Game/Rulesets/Objects/SliderPath.cs @@ -274,9 +274,9 @@ namespace osu.Game.Rulesets.Objects /// /// /// The angle is first obtained based on the farthest vector from the first, - /// then we find the angle of each vector from the first, + /// then we find the angle of each vector from the first, /// and calculate the distance between the two angle vectors. - /// We than scale this distance to the distance from the first vector + /// We than scale this distance to the distance from the first vector /// (or by 10 if the distance is smaller), /// and if it is greater than acceptableDifference, we return false. /// @@ -288,6 +288,7 @@ namespace osu.Game.Rulesets.Objects Vector2 farthest = vectors.MaxBy(x => Vector2.Distance(first, x)); Vector2 angle = Vector2.Normalize(farthest - first); + foreach (Vector2 vector in vectors) { if (vector == first) From 9045ec24abc5e844da7cc646fcc7fcbb620e75bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 24 May 2024 12:04:09 +0200 Subject: [PATCH 047/119] Rewrite test --- .../SongSelect/TestScenePlaySongSelect.cs | 64 ++++++++++--------- 1 file changed, 34 insertions(+), 30 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index af8b2a7760..7f0c209215 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -93,49 +93,49 @@ namespace osu.Game.Tests.Visual.SongSelect createSongSelect(); changeMods(); - AddStep("decreasing speed without mods", () => songSelect?.ChangeSpeed(-0.05)); - AddAssert("halftime at 0.95", () => songSelect!.Mods.Value.Single() is ModHalfTime mod && Math.Round(mod.SpeedChange.Value, 2) == 0.95); + AddStep("decrease speed", () => songSelect?.ChangeSpeed(-0.05)); + AddAssert("half time activated at 0.95x", () => songSelect!.Mods.Value.OfType().Single().SpeedChange.Value, () => Is.EqualTo(0.95).Within(0.005)); - AddStep("decreasing speed with halftime", () => songSelect?.ChangeSpeed(-0.05)); - AddAssert("halftime at 0.9", () => songSelect!.Mods.Value.Single() is ModHalfTime mod && Math.Round(mod.SpeedChange.Value, 2) == 0.9); + AddStep("decrease speed", () => songSelect?.ChangeSpeed(-0.05)); + AddAssert("half time speed changed to 0.9x", () => songSelect!.Mods.Value.OfType().Single().SpeedChange.Value, () => Is.EqualTo(0.9).Within(0.005)); - AddStep("increasing speed with halftime", () => songSelect?.ChangeSpeed(+0.05)); - AddAssert("halftime at 0.95", () => songSelect!.Mods.Value.Single() is ModHalfTime mod && Math.Round(mod.SpeedChange.Value, 2) == 0.95); + AddStep("increase speed", () => songSelect?.ChangeSpeed(0.05)); + AddAssert("half time speed changed to 0.95x", () => songSelect!.Mods.Value.OfType().Single().SpeedChange.Value, () => Is.EqualTo(0.95).Within(0.005)); - AddStep("increasing speed with halftime to nomod", () => songSelect?.ChangeSpeed(+0.05)); + AddStep("increase speed", () => songSelect?.ChangeSpeed(0.05)); AddAssert("no mods selected", () => songSelect!.Mods.Value.Count == 0); - AddStep("increasing speed without mods", () => songSelect?.ChangeSpeed(+0.05)); - AddAssert("doubletime at 1.05", () => songSelect!.Mods.Value.Single() is ModDoubleTime mod && Math.Round(mod.SpeedChange.Value, 2) == 1.05); + AddStep("increase speed", () => songSelect?.ChangeSpeed(0.05)); + AddAssert("double time activated at 1.05x", () => songSelect!.Mods.Value.OfType().Single().SpeedChange.Value, () => Is.EqualTo(1.05).Within(0.005)); - AddStep("increasing speed with doubletime", () => songSelect?.ChangeSpeed(+0.05)); - AddAssert("doubletime at 1.1", () => songSelect!.Mods.Value.Single() is ModDoubleTime mod && Math.Round(mod.SpeedChange.Value, 2) == 1.1); + AddStep("increase speed", () => songSelect?.ChangeSpeed(0.05)); + AddAssert("double time speed changed to 1.1x", () => songSelect!.Mods.Value.OfType().Single().SpeedChange.Value, () => Is.EqualTo(1.1).Within(0.005)); - AddStep("decreasing speed with doubletime", () => songSelect?.ChangeSpeed(-0.05)); - AddAssert("doubletime at 1.05", () => songSelect!.Mods.Value.Single() is ModDoubleTime mod && Math.Round(mod.SpeedChange.Value, 2) == 1.05); + AddStep("decrease speed", () => songSelect?.ChangeSpeed(-0.05)); + AddAssert("double time speed changed to 1.05x", () => songSelect!.Mods.Value.OfType().Single().SpeedChange.Value, () => Is.EqualTo(1.05).Within(0.005)); OsuModNightcore nc = new OsuModNightcore { SpeedChange = { Value = 1.05 } }; changeMods(nc); - AddStep("increasing speed with nightcore", () => songSelect?.ChangeSpeed(+0.05)); - AddAssert("nightcore at 1.1", () => songSelect!.Mods.Value.Single() is ModNightcore mod && Math.Round(mod.SpeedChange.Value, 2) == 1.1); + AddStep("increase speed", () => songSelect?.ChangeSpeed(0.05)); + AddAssert("nightcore speed changed to 1.1x", () => songSelect!.Mods.Value.OfType().Single().SpeedChange.Value, () => Is.EqualTo(1.1).Within(0.005)); - AddStep("decreasing speed with nightcore", () => songSelect?.ChangeSpeed(-0.05)); - AddAssert("doubletime at 1.05", () => songSelect!.Mods.Value.Single() is ModNightcore mod && Math.Round(mod.SpeedChange.Value, 2) == 1.05); + AddStep("decrease speed", () => songSelect?.ChangeSpeed(-0.05)); + AddAssert("nightcore speed changed to 1.05x", () => songSelect!.Mods.Value.OfType().Single().SpeedChange.Value, () => Is.EqualTo(1.05).Within(0.005)); - AddStep("decreasing speed with nightcore to nomod", () => songSelect?.ChangeSpeed(-0.05)); + AddStep("decrease speed", () => songSelect?.ChangeSpeed(-0.05)); AddAssert("no mods selected", () => songSelect!.Mods.Value.Count == 0); - AddStep("decreasing speed nomod, nightcore was selected", () => songSelect?.ChangeSpeed(-0.05)); - AddAssert("daycore at 0.95", () => songSelect!.Mods.Value.Single() is ModDaycore mod && Math.Round(mod.SpeedChange.Value, 2) == 0.95); + AddStep("decrease speed", () => songSelect?.ChangeSpeed(-0.05)); + AddAssert("daycore activated at 0.95x", () => songSelect!.Mods.Value.OfType().Single().SpeedChange.Value, () => Is.EqualTo(0.95).Within(0.005)); - AddStep("decreasing speed with daycore", () => songSelect?.ChangeSpeed(-0.05)); - AddAssert("daycore at 0.9", () => songSelect!.Mods.Value.Single() is ModDaycore mod && Math.Round(mod.SpeedChange.Value, 2) == 0.9); + AddStep("decrease speed", () => songSelect?.ChangeSpeed(-0.05)); + AddAssert("daycore activated at 0.95x", () => songSelect!.Mods.Value.OfType().Single().SpeedChange.Value, () => Is.EqualTo(0.9).Within(0.005)); - AddStep("increasing speed with daycore", () => songSelect?.ChangeSpeed(0.05)); - AddAssert("daycore at 0.95", () => songSelect!.Mods.Value.Single() is ModDaycore mod && Math.Round(mod.SpeedChange.Value, 2) == 0.95); + AddStep("increase speed", () => songSelect?.ChangeSpeed(0.05)); + AddAssert("daycore activated at 0.95x", () => songSelect!.Mods.Value.OfType().Single().SpeedChange.Value, () => Is.EqualTo(0.95).Within(0.005)); OsuModDoubleTime dt = new OsuModDoubleTime { @@ -143,8 +143,9 @@ namespace osu.Game.Tests.Visual.SongSelect AdjustPitch = { Value = true }, }; changeMods(dt); - AddStep("decreasing speed from doubletime 1.02 with adjustpitch enabled", () => songSelect?.ChangeSpeed(-0.05)); - AddAssert("halftime at 0.97 with adjustpitch enabled", () => songSelect!.Mods.Value.Single() is ModHalfTime mod && Math.Round(mod.SpeedChange.Value, 2) == 0.97 && mod.AdjustPitch.Value); + AddStep("decrease speed", () => songSelect?.ChangeSpeed(-0.05)); + AddAssert("half time activated at 0.97x", () => songSelect!.Mods.Value.OfType().Single().SpeedChange.Value, () => Is.EqualTo(0.97).Within(0.005)); + AddAssert("adjust pitch preserved", () => songSelect!.Mods.Value.OfType().Single().AdjustPitch.Value, () => Is.True); OsuModHalfTime ht = new OsuModHalfTime { @@ -153,16 +154,19 @@ namespace osu.Game.Tests.Visual.SongSelect }; Mod[] modlist = { ht, new OsuModHardRock(), new OsuModHidden() }; changeMods(modlist); - AddStep("decreasing speed from halftime 0.97 with adjustpitch enabled, HDHR enabled", () => songSelect?.ChangeSpeed(0.05)); - AddAssert("doubletime at 1.02 with adjustpitch enabled, HDHR still enabled", () => songSelect!.Mods.Value.Count(mod => (mod is ModDoubleTime modDt && modDt.AdjustPitch.Value && Math.Round(modDt.SpeedChange.Value, 2) == 1.02) || mod is ModHardRock || mod is ModHidden) == 3); + AddStep("decrease speed", () => songSelect?.ChangeSpeed(0.05)); + AddAssert("double time activated at 1.02x", () => songSelect!.Mods.Value.OfType().Single().SpeedChange.Value, () => Is.EqualTo(1.02).Within(0.005)); + AddAssert("double time activated at 1.02x", () => songSelect!.Mods.Value.OfType().Single().AdjustPitch.Value, () => Is.True); + AddAssert("HD still enabled", () => songSelect!.Mods.Value.OfType().SingleOrDefault(), () => Is.Not.Null); + AddAssert("HR still enabled", () => songSelect!.Mods.Value.OfType().SingleOrDefault(), () => Is.Not.Null); changeMods(new ModWindUp()); AddStep("windup active, trying to change speed", () => songSelect?.ChangeSpeed(0.05)); AddAssert("windup still active", () => songSelect!.Mods.Value.First() is ModWindUp); changeMods(new ModAdaptiveSpeed()); - AddStep("adaptivespeed active, trying to change speed", () => songSelect?.ChangeSpeed(0.05)); - AddAssert("adaptivespeed still active", () => songSelect!.Mods.Value.First() is ModAdaptiveSpeed); + AddStep("adaptive speed active, trying to change speed", () => songSelect?.ChangeSpeed(0.05)); + AddAssert("adaptive speed still active", () => songSelect!.Mods.Value.First() is ModAdaptiveSpeed); } [Test] From 63406b6feb2215111626903bceef923ffb5ca46d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 24 May 2024 12:59:24 +0200 Subject: [PATCH 048/119] Rewrite implementation --- .../SongSelect/TestScenePlaySongSelect.cs | 51 ++++-- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 11 +- .../Screens/Select/ModSpeedHotkeyHandler.cs | 105 ++++++++++++ osu.Game/Screens/Select/SongSelect.cs | 152 +----------------- 4 files changed, 149 insertions(+), 170 deletions(-) create mode 100644 osu.Game/Screens/Select/ModSpeedHotkeyHandler.cs diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index 7f0c209215..6581ce0323 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -93,25 +93,25 @@ namespace osu.Game.Tests.Visual.SongSelect createSongSelect(); changeMods(); - AddStep("decrease speed", () => songSelect?.ChangeSpeed(-0.05)); + decreaseModSpeed(); AddAssert("half time activated at 0.95x", () => songSelect!.Mods.Value.OfType().Single().SpeedChange.Value, () => Is.EqualTo(0.95).Within(0.005)); - AddStep("decrease speed", () => songSelect?.ChangeSpeed(-0.05)); + decreaseModSpeed(); AddAssert("half time speed changed to 0.9x", () => songSelect!.Mods.Value.OfType().Single().SpeedChange.Value, () => Is.EqualTo(0.9).Within(0.005)); - AddStep("increase speed", () => songSelect?.ChangeSpeed(0.05)); + increaseModSpeed(); AddAssert("half time speed changed to 0.95x", () => songSelect!.Mods.Value.OfType().Single().SpeedChange.Value, () => Is.EqualTo(0.95).Within(0.005)); - AddStep("increase speed", () => songSelect?.ChangeSpeed(0.05)); + increaseModSpeed(); AddAssert("no mods selected", () => songSelect!.Mods.Value.Count == 0); - AddStep("increase speed", () => songSelect?.ChangeSpeed(0.05)); + increaseModSpeed(); AddAssert("double time activated at 1.05x", () => songSelect!.Mods.Value.OfType().Single().SpeedChange.Value, () => Is.EqualTo(1.05).Within(0.005)); - AddStep("increase speed", () => songSelect?.ChangeSpeed(0.05)); + increaseModSpeed(); AddAssert("double time speed changed to 1.1x", () => songSelect!.Mods.Value.OfType().Single().SpeedChange.Value, () => Is.EqualTo(1.1).Within(0.005)); - AddStep("decrease speed", () => songSelect?.ChangeSpeed(-0.05)); + decreaseModSpeed(); AddAssert("double time speed changed to 1.05x", () => songSelect!.Mods.Value.OfType().Single().SpeedChange.Value, () => Is.EqualTo(1.05).Within(0.005)); OsuModNightcore nc = new OsuModNightcore @@ -119,22 +119,23 @@ namespace osu.Game.Tests.Visual.SongSelect SpeedChange = { Value = 1.05 } }; changeMods(nc); - AddStep("increase speed", () => songSelect?.ChangeSpeed(0.05)); + + increaseModSpeed(); AddAssert("nightcore speed changed to 1.1x", () => songSelect!.Mods.Value.OfType().Single().SpeedChange.Value, () => Is.EqualTo(1.1).Within(0.005)); - AddStep("decrease speed", () => songSelect?.ChangeSpeed(-0.05)); + decreaseModSpeed(); AddAssert("nightcore speed changed to 1.05x", () => songSelect!.Mods.Value.OfType().Single().SpeedChange.Value, () => Is.EqualTo(1.05).Within(0.005)); - AddStep("decrease speed", () => songSelect?.ChangeSpeed(-0.05)); + decreaseModSpeed(); AddAssert("no mods selected", () => songSelect!.Mods.Value.Count == 0); - AddStep("decrease speed", () => songSelect?.ChangeSpeed(-0.05)); + decreaseModSpeed(); AddAssert("daycore activated at 0.95x", () => songSelect!.Mods.Value.OfType().Single().SpeedChange.Value, () => Is.EqualTo(0.95).Within(0.005)); - AddStep("decrease speed", () => songSelect?.ChangeSpeed(-0.05)); + decreaseModSpeed(); AddAssert("daycore activated at 0.95x", () => songSelect!.Mods.Value.OfType().Single().SpeedChange.Value, () => Is.EqualTo(0.9).Within(0.005)); - AddStep("increase speed", () => songSelect?.ChangeSpeed(0.05)); + increaseModSpeed(); AddAssert("daycore activated at 0.95x", () => songSelect!.Mods.Value.OfType().Single().SpeedChange.Value, () => Is.EqualTo(0.95).Within(0.005)); OsuModDoubleTime dt = new OsuModDoubleTime @@ -143,7 +144,8 @@ namespace osu.Game.Tests.Visual.SongSelect AdjustPitch = { Value = true }, }; changeMods(dt); - AddStep("decrease speed", () => songSelect?.ChangeSpeed(-0.05)); + + decreaseModSpeed(); AddAssert("half time activated at 0.97x", () => songSelect!.Mods.Value.OfType().Single().SpeedChange.Value, () => Is.EqualTo(0.97).Within(0.005)); AddAssert("adjust pitch preserved", () => songSelect!.Mods.Value.OfType().Single().AdjustPitch.Value, () => Is.True); @@ -154,19 +156,34 @@ namespace osu.Game.Tests.Visual.SongSelect }; Mod[] modlist = { ht, new OsuModHardRock(), new OsuModHidden() }; changeMods(modlist); - AddStep("decrease speed", () => songSelect?.ChangeSpeed(0.05)); + + increaseModSpeed(); AddAssert("double time activated at 1.02x", () => songSelect!.Mods.Value.OfType().Single().SpeedChange.Value, () => Is.EqualTo(1.02).Within(0.005)); AddAssert("double time activated at 1.02x", () => songSelect!.Mods.Value.OfType().Single().AdjustPitch.Value, () => Is.True); AddAssert("HD still enabled", () => songSelect!.Mods.Value.OfType().SingleOrDefault(), () => Is.Not.Null); AddAssert("HR still enabled", () => songSelect!.Mods.Value.OfType().SingleOrDefault(), () => Is.Not.Null); changeMods(new ModWindUp()); - AddStep("windup active, trying to change speed", () => songSelect?.ChangeSpeed(0.05)); + increaseModSpeed(); AddAssert("windup still active", () => songSelect!.Mods.Value.First() is ModWindUp); changeMods(new ModAdaptiveSpeed()); - AddStep("adaptive speed active, trying to change speed", () => songSelect?.ChangeSpeed(0.05)); + increaseModSpeed(); AddAssert("adaptive speed still active", () => songSelect!.Mods.Value.First() is ModAdaptiveSpeed); + + void increaseModSpeed() => AddStep("increase mod speed", () => + { + InputManager.PressKey(Key.ControlLeft); + InputManager.Key(Key.Up); + InputManager.ReleaseKey(Key.ControlLeft); + }); + + void decreaseModSpeed() => AddStep("decrease mod speed", () => + { + InputManager.PressKey(Key.ControlLeft); + InputManager.Key(Key.Down); + InputManager.ReleaseKey(Key.ControlLeft); + }); } [Test] diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index ad589e8fa9..f8c67f4a10 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -64,9 +64,6 @@ namespace osu.Game.Overlays.Mods private Func isValidMod = _ => true; - [Resolved] - private SongSelect? songSelect { get; set; } - /// /// A function determining whether each mod in the column should be displayed. /// A return value of means that the mod is not filtered and therefore its corresponding panel should be displayed. @@ -138,6 +135,7 @@ namespace osu.Game.Overlays.Mods private FillFlowContainer footerButtonFlow = null!; private FillFlowContainer footerContentFlow = null!; private DeselectAllModsButton deselectAllModsButton = null!; + private ModSpeedHotkeyHandler modSpeedHotkeyHandler = null!; private Container aboveColumnsContent = null!; private RankingInformationDisplay? rankingInformationDisplay; @@ -190,7 +188,8 @@ namespace osu.Game.Overlays.Mods Anchor = Anchor.BottomCentre, Origin = Anchor.BottomCentre, Height = 0 - } + }, + modSpeedHotkeyHandler = new ModSpeedHotkeyHandler(), }); MainAreaContent.AddRange(new Drawable[] @@ -758,11 +757,11 @@ namespace osu.Game.Overlays.Mods } case GlobalAction.IncreaseModSpeed: - songSelect?.ChangeSpeed(0.05); + modSpeedHotkeyHandler.ChangeSpeed(0.05, AllAvailableMods.Where(state => state.ValidForSelection.Value).Select(state => state.Mod)); return true; case GlobalAction.DecreaseModSpeed: - songSelect?.ChangeSpeed(-0.05); + modSpeedHotkeyHandler.ChangeSpeed(-0.05, AllAvailableMods.Where(state => state.ValidForSelection.Value).Select(state => state.Mod)); return true; } diff --git a/osu.Game/Screens/Select/ModSpeedHotkeyHandler.cs b/osu.Game/Screens/Select/ModSpeedHotkeyHandler.cs new file mode 100644 index 0000000000..af64002bcf --- /dev/null +++ b/osu.Game/Screens/Select/ModSpeedHotkeyHandler.cs @@ -0,0 +1,105 @@ +// 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.Framework.Graphics; +using osu.Framework.Utils; +using osu.Game.Configuration; +using osu.Game.Overlays; +using osu.Game.Overlays.OSD; +using osu.Game.Rulesets.Mods; +using osu.Game.Utils; + +namespace osu.Game.Screens.Select +{ + public partial class ModSpeedHotkeyHandler : Component + { + [Resolved] + private Bindable> selectedMods { get; set; } = null!; + + [Resolved] + private OsuConfigManager config { get; set; } = null!; + + [Resolved] + private OnScreenDisplay? onScreenDisplay { get; set; } + + private ModRateAdjust? lastActiveRateAdjustMod; + + protected override void LoadComplete() + { + base.LoadComplete(); + + selectedMods.BindValueChanged(val => + { + lastActiveRateAdjustMod = val.NewValue.OfType().SingleOrDefault() ?? lastActiveRateAdjustMod; + }, true); + } + + public bool ChangeSpeed(double delta, IEnumerable availableMods) + { + double targetSpeed = (selectedMods.Value.OfType().SingleOrDefault()?.SpeedChange.Value ?? 1) + delta; + + if (Precision.AlmostEquals(targetSpeed, 1, 0.005)) + { + selectedMods.Value = selectedMods.Value.Where(m => m is not ModRateAdjust).ToList(); + onScreenDisplay?.Display(new SpeedChangeToast(config, targetSpeed)); + return true; + } + + ModRateAdjust? targetMod; + + if (lastActiveRateAdjustMod is ModDaycore || lastActiveRateAdjustMod is ModNightcore) + { + targetMod = targetSpeed < 1 + ? availableMods.OfType().SingleOrDefault() + : availableMods.OfType().SingleOrDefault(); + } + else + { + targetMod = targetSpeed < 1 + ? availableMods.OfType().SingleOrDefault() + : availableMods.OfType().SingleOrDefault(); + } + + if (targetMod == null) + return false; + + // preserve other settings from latest rate adjust mod instance seen + if (lastActiveRateAdjustMod != null) + { + foreach (var (_, sourceProperty) in lastActiveRateAdjustMod.GetSettingsSourceProperties()) + { + if (sourceProperty.Name == nameof(ModRateAdjust.SpeedChange)) + continue; + + var targetProperty = targetMod.GetType().GetProperty(sourceProperty.Name); + + if (targetProperty == null) + continue; + + var targetBindable = (IBindable)targetProperty.GetValue(targetMod)!; + var sourceBindable = (IBindable)sourceProperty.GetValue(lastActiveRateAdjustMod)!; + + if (targetBindable.GetType() != sourceBindable.GetType()) + continue; + + lastActiveRateAdjustMod.CopyAdjustedSetting(targetBindable, sourceBindable); + } + } + + targetMod.SpeedChange.Value = targetSpeed; + + var intendedMods = selectedMods.Value.Where(m => m is not ModRateAdjust).Append(targetMod).ToList(); + + if (!ModUtils.CheckCompatibleSet(intendedMods)) + return false; + + selectedMods.Value = intendedMods; + onScreenDisplay?.Display(new SpeedChangeToast(config, targetMod.SpeedChange.Value)); + return true; + } + } +} diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index b3823d7a0f..14e3931fce 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -30,7 +30,6 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; using osu.Game.Overlays; using osu.Game.Overlays.Mods; -using osu.Game.Overlays.OSD; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Screens.Backgrounds; @@ -40,6 +39,7 @@ using osu.Game.Screens.Play; using osu.Game.Screens.Select.Details; using osu.Game.Screens.Select.Options; using osu.Game.Skinning; +using osu.Game.Utils; using osuTK; using osuTK.Graphics; using osuTK.Input; @@ -137,6 +137,7 @@ namespace osu.Game.Screens.Select private double audioFeedbackLastPlaybackTime; private IDisposable? modSelectOverlayRegistration; + private ModSpeedHotkeyHandler modSpeedHotkeyHandler = null!; private AdvancedStats advancedStats = null!; @@ -148,16 +149,6 @@ namespace osu.Game.Screens.Select private Bindable configBackgroundBlur = null!; - private bool lastPitchState; - - private bool usedPitchMods; - - [Resolved] - private OnScreenDisplay? onScreenDisplay { get; set; } - - [Resolved] - private OsuConfigManager config { get; set; } = null!; - [BackgroundDependencyLoader(true)] private void load(AudioManager audio, OsuColour colours, ManageCollectionsDialog? manageCollectionsDialog, DifficultyRecommender? recommender, OsuConfigManager config) { @@ -333,6 +324,7 @@ namespace osu.Game.Screens.Select { RelativeSizeAxes = Axes.Both, }, + modSpeedHotkeyHandler = new ModSpeedHotkeyHandler(), }); if (ShowFooter) @@ -823,140 +815,6 @@ namespace osu.Game.Screens.Select return false; } - private Mod getRateMod(ModType modType, Type type) - { - var modList = game.AvailableMods.Value[modType]; - var multiMod = (MultiMod)modList.First(mod => mod is MultiMod multiMod && multiMod.Mods.Count(mod2 => mod2.GetType().IsSubclassOf(type)) > 0); - var mod = multiMod.Mods.First(mod => mod.GetType().IsSubclassOf(type)); - return mod; - } - - public void ChangeSpeed(double delta) - { - ModNightcore modNc = (ModNightcore)getRateMod(ModType.DifficultyIncrease, typeof(ModNightcore)); - ModDoubleTime modDt = (ModDoubleTime)getRateMod(ModType.DifficultyIncrease, typeof(ModDoubleTime)); - ModDaycore modDc = (ModDaycore)getRateMod(ModType.DifficultyReduction, typeof(ModDaycore)); - ModHalfTime modHt = (ModHalfTime)getRateMod(ModType.DifficultyReduction, typeof(ModHalfTime)); - bool rateModActive = selectedMods.Value.Count(mod => mod is ModRateAdjust) > 0; - bool incompatibleModActive = selectedMods.Value.Count(mod => modDt.IncompatibleMods.Count(incompatibleMod => (mod.GetType().IsSubclassOf(incompatibleMod) || mod.GetType() == incompatibleMod) && incompatibleMod != typeof(ModRateAdjust)) > 0) > 0; - double newRate = Math.Round(1d + delta, 2); - bool isPositive = delta > 0; - - if (incompatibleModActive) - return; - - if (!rateModActive) - { - onScreenDisplay?.Display(new SpeedChangeToast(config, newRate)); - - // If no ModRateAdjust is active, activate one - ModRateAdjust? newMod = null; - - if (isPositive && !usedPitchMods) - newMod = modDt; - - if (isPositive && usedPitchMods) - newMod = modNc; - - if (!isPositive && !usedPitchMods) - newMod = modHt; - - if (!isPositive && usedPitchMods) - newMod = modDc; - - if (!usedPitchMods && newMod is ModDoubleTime newModDt) - newModDt.AdjustPitch.Value = lastPitchState; - - if (!usedPitchMods && newMod is ModHalfTime newModHt) - newModHt.AdjustPitch.Value = lastPitchState; - - newMod!.SpeedChange.Value = newRate; - selectedMods.Value = selectedMods.Value.Append(newMod).ToList(); - return; - } - - ModRateAdjust mod = (ModRateAdjust)selectedMods.Value.First(mod => mod is ModRateAdjust); - newRate = Math.Round(mod.SpeedChange.Value + delta, 2); - - // Disable RateAdjustMods if newRate is 1 - if (newRate == 1.0) - { - lastPitchState = false; - usedPitchMods = false; - - if (mod is ModDoubleTime dtmod && dtmod.AdjustPitch.Value) - lastPitchState = true; - - if (mod is ModHalfTime htmod && htmod.AdjustPitch.Value) - lastPitchState = true; - - if (mod is ModNightcore || mod is ModDaycore) - usedPitchMods = true; - - //Disable RateAdjustMods - selectedMods.Value = selectedMods.Value.Where(search => search is not ModRateAdjust).ToList(); - - onScreenDisplay?.Display(new SpeedChangeToast(config, newRate)); - - return; - } - - bool overMaxRateLimit = (mod is ModHalfTime || mod is ModDaycore) && newRate > mod.SpeedChange.MaxValue; - bool underMinRateLimit = (mod is ModDoubleTime || mod is ModNightcore) && newRate < mod.SpeedChange.MinValue; - - // Swap mod to opposite mod if newRate exceeds max/min speed values - if (overMaxRateLimit || underMinRateLimit) - { - bool adjustPitch = (mod is ModDoubleTime dtmod && dtmod.AdjustPitch.Value) || (mod is ModHalfTime htmod && htmod.AdjustPitch.Value); - - //Disable RateAdjustMods - selectedMods.Value = selectedMods.Value.Where(search => search is not ModRateAdjust).ToList(); - - ModRateAdjust? oppositeMod = null; - - switch (mod) - { - case ModDoubleTime: - modHt.AdjustPitch.Value = adjustPitch; - oppositeMod = modHt; - break; - - case ModHalfTime: - modDt.AdjustPitch.Value = adjustPitch; - oppositeMod = modDt; - break; - - case ModNightcore: - oppositeMod = modDc; - break; - - case ModDaycore: - oppositeMod = modNc; - break; - } - - if (oppositeMod == null) return; - - oppositeMod.SpeedChange.Value = newRate; - selectedMods.Value = selectedMods.Value.Append(oppositeMod).ToList(); - - onScreenDisplay?.Display(new SpeedChangeToast(config, newRate)); - - return; - } - - // Cap newRate to max/min values and change rate of current active mod - if (newRate > mod.SpeedChange.MaxValue && (mod is ModDoubleTime || mod is ModNightcore)) - newRate = mod.SpeedChange.MaxValue; - - if (newRate < mod.SpeedChange.MinValue && (mod is ModHalfTime || mod is ModDaycore)) - newRate = mod.SpeedChange.MinValue; - - mod.SpeedChange.Value = newRate; - - onScreenDisplay?.Display(new SpeedChangeToast(config, newRate)); - } - protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); @@ -1160,11 +1018,11 @@ namespace osu.Game.Screens.Select switch (e.Action) { case GlobalAction.IncreaseModSpeed: - ChangeSpeed(0.05); + modSpeedHotkeyHandler.ChangeSpeed(0.05, ModUtils.FlattenMods(game.AvailableMods.Value.SelectMany(kv => kv.Value))); return true; case GlobalAction.DecreaseModSpeed: - ChangeSpeed(-0.05); + modSpeedHotkeyHandler.ChangeSpeed(-0.05, ModUtils.FlattenMods(game.AvailableMods.Value.SelectMany(kv => kv.Value))); return true; } From 345fb60679c3017f75b1b2d9dd54c210fde50a25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 24 May 2024 13:08:17 +0200 Subject: [PATCH 049/119] Fix toast strings --- osu.Game/Localisation/ToastStrings.cs | 4 ++-- osu.Game/Overlays/OSD/SpeedChangeToast.cs | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/osu.Game/Localisation/ToastStrings.cs b/osu.Game/Localisation/ToastStrings.cs index 25899153f8..942540cfc5 100644 --- a/osu.Game/Localisation/ToastStrings.cs +++ b/osu.Game/Localisation/ToastStrings.cs @@ -50,9 +50,9 @@ namespace osu.Game.Localisation public static LocalisableString UrlCopied => new TranslatableString(getKey(@"url_copied"), @"URL copied"); /// - /// "Speed changed to" + /// "Speed changed to {0:N2}x" /// - public static LocalisableString SpeedChangedTo => new TranslatableString(getKey(@"speed_changed"), @"Speed changed to"); + public static LocalisableString SpeedChangedTo(double speed) => new TranslatableString(getKey(@"speed_changed"), @"Speed changed to {0:N2}x", speed); private static string getKey(string key) => $@"{prefix}:{key}"; } diff --git a/osu.Game/Overlays/OSD/SpeedChangeToast.cs b/osu.Game/Overlays/OSD/SpeedChangeToast.cs index df4f825541..49d3985b04 100644 --- a/osu.Game/Overlays/OSD/SpeedChangeToast.cs +++ b/osu.Game/Overlays/OSD/SpeedChangeToast.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Threading; using osu.Game.Configuration; using osu.Game.Input.Bindings; using osu.Game.Localisation; @@ -11,7 +10,7 @@ namespace osu.Game.Overlays.OSD public partial class SpeedChangeToast : Toast { public SpeedChangeToast(OsuConfigManager config, double newSpeed) - : base(CommonStrings.Beatmaps, ToastStrings.SpeedChangedTo + " " + newSpeed.ToString(Thread.CurrentThread.CurrentCulture), config.LookupKeyBindings(GlobalAction.IncreaseModSpeed) + " / " + config.LookupKeyBindings(GlobalAction.DecreaseModSpeed)) + : base(ModSelectOverlayStrings.ModCustomisation, ToastStrings.SpeedChangedTo(newSpeed), config.LookupKeyBindings(GlobalAction.IncreaseModSpeed) + " / " + config.LookupKeyBindings(GlobalAction.DecreaseModSpeed)) { } } From 8cac87e4960253eae950f68c88923ad6dcf622dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 24 May 2024 13:09:07 +0200 Subject: [PATCH 050/119] Fix speed controls in mod select overlay not handling repeat --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index f8c67f4a10..bc87bb4e3d 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -704,6 +704,17 @@ namespace osu.Game.Overlays.Mods public override bool OnPressed(KeyBindingPressEvent e) { + switch (e.Action) + { + case GlobalAction.IncreaseModSpeed: + modSpeedHotkeyHandler.ChangeSpeed(0.05, AllAvailableMods.Where(state => state.ValidForSelection.Value).Select(state => state.Mod)); + return true; + + case GlobalAction.DecreaseModSpeed: + modSpeedHotkeyHandler.ChangeSpeed(-0.05, AllAvailableMods.Where(state => state.ValidForSelection.Value).Select(state => state.Mod)); + return true; + } + if (e.Repeat) return false; @@ -755,14 +766,6 @@ namespace osu.Game.Overlays.Mods return true; } - - case GlobalAction.IncreaseModSpeed: - modSpeedHotkeyHandler.ChangeSpeed(0.05, AllAvailableMods.Where(state => state.ValidForSelection.Value).Select(state => state.Mod)); - return true; - - case GlobalAction.DecreaseModSpeed: - modSpeedHotkeyHandler.ChangeSpeed(-0.05, AllAvailableMods.Where(state => state.ValidForSelection.Value).Select(state => state.Mod)); - return true; } return base.OnPressed(e); From b1b207960a46337b0a64036d575a23b846f638c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 24 May 2024 13:09:44 +0200 Subject: [PATCH 051/119] Actually use return value --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 6 ++---- osu.Game/Screens/Select/SongSelect.cs | 6 ++---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index bc87bb4e3d..8489b06f47 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -707,12 +707,10 @@ namespace osu.Game.Overlays.Mods switch (e.Action) { case GlobalAction.IncreaseModSpeed: - modSpeedHotkeyHandler.ChangeSpeed(0.05, AllAvailableMods.Where(state => state.ValidForSelection.Value).Select(state => state.Mod)); - return true; + return modSpeedHotkeyHandler.ChangeSpeed(0.05, AllAvailableMods.Where(state => state.ValidForSelection.Value).Select(state => state.Mod)); case GlobalAction.DecreaseModSpeed: - modSpeedHotkeyHandler.ChangeSpeed(-0.05, AllAvailableMods.Where(state => state.ValidForSelection.Value).Select(state => state.Mod)); - return true; + return modSpeedHotkeyHandler.ChangeSpeed(-0.05, AllAvailableMods.Where(state => state.ValidForSelection.Value).Select(state => state.Mod)); } if (e.Repeat) diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 14e3931fce..269ca37ff5 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -1018,12 +1018,10 @@ namespace osu.Game.Screens.Select switch (e.Action) { case GlobalAction.IncreaseModSpeed: - modSpeedHotkeyHandler.ChangeSpeed(0.05, ModUtils.FlattenMods(game.AvailableMods.Value.SelectMany(kv => kv.Value))); - return true; + return modSpeedHotkeyHandler.ChangeSpeed(0.05, ModUtils.FlattenMods(game.AvailableMods.Value.SelectMany(kv => kv.Value))); case GlobalAction.DecreaseModSpeed: - modSpeedHotkeyHandler.ChangeSpeed(-0.05, ModUtils.FlattenMods(game.AvailableMods.Value.SelectMany(kv => kv.Value))); - return true; + return modSpeedHotkeyHandler.ChangeSpeed(-0.05, ModUtils.FlattenMods(game.AvailableMods.Value.SelectMany(kv => kv.Value))); } if (e.Repeat) From cab8cf741073959dc314516bac3e3fb63dcc262b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 24 May 2024 13:14:06 +0200 Subject: [PATCH 052/119] Move mod speed hotkey handler to user mod select overlay The very base class is the wrong place for it because `FreeModSelectOverlay` inherits from it, and that one has different assumptions and rules than "user" selection. In particular, in non-user selection, more than one rate adjust mod may be active, which breaks the mod speed hotkey's basic assumptions. --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 12 --------- .../Overlays/Mods/UserModSelectOverlay.cs | 26 +++++++++++++++++++ 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 8489b06f47..d2d7ace936 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -27,7 +27,6 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; using osu.Game.Localisation; using osu.Game.Rulesets.Mods; -using osu.Game.Screens.Select; using osu.Game.Utils; using osuTK; using osuTK.Input; @@ -135,7 +134,6 @@ namespace osu.Game.Overlays.Mods private FillFlowContainer footerButtonFlow = null!; private FillFlowContainer footerContentFlow = null!; private DeselectAllModsButton deselectAllModsButton = null!; - private ModSpeedHotkeyHandler modSpeedHotkeyHandler = null!; private Container aboveColumnsContent = null!; private RankingInformationDisplay? rankingInformationDisplay; @@ -189,7 +187,6 @@ namespace osu.Game.Overlays.Mods Origin = Anchor.BottomCentre, Height = 0 }, - modSpeedHotkeyHandler = new ModSpeedHotkeyHandler(), }); MainAreaContent.AddRange(new Drawable[] @@ -704,15 +701,6 @@ namespace osu.Game.Overlays.Mods public override bool OnPressed(KeyBindingPressEvent e) { - switch (e.Action) - { - case GlobalAction.IncreaseModSpeed: - return modSpeedHotkeyHandler.ChangeSpeed(0.05, AllAvailableMods.Where(state => state.ValidForSelection.Value).Select(state => state.Mod)); - - case GlobalAction.DecreaseModSpeed: - return modSpeedHotkeyHandler.ChangeSpeed(-0.05, AllAvailableMods.Where(state => state.ValidForSelection.Value).Select(state => state.Mod)); - } - if (e.Repeat) return false; diff --git a/osu.Game/Overlays/Mods/UserModSelectOverlay.cs b/osu.Game/Overlays/Mods/UserModSelectOverlay.cs index 49469b99f3..16d71e557b 100644 --- a/osu.Game/Overlays/Mods/UserModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/UserModSelectOverlay.cs @@ -3,18 +3,30 @@ using System.Collections.Generic; using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Input.Events; +using osu.Game.Input.Bindings; using osu.Game.Rulesets.Mods; +using osu.Game.Screens.Select; using osu.Game.Utils; namespace osu.Game.Overlays.Mods { public partial class UserModSelectOverlay : ModSelectOverlay { + private ModSpeedHotkeyHandler modSpeedHotkeyHandler = null!; + public UserModSelectOverlay(OverlayColourScheme colourScheme = OverlayColourScheme.Green) : base(colourScheme) { } + [BackgroundDependencyLoader] + private void load() + { + Add(modSpeedHotkeyHandler = new ModSpeedHotkeyHandler()); + } + protected override ModColumn CreateModColumn(ModType modType) => new UserModColumn(modType, false); protected override IReadOnlyList ComputeNewModsFromSelection(IReadOnlyList oldSelection, IReadOnlyList newSelection) @@ -38,6 +50,20 @@ namespace osu.Game.Overlays.Mods return modsAfterRemoval.ToList(); } + public override bool OnPressed(KeyBindingPressEvent e) + { + switch (e.Action) + { + case GlobalAction.IncreaseModSpeed: + return modSpeedHotkeyHandler.ChangeSpeed(0.05, AllAvailableMods.Where(state => state.ValidForSelection.Value).Select(state => state.Mod)); + + case GlobalAction.DecreaseModSpeed: + return modSpeedHotkeyHandler.ChangeSpeed(-0.05, AllAvailableMods.Where(state => state.ValidForSelection.Value).Select(state => state.Mod)); + } + + return base.OnPressed(e); + } + private partial class UserModColumn : ModColumn { public UserModColumn(ModType modType, bool allowIncompatibleSelection) From b2c4e0e951bb9fc5e1ba50ffe4429109f11b34cc Mon Sep 17 00:00:00 2001 From: Aurelian Date: Fri, 24 May 2024 14:05:56 +0200 Subject: [PATCH 053/119] Reworked linear line check, and optimized scaled flat slider test --- .../Editor/TestSliderScaling.cs | 147 ++++++------------ osu.Game/Rulesets/Objects/SliderPath.cs | 43 +---- 2 files changed, 53 insertions(+), 137 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSliderScaling.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSliderScaling.cs index 99694f82d1..ef3824b5b0 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSliderScaling.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSliderScaling.cs @@ -6,6 +6,7 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Testing; +using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; @@ -68,108 +69,52 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddAssert("slider length shrunk", () => slider.Path.Distance < distanceBefore); } - [Test] - [Timeout(4000)] //Catches crashes in other threads, but not ideal. Hopefully there is a improvement to this. - public void TestScalingSliderFlat( - [Values(0, 1, 2, 3)] int typeInt - ) - { - Slider slider = null!; - - switch (typeInt) - { - case 0: - AddStep("Add linear slider", () => - { - slider = new Slider { StartTime = EditorClock.CurrentTime, Position = new Vector2(300) }; - - PathControlPoint[] points = - { - new PathControlPoint(new Vector2(0), PathType.LINEAR), - new PathControlPoint(new Vector2(50, 100)), - }; - - slider.Path = new SliderPath(points); - EditorBeatmap.Add(slider); - }); - break; - - case 1: - AddStep("Add perfect curve slider", () => - { - slider = new Slider { StartTime = EditorClock.CurrentTime, Position = new Vector2(300) }; - - PathControlPoint[] points = - { - new PathControlPoint(new Vector2(0), PathType.PERFECT_CURVE), - new PathControlPoint(new Vector2(50, 25)), - new PathControlPoint(new Vector2(25, 100)), - }; - - slider.Path = new SliderPath(points); - EditorBeatmap.Add(slider); - }); - break; - - case 3: - AddStep("Add catmull slider", () => - { - slider = new Slider { StartTime = EditorClock.CurrentTime, Position = new Vector2(300) }; - - PathControlPoint[] points = - { - new PathControlPoint(new Vector2(0), PathType.CATMULL), - new PathControlPoint(new Vector2(50, 25)), - new PathControlPoint(new Vector2(25, 80)), - new PathControlPoint(new Vector2(40, 100)), - }; - - slider.Path = new SliderPath(points); - EditorBeatmap.Add(slider); - }); - break; - - default: - AddStep("Add bezier slider", () => - { - slider = new Slider { StartTime = EditorClock.CurrentTime, Position = new Vector2(300) }; - - PathControlPoint[] points = - { - new PathControlPoint(new Vector2(0), PathType.BEZIER), - new PathControlPoint(new Vector2(50, 25)), - new PathControlPoint(new Vector2(25, 80)), - new PathControlPoint(new Vector2(40, 100)), - }; - - slider.Path = new SliderPath(points); - EditorBeatmap.Add(slider); - }); - break; - } - - AddAssert("ensure object placed", () => EditorBeatmap.HitObjects.Count == 1); - - moveMouse(new Vector2(300)); - AddStep("select slider", () => InputManager.Click(MouseButton.Left)); - AddStep("slider is valid", () => slider.Path.GetSegmentEnds()); //To run ensureValid(); - - SelectionBoxDragHandle dragHandle = null!; - AddStep("store drag handle", () => dragHandle = Editor.ChildrenOfType().Skip(1).First()); - AddAssert("is dragHandle not null", () => dragHandle != null); - - AddStep("move mouse to handle", () => InputManager.MoveMouseTo(dragHandle)); - AddStep("begin drag", () => InputManager.PressButton(MouseButton.Left)); - moveMouse(new Vector2(0, 300)); - AddStep("end drag", () => InputManager.ReleaseButton(MouseButton.Left)); - - AddStep("move mouse to handle", () => InputManager.MoveMouseTo(dragHandle)); - AddStep("begin drag", () => InputManager.PressButton(MouseButton.Left)); - moveMouse(new Vector2(0, 300)); //Should crash here if broken, although doesn't count as failed... - AddStep("end drag", () => InputManager.ReleaseButton(MouseButton.Left)); - } - private void moveMouse(Vector2 pos) => AddStep($"move mouse to {pos}", () => InputManager.MoveMouseTo(playfield.ToScreenSpace(pos))); } + [TestFixture] + public class TestSliderNearLinearScaling + { + [Test] + public void TestScalingSliderFlat() + { + Slider sliderPerfect = new Slider + { + Position = new Vector2(300), + Path = new SliderPath( + [ + new PathControlPoint(new Vector2(0), PathType.PERFECT_CURVE), + new PathControlPoint(new Vector2(50, 25)), + new PathControlPoint(new Vector2(25, 100)), + ]) + }; + + Slider sliderBezier = new Slider + { + Position = new Vector2(300), + Path = new SliderPath( + [ + new PathControlPoint(new Vector2(0), PathType.BEZIER), + new PathControlPoint(new Vector2(50, 25)), + new PathControlPoint(new Vector2(25, 100)), + ]) + }; + + scaleSlider(sliderPerfect, new Vector2(0.000001f, 1)); + scaleSlider(sliderBezier, new Vector2(0.000001f, 1)); + + for (int i = 0; i < 100; i++) + { + Assert.True(Precision.AlmostEquals(sliderPerfect.Path.PositionAt(i / 100.0f), sliderBezier.Path.PositionAt(i / 100.0f))); + } + } + + private void scaleSlider(Slider slider, Vector2 scale) + { + for (int i = 0; i < slider.Path.ControlPoints.Count; i++) + { + slider.Path.ControlPoints[i].Position *= scale; + } + } + } } diff --git a/osu.Game/Rulesets/Objects/SliderPath.cs b/osu.Game/Rulesets/Objects/SliderPath.cs index ddea23034c..eca14269fe 100644 --- a/osu.Game/Rulesets/Objects/SliderPath.cs +++ b/osu.Game/Rulesets/Objects/SliderPath.cs @@ -269,38 +269,6 @@ namespace osu.Game.Rulesets.Objects pathCache.Validate(); } - /// - /// Checks if the array of vectors is almost straight. - /// - /// - /// The angle is first obtained based on the farthest vector from the first, - /// then we find the angle of each vector from the first, - /// and calculate the distance between the two angle vectors. - /// We than scale this distance to the distance from the first vector - /// (or by 10 if the distance is smaller), - /// and if it is greater than acceptableDifference, we return false. - /// - private static bool isAlmostStraight(Vector2[] vectors, float acceptableDifference = 0.1f) - { - if (vectors.Length <= 2 || vectors.All(x => x == vectors.First())) return true; - - Vector2 first = vectors.First(); - Vector2 farthest = vectors.MaxBy(x => Vector2.Distance(first, x)); - - Vector2 angle = Vector2.Normalize(farthest - first); - - foreach (Vector2 vector in vectors) - { - if (vector == first) - continue; - - if (Math.Max(10.0f, Vector2.Distance(vector, first)) * Vector2.Distance(Vector2.Normalize(vector - first), angle) > acceptableDifference) - return false; - } - - return true; - } - private void calculatePath() { calculatedPath.Clear(); @@ -325,10 +293,6 @@ namespace osu.Game.Rulesets.Objects var segmentVertices = vertices.AsSpan().Slice(start, i - start + 1); var segmentType = ControlPoints[start].Type ?? PathType.LINEAR; - //If a segment is almost straight, treat it as linear. - if (segmentType != PathType.LINEAR && isAlmostStraight(segmentVertices.ToArray())) - segmentType = PathType.LINEAR; - // No need to calculate path when there is only 1 vertex if (segmentVertices.Length == 1) calculatedPath.Add(segmentVertices[0]); @@ -366,6 +330,13 @@ namespace osu.Game.Rulesets.Objects if (subControlPoints.Length != 3) break; + //If a curve's theta range almost equals zero, the radius needed to have more than a + //floating point error difference is very large and results in a nearly straight path. + //Calculate it via a bezier aproximation instead. + //0.0005 corresponds with a radius of 8000 to have a more than 0.001 shift in the X value + if (Math.Abs(new CircularArcProperties(subControlPoints).ThetaRange) <= 0.0005d) + break; + List subPath = PathApproximator.CircularArcToPiecewiseLinear(subControlPoints); // If for some reason a circular arc could not be fit to the 3 given points, fall back to a numerically stable bezier approximation. From 1e90e5e38e22e43c7bbcbb6ee5d11d82aac77216 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sat, 25 May 2024 13:24:25 +0300 Subject: [PATCH 054/119] Use sb element path as a name --- osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs | 1 + osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs | 1 + 2 files changed, 2 insertions(+) diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs index fae9ec7f2e..f66f84af7a 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs @@ -83,6 +83,7 @@ namespace osu.Game.Storyboards.Drawables Origin = animation.Origin; Position = animation.InitialPosition; Loop = animation.LoopType == AnimationLoopType.LoopForever; + Name = animation.Path; LifetimeStart = animation.StartTime; LifetimeEnd = animation.EndTimeForDisplay; diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs index ec875219b6..c5d70ddecc 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs @@ -85,6 +85,7 @@ namespace osu.Game.Storyboards.Drawables Sprite = sprite; Origin = sprite.Origin; Position = sprite.InitialPosition; + Name = sprite.Path; LifetimeStart = sprite.StartTime; LifetimeEnd = sprite.EndTimeForDisplay; From 6aa92bcc4559ec52a85e8e026e579c03f1d7aca0 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sat, 25 May 2024 18:31:19 +0200 Subject: [PATCH 055/119] Add simple scale tool --- .../Edit/OsuHitObjectComposer.cs | 6 +- .../Edit/PreciseScalePopover.cs | 121 ++++++++++++++++++ .../Edit/TransformToolboxGroup.cs | 33 ++++- .../Input/Bindings/GlobalActionContainer.cs | 4 + .../GlobalActionKeyBindingStrings.cs | 5 + 5 files changed, 167 insertions(+), 2 deletions(-) create mode 100644 osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index 3ead61f64a..6f3ed9730e 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -101,7 +101,11 @@ namespace osu.Game.Rulesets.Osu.Edit RightToolbox.AddRange(new EditorToolboxGroup[] { - new TransformToolboxGroup { RotationHandler = BlueprintContainer.SelectionHandler.RotationHandler, }, + new TransformToolboxGroup + { + RotationHandler = BlueprintContainer.SelectionHandler.RotationHandler, + ScaleHandler = BlueprintContainer.SelectionHandler.ScaleHandler, + }, FreehandlSliderToolboxGroup } ); diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs new file mode 100644 index 0000000000..62408e223d --- /dev/null +++ b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs @@ -0,0 +1,121 @@ +// 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.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Rulesets.Osu.UI; +using osu.Game.Screens.Edit.Components.RadioButtons; +using osu.Game.Screens.Edit.Compose.Components; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Edit +{ + public partial class PreciseScalePopover : OsuPopover + { + private readonly SelectionScaleHandler scaleHandler; + + private readonly Bindable scaleInfo = new Bindable(new PreciseScaleInfo(1, ScaleOrigin.PlayfieldCentre, true, true)); + + private SliderWithTextBoxInput scaleInput = null!; + private EditorRadioButtonCollection scaleOrigin = null!; + + private RadioButton selectionCentreButton = null!; + + public PreciseScalePopover(SelectionScaleHandler scaleHandler) + { + this.scaleHandler = scaleHandler; + + AllowableAnchors = new[] { Anchor.CentreLeft, Anchor.CentreRight }; + } + + [BackgroundDependencyLoader] + private void load() + { + Child = new FillFlowContainer + { + Width = 220, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(20), + Children = new Drawable[] + { + scaleInput = new SliderWithTextBoxInput("Scale:") + { + Current = new BindableNumber + { + MinValue = 0.5f, + MaxValue = 2, + Precision = 0.001f, + Value = 1, + Default = 1, + }, + Instantaneous = true + }, + scaleOrigin = new EditorRadioButtonCollection + { + RelativeSizeAxes = Axes.X, + Items = new[] + { + new RadioButton("Playfield centre", + () => scaleInfo.Value = scaleInfo.Value with { Origin = ScaleOrigin.PlayfieldCentre }, + () => new SpriteIcon { Icon = FontAwesome.Regular.Square }), + selectionCentreButton = new RadioButton("Selection centre", + () => scaleInfo.Value = scaleInfo.Value with { Origin = ScaleOrigin.SelectionCentre }, + () => new SpriteIcon { Icon = FontAwesome.Solid.VectorSquare }) + } + } + } + }; + selectionCentreButton.Selected.DisabledChanged += isDisabled => + { + selectionCentreButton.TooltipText = isDisabled ? "Select more than one object to perform selection-based scaling." : string.Empty; + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + ScheduleAfterChildren(() => scaleInput.TakeFocus()); + scaleInput.Current.BindValueChanged(scale => scaleInfo.Value = scaleInfo.Value with { Scale = scale.NewValue }); + scaleOrigin.Items.First().Select(); + + scaleHandler.CanScaleX.BindValueChanged(e => + { + selectionCentreButton.Selected.Disabled = !e.NewValue; + }, true); + + scaleInfo.BindValueChanged(scale => + { + var newScale = new Vector2(scale.NewValue.XAxis ? scale.NewValue.Scale : 1, scale.NewValue.YAxis ? scale.NewValue.Scale : 1); + scaleHandler.Update(newScale, scale.NewValue.Origin == ScaleOrigin.PlayfieldCentre ? OsuPlayfield.BASE_SIZE / 2 : null); + }); + } + + protected override void PopIn() + { + base.PopIn(); + scaleHandler.Begin(); + } + + protected override void PopOut() + { + base.PopOut(); + + if (IsLoaded) + scaleHandler.Commit(); + } + } + + public enum ScaleOrigin + { + PlayfieldCentre, + SelectionCentre + } + + public record PreciseScaleInfo(float Scale, ScaleOrigin Origin, bool XAxis, bool YAxis); +} diff --git a/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs b/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs index 9499bacade..146d771e19 100644 --- a/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs +++ b/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs @@ -19,13 +19,20 @@ namespace osu.Game.Rulesets.Osu.Edit public partial class TransformToolboxGroup : EditorToolboxGroup, IKeyBindingHandler { private readonly Bindable canRotate = new BindableBool(); + private readonly Bindable canScale = new BindableBool(); private EditorToolButton rotateButton = null!; + private EditorToolButton scaleButton = null!; private Bindable canRotatePlayfieldOrigin = null!; private Bindable canRotateSelectionOrigin = null!; + private Bindable canScaleX = null!; + private Bindable canScaleY = null!; + private Bindable canScaleDiagonally = null!; + public SelectionRotationHandler RotationHandler { get; init; } = null!; + public SelectionScaleHandler ScaleHandler { get; init; } = null!; public TransformToolboxGroup() : base("transform") @@ -45,7 +52,9 @@ namespace osu.Game.Rulesets.Osu.Edit rotateButton = new EditorToolButton("Rotate", () => new SpriteIcon { Icon = FontAwesome.Solid.Undo }, () => new PreciseRotationPopover(RotationHandler)), - // TODO: scale + scaleButton = new EditorToolButton("Scale", + () => new SpriteIcon { Icon = FontAwesome.Solid.ArrowsAlt }, + () => new PreciseScalePopover(ScaleHandler)) } }; } @@ -66,9 +75,25 @@ namespace osu.Game.Rulesets.Osu.Edit canRotate.Value = RotationHandler.CanRotatePlayfieldOrigin.Value || RotationHandler.CanRotateSelectionOrigin.Value; } + // aggregate three values into canScale + canScaleX = ScaleHandler.CanScaleX.GetBoundCopy(); + canScaleX.BindValueChanged(_ => updateCanScaleAggregate()); + + canScaleY = ScaleHandler.CanScaleY.GetBoundCopy(); + canScaleY.BindValueChanged(_ => updateCanScaleAggregate()); + + canScaleDiagonally = ScaleHandler.CanScaleDiagonally.GetBoundCopy(); + canScaleDiagonally.BindValueChanged(_ => updateCanScaleAggregate()); + + void updateCanScaleAggregate() + { + canScale.Value = ScaleHandler.CanScaleX.Value || ScaleHandler.CanScaleY.Value || ScaleHandler.CanScaleDiagonally.Value; + } + // bindings to `Enabled` on the buttons are decoupled on purpose // due to the weird `OsuButton` behaviour of resetting `Enabled` to `false` when `Action` is set. canRotate.BindValueChanged(_ => rotateButton.Enabled.Value = canRotate.Value, true); + canScale.BindValueChanged(_ => scaleButton.Enabled.Value = canScale.Value, true); } public bool OnPressed(KeyBindingPressEvent e) @@ -82,6 +107,12 @@ namespace osu.Game.Rulesets.Osu.Edit rotateButton.TriggerClick(); return true; } + + case GlobalAction.EditorToggleScaleControl: + { + scaleButton.TriggerClick(); + return true; + } } return false; diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index 09db7461d6..394cb98089 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -142,6 +142,7 @@ namespace osu.Game.Input.Bindings new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.MouseWheelRight }, GlobalAction.EditorCyclePreviousBeatSnapDivisor), new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.MouseWheelLeft }, GlobalAction.EditorCycleNextBeatSnapDivisor), new KeyBinding(new[] { InputKey.Control, InputKey.R }, GlobalAction.EditorToggleRotateControl), + new KeyBinding(new[] { InputKey.S }, GlobalAction.EditorToggleScaleControl), }; private static IEnumerable inGameKeyBindings => new[] @@ -411,6 +412,9 @@ namespace osu.Game.Input.Bindings [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorToggleRotateControl))] EditorToggleRotateControl, + [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorToggleScaleControl))] + EditorToggleScaleControl, + [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.IncreaseOffset))] IncreaseOffset, diff --git a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs index 18a1d3e4fe..2e44b96625 100644 --- a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs +++ b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs @@ -369,6 +369,11 @@ namespace osu.Game.Localisation /// public static LocalisableString EditorToggleRotateControl => new TranslatableString(getKey(@"editor_toggle_rotate_control"), @"Toggle rotate control"); + /// + /// "Toggle scale control" + /// + public static LocalisableString EditorToggleScaleControl => new TranslatableString(getKey(@"editor_toggle_scale_control"), @"Toggle scale control"); + /// /// "Increase mod speed" /// From 88314dc584f186cf8a99c631d16047f321135292 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sat, 25 May 2024 18:41:31 +0200 Subject: [PATCH 056/119] select all input text on popup for an easy typing experience --- osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs | 6 +++++- osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs | 6 +++++- osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs | 2 ++ osu.Game/Graphics/UserInterfaceV2/SliderWithTextBoxInput.cs | 2 ++ 4 files changed, 14 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs index 88c3d7414b..812d622ae5 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs @@ -78,7 +78,11 @@ namespace osu.Game.Rulesets.Osu.Edit { base.LoadComplete(); - ScheduleAfterChildren(() => angleInput.TakeFocus()); + ScheduleAfterChildren(() => + { + angleInput.TakeFocus(); + angleInput.SelectAll(); + }); angleInput.Current.BindValueChanged(angle => rotationInfo.Value = rotationInfo.Value with { Degrees = angle.NewValue }); rotationOrigin.Items.First().Select(); diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs index 62408e223d..ed52da56d3 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs @@ -80,7 +80,11 @@ namespace osu.Game.Rulesets.Osu.Edit { base.LoadComplete(); - ScheduleAfterChildren(() => scaleInput.TakeFocus()); + ScheduleAfterChildren(() => + { + scaleInput.TakeFocus(); + scaleInput.SelectAll(); + }); scaleInput.Current.BindValueChanged(scale => scaleInfo.Value = scaleInfo.Value with { Scale = scale.NewValue }); scaleOrigin.Items.First().Select(); diff --git a/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs b/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs index 863ad5a173..8dfe729ce7 100644 --- a/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs +++ b/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs @@ -50,6 +50,8 @@ namespace osu.Game.Graphics.UserInterfaceV2 Component.BorderColour = colours.Blue; } + public bool SelectAll() => Component.SelectAll(); + protected virtual OsuTextBox CreateTextBox() => new OsuTextBox(); public override bool AcceptsFocus => true; diff --git a/osu.Game/Graphics/UserInterfaceV2/SliderWithTextBoxInput.cs b/osu.Game/Graphics/UserInterfaceV2/SliderWithTextBoxInput.cs index 4c16cb4951..f1f4fe3b46 100644 --- a/osu.Game/Graphics/UserInterfaceV2/SliderWithTextBoxInput.cs +++ b/osu.Game/Graphics/UserInterfaceV2/SliderWithTextBoxInput.cs @@ -87,6 +87,8 @@ namespace osu.Game.Graphics.UserInterfaceV2 public bool TakeFocus() => GetContainingFocusManager().ChangeFocus(textBox); + public bool SelectAll() => textBox.SelectAll(); + private bool updatingFromTextBox; private void textChanged(ValueChangedEvent change) From 4eeebdf60cd70eedcf6692cbad36cc8df6abc8de Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sat, 25 May 2024 20:17:27 +0200 Subject: [PATCH 057/119] calculate max scale bounds for scale slider --- .../Edit/OsuSelectionScaleHandler.cs | 47 ++++++++++--------- .../Edit/PreciseScalePopover.cs | 35 ++++++++++++-- .../Components/SelectionScaleHandler.cs | 8 ++++ 3 files changed, 64 insertions(+), 26 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs index af03c4d925..331e8de3f1 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs @@ -67,7 +67,7 @@ namespace osu.Game.Rulesets.Osu.Edit objectsInScale = selectedMovableObjects.ToDictionary(ho => ho, ho => new OriginalHitObjectState(ho)); OriginalSurroundingQuad = objectsInScale.Count == 1 && objectsInScale.First().Key is Slider slider - ? GeometryUtils.GetSurroundingQuad(slider.Path.ControlPoints.Select(p => p.Position)) + ? GeometryUtils.GetSurroundingQuad(slider.Path.ControlPoints.Select(p => slider.Position + p.Position)) : GeometryUtils.GetSurroundingQuad(objectsInScale.Keys); defaultOrigin = OriginalSurroundingQuad.Value.Centre; } @@ -92,7 +92,7 @@ namespace osu.Game.Rulesets.Osu.Edit } else { - scale = getClampedScale(OriginalSurroundingQuad.Value, actualOrigin, scale); + scale = GetClampedScale(scale, actualOrigin); foreach (var (ho, originalState) in objectsInScale) { @@ -155,30 +155,33 @@ namespace osu.Game.Rulesets.Osu.Edit return (xInBounds, yInBounds); } - /// - /// Clamp scale for multi-object-scaling where selection does not exceed playfield bounds or flip. - /// - /// The quad surrounding the hitobjects - /// The origin from which the scale operation is performed - /// The scale to be clamped - /// The clamped scale vector - private Vector2 getClampedScale(Quad selectionQuad, Vector2 origin, Vector2 scale) + public override Vector2 GetClampedScale(Vector2 scale, Vector2? origin = null) { //todo: this is not always correct for selections involving sliders. This approximation assumes each point is scaled independently, but sliderends move with the sliderhead. + if (objectsInScale == null) + return scale; - var tl1 = Vector2.Divide(-origin, selectionQuad.TopLeft - origin); - var tl2 = Vector2.Divide(OsuPlayfield.BASE_SIZE - origin, selectionQuad.TopLeft - origin); - var br1 = Vector2.Divide(-origin, selectionQuad.BottomRight - origin); - var br2 = Vector2.Divide(OsuPlayfield.BASE_SIZE - origin, selectionQuad.BottomRight - origin); + Debug.Assert(defaultOrigin != null && OriginalSurroundingQuad != null); - if (!Precision.AlmostEquals(selectionQuad.TopLeft.X - origin.X, 0)) - scale.X = selectionQuad.TopLeft.X - origin.X < 0 ? MathHelper.Clamp(scale.X, tl2.X, tl1.X) : MathHelper.Clamp(scale.X, tl1.X, tl2.X); - if (!Precision.AlmostEquals(selectionQuad.TopLeft.Y - origin.Y, 0)) - scale.Y = selectionQuad.TopLeft.Y - origin.Y < 0 ? MathHelper.Clamp(scale.Y, tl2.Y, tl1.Y) : MathHelper.Clamp(scale.Y, tl1.Y, tl2.Y); - if (!Precision.AlmostEquals(selectionQuad.BottomRight.X - origin.X, 0)) - scale.X = selectionQuad.BottomRight.X - origin.X < 0 ? MathHelper.Clamp(scale.X, br2.X, br1.X) : MathHelper.Clamp(scale.X, br1.X, br2.X); - if (!Precision.AlmostEquals(selectionQuad.BottomRight.Y - origin.Y, 0)) - scale.Y = selectionQuad.BottomRight.Y - origin.Y < 0 ? MathHelper.Clamp(scale.Y, br2.Y, br1.Y) : MathHelper.Clamp(scale.Y, br1.Y, br2.Y); + if (objectsInScale.Count == 1 && objectsInScale.First().Key is Slider slider) + origin = slider.Position; + + Vector2 actualOrigin = origin ?? defaultOrigin.Value; + var selectionQuad = OriginalSurroundingQuad.Value; + + var tl1 = Vector2.Divide(-actualOrigin, selectionQuad.TopLeft - actualOrigin); + var tl2 = Vector2.Divide(OsuPlayfield.BASE_SIZE - actualOrigin, selectionQuad.TopLeft - actualOrigin); + var br1 = Vector2.Divide(-actualOrigin, selectionQuad.BottomRight - actualOrigin); + var br2 = Vector2.Divide(OsuPlayfield.BASE_SIZE - actualOrigin, selectionQuad.BottomRight - actualOrigin); + + if (!Precision.AlmostEquals(selectionQuad.TopLeft.X - actualOrigin.X, 0)) + scale.X = selectionQuad.TopLeft.X - actualOrigin.X < 0 ? MathHelper.Clamp(scale.X, tl2.X, tl1.X) : MathHelper.Clamp(scale.X, tl1.X, tl2.X); + if (!Precision.AlmostEquals(selectionQuad.TopLeft.Y - actualOrigin.Y, 0)) + scale.Y = selectionQuad.TopLeft.Y - actualOrigin.Y < 0 ? MathHelper.Clamp(scale.Y, tl2.Y, tl1.Y) : MathHelper.Clamp(scale.Y, tl1.Y, tl2.Y); + if (!Precision.AlmostEquals(selectionQuad.BottomRight.X - actualOrigin.X, 0)) + scale.X = selectionQuad.BottomRight.X - actualOrigin.X < 0 ? MathHelper.Clamp(scale.X, br2.X, br1.X) : MathHelper.Clamp(scale.X, br1.X, br2.X); + if (!Precision.AlmostEquals(selectionQuad.BottomRight.Y - actualOrigin.Y, 0)) + scale.Y = selectionQuad.BottomRight.Y - actualOrigin.Y < 0 ? MathHelper.Clamp(scale.Y, br2.Y, br1.Y) : MathHelper.Clamp(scale.Y, br1.Y, br2.Y); return Vector2.ComponentMax(scale, new Vector2(Precision.FLOAT_EPSILON)); } diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs index ed52da56d3..50195ebd1e 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -22,6 +23,7 @@ namespace osu.Game.Rulesets.Osu.Edit private readonly Bindable scaleInfo = new Bindable(new PreciseScaleInfo(1, ScaleOrigin.PlayfieldCentre, true, true)); private SliderWithTextBoxInput scaleInput = null!; + private BindableNumber scaleInputBindable = null!; private EditorRadioButtonCollection scaleOrigin = null!; private RadioButton selectionCentreButton = null!; @@ -45,7 +47,7 @@ namespace osu.Game.Rulesets.Osu.Edit { scaleInput = new SliderWithTextBoxInput("Scale:") { - Current = new BindableNumber + Current = scaleInputBindable = new BindableNumber { MinValue = 0.5f, MaxValue = 2, @@ -61,10 +63,10 @@ namespace osu.Game.Rulesets.Osu.Edit Items = new[] { new RadioButton("Playfield centre", - () => scaleInfo.Value = scaleInfo.Value with { Origin = ScaleOrigin.PlayfieldCentre }, + () => setOrigin(ScaleOrigin.PlayfieldCentre), () => new SpriteIcon { Icon = FontAwesome.Regular.Square }), selectionCentreButton = new RadioButton("Selection centre", - () => scaleInfo.Value = scaleInfo.Value with { Origin = ScaleOrigin.SelectionCentre }, + () => setOrigin(ScaleOrigin.SelectionCentre), () => new SpriteIcon { Icon = FontAwesome.Solid.VectorSquare }) } } @@ -96,14 +98,39 @@ namespace osu.Game.Rulesets.Osu.Edit scaleInfo.BindValueChanged(scale => { var newScale = new Vector2(scale.NewValue.XAxis ? scale.NewValue.Scale : 1, scale.NewValue.YAxis ? scale.NewValue.Scale : 1); - scaleHandler.Update(newScale, scale.NewValue.Origin == ScaleOrigin.PlayfieldCentre ? OsuPlayfield.BASE_SIZE / 2 : null); + scaleHandler.Update(newScale, getOriginPosition(scale.NewValue)); }); } + private void updateMaxScale() + { + if (!scaleHandler.OriginalSurroundingQuad.HasValue) + return; + + const float max_scale = 10; + var scale = scaleHandler.GetClampedScale(new Vector2(max_scale), getOriginPosition(scaleInfo.Value)); + + if (!scaleInfo.Value.XAxis) + scale.X = max_scale; + if (!scaleInfo.Value.YAxis) + scale.Y = max_scale; + + scaleInputBindable.MaxValue = MathF.Max(1, MathF.Min(scale.X, scale.Y)); + } + + private void setOrigin(ScaleOrigin origin) + { + scaleInfo.Value = scaleInfo.Value with { Origin = origin }; + updateMaxScale(); + } + + private Vector2? getOriginPosition(PreciseScaleInfo scale) => scale.Origin == ScaleOrigin.PlayfieldCentre ? OsuPlayfield.BASE_SIZE / 2 : null; + protected override void PopIn() { base.PopIn(); scaleHandler.Begin(); + updateMaxScale(); } protected override void PopOut() diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionScaleHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionScaleHandler.cs index a96f627e56..fb421c2329 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionScaleHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionScaleHandler.cs @@ -34,6 +34,14 @@ namespace osu.Game.Screens.Edit.Compose.Components public Quad? OriginalSurroundingQuad { get; protected set; } + /// + /// Clamp scale where selection does not exceed playfield bounds or flip. + /// + /// The origin from which the scale operation is performed + /// The scale to be clamped + /// The clamped scale vector + public virtual Vector2 GetClampedScale(Vector2 scale, Vector2? origin = null) => scale; + /// /// Performs a single, instant, atomic scale operation. /// From 37530eebccdc6b7bacee3742946830ac61e2e815 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sat, 25 May 2024 20:35:06 +0200 Subject: [PATCH 058/119] Enable scale buttons at the correct times --- osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs | 2 ++ osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs | 2 +- osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs | 8 ++++---- .../Edit/Compose/Components/SelectionScaleHandler.cs | 10 ++++++++++ 4 files changed, 17 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs index 331e8de3f1..e45494977f 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs @@ -53,6 +53,8 @@ namespace osu.Game.Rulesets.Osu.Edit CanScaleX.Value = quad.Width > 0; CanScaleY.Value = quad.Height > 0; CanScaleDiagonally.Value = CanScaleX.Value && CanScaleY.Value; + CanScaleSelectionOrigin.Value = CanScaleX.Value || CanScaleY.Value; + CanScalePlayfieldOrigin.Value = selectedMovableObjects.Any(); } private Dictionary? objectsInScale; diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs index 50195ebd1e..355064f000 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs @@ -90,7 +90,7 @@ namespace osu.Game.Rulesets.Osu.Edit scaleInput.Current.BindValueChanged(scale => scaleInfo.Value = scaleInfo.Value with { Scale = scale.NewValue }); scaleOrigin.Items.First().Select(); - scaleHandler.CanScaleX.BindValueChanged(e => + scaleHandler.CanScaleSelectionOrigin.BindValueChanged(e => { selectionCentreButton.Selected.Disabled = !e.NewValue; }, true); diff --git a/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs b/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs index 146d771e19..e1f53846dc 100644 --- a/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs +++ b/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs @@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Osu.Edit private Bindable canScaleX = null!; private Bindable canScaleY = null!; - private Bindable canScaleDiagonally = null!; + private Bindable canScalePlayfieldOrigin = null!; public SelectionRotationHandler RotationHandler { get; init; } = null!; public SelectionScaleHandler ScaleHandler { get; init; } = null!; @@ -82,12 +82,12 @@ namespace osu.Game.Rulesets.Osu.Edit canScaleY = ScaleHandler.CanScaleY.GetBoundCopy(); canScaleY.BindValueChanged(_ => updateCanScaleAggregate()); - canScaleDiagonally = ScaleHandler.CanScaleDiagonally.GetBoundCopy(); - canScaleDiagonally.BindValueChanged(_ => updateCanScaleAggregate()); + canScalePlayfieldOrigin = ScaleHandler.CanScalePlayfieldOrigin.GetBoundCopy(); + canScalePlayfieldOrigin.BindValueChanged(_ => updateCanScaleAggregate()); void updateCanScaleAggregate() { - canScale.Value = ScaleHandler.CanScaleX.Value || ScaleHandler.CanScaleY.Value || ScaleHandler.CanScaleDiagonally.Value; + canScale.Value = ScaleHandler.CanScaleX.Value || ScaleHandler.CanScaleY.Value || ScaleHandler.CanScalePlayfieldOrigin.Value; } // bindings to `Enabled` on the buttons are decoupled on purpose diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionScaleHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionScaleHandler.cs index fb421c2329..495cce7ad6 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionScaleHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionScaleHandler.cs @@ -32,6 +32,16 @@ namespace osu.Game.Screens.Edit.Compose.Components /// public Bindable CanScaleDiagonally { get; private set; } = new BindableBool(); + /// + /// Whether scaling anchored by the selection origin can currently be performed. + /// + public Bindable CanScaleSelectionOrigin { get; private set; } = new BindableBool(); + + /// + /// Whether scaling anchored by the center of the playfield can currently be performed. + /// + public Bindable CanScalePlayfieldOrigin { get; private set; } = new BindableBool(); + public Quad? OriginalSurroundingQuad { get; protected set; } /// From d4489545f275fb29eae60ea96fbe4c84d8f82cf2 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sat, 25 May 2024 21:44:08 +0200 Subject: [PATCH 059/119] add axis toggles --- .../Edit/PreciseScalePopover.cs | 36 ++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs index 355064f000..b76d778b3d 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs @@ -8,6 +8,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Rulesets.Osu.UI; using osu.Game.Screens.Edit.Components.RadioButtons; @@ -28,6 +29,9 @@ namespace osu.Game.Rulesets.Osu.Edit private RadioButton selectionCentreButton = null!; + private OsuCheckbox xCheckBox = null!; + private OsuCheckbox yCheckBox = null!; + public PreciseScalePopover(SelectionScaleHandler scaleHandler) { this.scaleHandler = scaleHandler; @@ -69,7 +73,28 @@ namespace osu.Game.Rulesets.Osu.Edit () => setOrigin(ScaleOrigin.SelectionCentre), () => new SpriteIcon { Icon = FontAwesome.Solid.VectorSquare }) } - } + }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(4), + Children = new Drawable[] + { + xCheckBox = new OsuCheckbox(false) + { + RelativeSizeAxes = Axes.X, + LabelText = "X-axis", + Current = { Value = true }, + }, + yCheckBox = new OsuCheckbox(false) + { + RelativeSizeAxes = Axes.X, + LabelText = "Y-axis", + Current = { Value = true }, + }, + } + }, } }; selectionCentreButton.Selected.DisabledChanged += isDisabled => @@ -90,6 +115,9 @@ namespace osu.Game.Rulesets.Osu.Edit scaleInput.Current.BindValueChanged(scale => scaleInfo.Value = scaleInfo.Value with { Scale = scale.NewValue }); scaleOrigin.Items.First().Select(); + xCheckBox.Current.BindValueChanged(x => setAxis(x.NewValue, yCheckBox.Current.Value)); + yCheckBox.Current.BindValueChanged(y => setAxis(xCheckBox.Current.Value, y.NewValue)); + scaleHandler.CanScaleSelectionOrigin.BindValueChanged(e => { selectionCentreButton.Selected.Disabled = !e.NewValue; @@ -126,6 +154,12 @@ namespace osu.Game.Rulesets.Osu.Edit private Vector2? getOriginPosition(PreciseScaleInfo scale) => scale.Origin == ScaleOrigin.PlayfieldCentre ? OsuPlayfield.BASE_SIZE / 2 : null; + private void setAxis(bool x, bool y) + { + scaleInfo.Value = scaleInfo.Value with { XAxis = x, YAxis = y }; + updateMaxScale(); + } + protected override void PopIn() { base.PopIn(); From 76f13b21da3ed79e9daf3d7421342bdb29762dae Mon Sep 17 00:00:00 2001 From: Joppe27 Date: Sat, 25 May 2024 23:28:51 +0200 Subject: [PATCH 060/119] Correct scale of taiko-glow element --- osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyKiaiGlow.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyKiaiGlow.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyKiaiGlow.cs index 623243e9e1..487106d879 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyKiaiGlow.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyKiaiGlow.cs @@ -21,6 +21,8 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy private Sprite sprite = null!; + private const float base_scale = 0.8f; + [BackgroundDependencyLoader(true)] private void load(ISkinSource skin, HealthProcessor? healthProcessor) { @@ -30,7 +32,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy Origin = Anchor.Centre, Anchor = Anchor.Centre, Alpha = 0, - Scale = new Vector2(0.7f), + Scale = new Vector2(base_scale), Colour = new Colour4(255, 228, 0, 255), }; @@ -58,8 +60,8 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy if (!result.IsHit || !isKiaiActive) return; - sprite.ScaleTo(0.85f).Then() - .ScaleTo(0.7f, 80, Easing.OutQuad); + sprite.ScaleTo(base_scale + 0.15f).Then() + .ScaleTo(base_scale, 80, Easing.OutQuad); } } } From a62b9fa633437bd31dd375bf6c9b12a8591ff225 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 27 May 2024 11:42:36 +0900 Subject: [PATCH 061/119] Revert windows 16px icon to original version This also fixes the 48px version looking uncanny due to smaller paddings. --- osu.Desktop/lazer.ico | Bin 76679 -> 76679 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/osu.Desktop/lazer.ico b/osu.Desktop/lazer.ico index 24c1c29ba269aaeb38a9b9c604a1c364c986a753..d5dbf933c1c6795b54b485b349aa76f914707996 100644 GIT binary patch delta 11340 zcmbta32>d&bw)r^mQa#mDQyV}18E9PCbYv$(+)}KOgklj@uGc~S9XF)A%rEr0o#%# zOR^-(vi8lkuq|6&WV`^jF<@g``@ZW*deVDZpY~lk{l0tO|MVmbP1`$j-hc18=bn4+ zx%+otc={UZbpg(~;K|cZAknoR# zo4$|8&p|A71vHlM-q1hXHj3C~kOBG`#9sA7pmC5Zj1A35XZ@(uelZ|Lh23)T zpiM63+f0#V2}X&^~3V;ddI83A#VwsKd)ZBpB2QQo&j~B1gA3OSE5! zeDK5-+5FopvUS;2R8}P6%fEq;w{=;OY<}{pDih;hDktLG#NNEfIjqC~y2T;kyyIUC z@V^7v4pL!sU|LQlT4dYGV$@$G5zC8Z+lrDugBHuN+y}qCED_5}CDyM(K1sF8*r3b% z^QbHW+949d`w$W>*k z%y!QTSX3tIFV%^qY}l;sW=1$rZ-OC+n z@aI#NZ1*geh-XTa9`|xlD=JhSWvBXnfVy2F8!`Im5d)EEll?`WO}0K&0!^i|ZMjKh zY7|5)E0JigN{RKZa#A(ECeh*rQZhc(@KNcKGG^H-iS~rmt15pWmk;-t_46A6tA1u6 z!ZFPLl+us-W&289!o8Ll)D8`@<;fy#32Kb>sZq4U`PJ#g{Oz9fdATgeEaW@o{~FDy zmgrSgQg*tZHZaf_KUnwRKO@5Ftn6+r9g#TST8Z_lfyUZJL_XW8Myq#C!mlUqPFOK{ zqdVP#B9Z7M7{lxbXt%ef7|&|i;aek?@)1;~0_WS0V8V|LMD~$2>X?y?7n>x;vleF5 zeT`_+_LY^&tSEf1C9{aAM0{YohvM2dW-Qqo?icb|#bu<~A7 z`5Ta$pP#1KBzi@yP{Y^AuK|kCTyNwf&ZftDF}_$f&ZkcCnMaCTj^T|mW?FrnY3O=L zDT-Qw=6lyE<*XoTS+#tc+6B8*;NtnOu=Y*^5f9KYkl33hBq5*ymNrO&Un7WLO|&r1 zryj~{TzQmZ3z(U&EATJh55`3A~`8)laB+k1YmemX6CSS|eNxV;kGJtts zBdx>G87k3;rq#Lx<4`#2nOZrMXrpZm+&CUuyj5Ix9e7)q$<1(gSThV{WzCBy5$Sjz zxYDyu<;SBDnhYI@S%r}T0nKbu-yL2Ur^~Ar_^$j$+3B~q0<*$6Z}d0hnAVjwOHyc) zI2LwW!@U7KJ#a-YWeZ9Q9kOE;wsQZLuTh+Lql7aXvsH?3EUk=+s9=W|#sOT(d>U1D(m@*;cuD8O{a8s!Srv#e22Lv2fRX<;m(Nx&i17BzSa_F~_VVuWpt^KMM+K+Ubkd2bM|E zDXUCQaGvWhHasa84p^{tm&5p0i3_NbFAlZI=%AWNrkL0*!;#+QdWWa|?Za%Q(x3C+)mG+!B$;v)`eui_yW zikn8%%2W{5q|*H9Zl093vT>OjM*)P^3Wr)p^Iq=|>R`B9&@VB|8zm{Q9o^inylQWB zRnD29X9Hx_nWpaZVcF@8@!;Dky8}BU(Wg!FKWI_P*zzME*d);xiu~H-bbPnWOwWVn zB;%C^+57trX}K~k$0O_#{Y<_5={Hv;{k3wLo1K@*(OF4aQ!CBIgGyJ;XM-4g7@S^4 zn-pyBl$ohHnV+ARy>GV2PE>mM{dV~zrAyM+wIhwu?BAvgN`P?<7hRiardt=BqUMt0 z4%z7i*?zAim(R0e(qtTS@#gI={i^jV#TZi9z>t%j~B$Ra(hx@9MK zU=vzdFZLGQ^8EKOc>Ju&rMm;H5(gJvJv?CQKI_&6ZVl*=)Zi|qn_j(gs87k4ecU6_ z%j?0nNlIWB_8X&CTFZ6oKHk+WyF)QQ9eSSkTBf9=U|5cBwo8IPRzuG=NeXTevjs=O zF%W&Mk_u(J*#L`fKw-2M1GriGoh>~c*(piBoiNCTE@;OXXp^D>=W1%;{^{0&(=@aA zC`P9@6bIWS4YUh|3pUx*0G%A4kwe?CF?r$M;8QB6lAAHY=VW?fPI6wdO3uq2!g)S1 zIwMINaI*bREVJt>Z(n3t~RA?a=(F-#?L zdB1M;ZY&mDTo~G_2c-I z26RcXpIsTjzEAY-kcKZgjSW^KS#B-ZN(Prmj{Y=Qo*vSpC@r{0GFEp>alsJw3g;3B z*`e+kxtKd7X+c^x+21Y)-|P`v?UZXJoSB@L)}nFw@Ug;5#qnU(#<>)o)v zM_I;J)Sexck-k|u^v6C)4(@QZpfJ+rY5@&O3F;Jkqi!v1aG%BjYyda2?B_580_>6z z(yJ-7SJH#A*ZA6H&x^fs>^+AVN8TNf?Dgm$f3(g`knYL|?UDR9`sJuGAO~LSSITHC z4b589D|?>r1COolg{Q!$2SU3QKRu{V(pGo6x}-4DF1vkfFaX{R>Xn=qx&V628Z?%{ z%dHC@MtYJ5iep>gE*L_qGei49eTp*hO%3Rgv_Kf(2Zh0!uk$I_uNspU#L8d@#0jOv`;dE4g7>46M`(HS^(=ZpfDo@O4iuL(WQ}+kD_3VZ?}@8w+bU%w@yxc=Yc}4 zq^=`f>G2`u?u7;np!|Sjfmm<4p{(hXtT1Nus|*S#0&k&UzbmeJkO_l4WJpv-(q-uZ zt%JO}^W!pT!(jV8JP@SIa13w?8?cXJ?NcNsNe#|dy zcU^4G#ACepa}3d2(6@R}QC6to5T%Tjgn>z7rWtpBl{-;s7SfDgONM2#pog@yJkuG$ zC`WzJpW@dmpYFn@w2VIH@fjeZX}S3YkC(|pkk}d~BrVhi<+_rb)k8vJW#%_KEa~V? z9`_}U+~jd@(z%;FzW1my#VP43K@&i_)S#q?*`%k%wUQLzR(Owr8vhNX&YI^^F!Nz= zb{MN1c71aZH`BSRM}%qDcMan@ox^e;hkxaq9Ys zE@(C%3NwNnvTvguXol;wz6X{n%;iBCsTPjbk}-8A$POKbHDg~RZKXSZRQ5tHclD^U z{#s?O&4abN#rNyNYsZ9@riIv~ty~{kU=%M2gnu7QC>{HrQF;QS6q^T2?p-^+h|F59 z#b3(%pVHLD7Ez9MvCfK9L$|>g6$qIH!YT#m#W|Tcr8=-zrZugnBP6{bt-j2x204c*irAzAfYw4z?#PlM7SfmdXIDT zY%+G;P@FLW9k6IZK3uOY`f&XW;tRC@8{(A7Lkrlu=>ZO$J8W`3wNK4|)MbWfL_EF& zE1obATMcq;+FVl4#A8#y?w0D0jVE|mS)zR7tabnTSvPC6A6O%0StfIxP0l3rs^uBF z%|N6f#9YRmUp#932VZf13d#ey4v+(#xMv(2#9^mfa-K&Y;mnyDfD;w`njSEKF9~N8 z@{mUv%9b1-lmX}21p07ZQul7U{Xsq%c_H%eAcB$l1cL~}=m6fMfECnT7*-cnP9_o+ z@&?X4g2GDz_oyRDw?E+VMsoKv6W&b81YyoAicQ3wunZ&m+ zLP?aVzo;)y^D+yCvKGo7VZ(2MMQQj-d&QWfzgi<(o+<|Mv1`dg*UE)7^WMj!T=>%^ zlKVyjARgC}o(xqtL-~VYRE{B1K;;+rcdIuQzG3mz$NjbETJdPSTSczGI}BcwA_3d+ zd+DHS9_2eLx%W9lxDTg;6rQy8$x>B-PbJ~Y@CFG4EEZ2Je1b6_fz0O^@+lYN_COQ^ zz&dhyrJPN-Q6<70JJACzx`&mmC+pIIUWIz{&4ceTJMdycoRDxE4d5;LF1Ehu(wKT5*lFVb_!2QEz)Z}ae7*&c zXUgor0|a^I+kkopFw67(05HEs-5UYT0HXV93`^ro&+zc@9i|byrEu+#BY^6NlA54( z9MH~)CowAV9>X{hZiR$DQ=@oM_>xS-JbFc)YdFgGLmfqG$86TOJv={5; zOiCL!2^pb00ExcNG1(PXFPHQCA&&>4O#nL;`f3qd^(?w!noIfh?El;6WU{B~^eE6P zcmN1#?rRpA19*LLUq6tNcKPu2F2JmF^5uS@A8&UG!6hyx`Qi0S`H3Cv3e0G_0&K{~ z0vW5~f0AsIq>yGQ2yc-jplCaNEfP=6@#9fkKPI_6-p2mF2iA;U%^y5zp&CkPfz_!cq z9u!zF{axcyae7FOMd(391bqM7?UMFv6R^crbj6H(b+Au%dRirO1Ktz>Okc?9k(04( zDDdZvost~T1q0fo`0y~hfcUV%4exfiqqp)D`pmma9})m4wwF)H#MqP+pY9TC!;r!O z_rGqJ0>iGx#-#$QRGjO^1Js1%{;myh{j3xL+TXdVQ?fR6DfIkO-hdEEV|VV`2&4=E zRic*_T`=zK0z>V(H^7?m&PqV(zCgx_`2aP^dA3K&j*Uuh^Q^R9nUrJi4=89RHOMX( zQitIBY3Zz-mUBrw7u$iTj>z%N{iuZ4Aks%hq~P6tbrRDC0GdS}jo=`hk*N~)|Mi>- zn1)%?qoR&o1d)jM5c^5?1r~&K3a}tzJ>(NhNOq=)6)}&<32^|01vwi)ezH%moZC%I zd0z51!Z7H_-e6bL5Ot<~4PGnU>JBInYQb4e8KmJD;yWg!lj%f1H1e?+vQdBpWQh$N z`h!DCkKk&yu3w$^PDB77P+`{aW$PJ`z6;A^fAhr&vdL^TLxWE0(R-tf&Y z`IJ+#i}J`@041_Q&_y_G5@Vp3S}spw0M1}5G~`ZQz5c~!6a!Dmp|^mP;rvc0!Gsj_ zSKqsNKQ_tC*D!=TUjrGGBf>z+3>uWf@8~TUbwxQ}i55(N7k-Ehsoz>Tsrr<+SzaF1 zC7)jv=Ouk9&%D(0yO6Ig^1S@3J3Q|17#mzOPzSB}Tb==M;Fx=s-Es9GuwJ-{_W<6< z_u@KDWPo2q>Mp-Hy*N+b>GeH+OzA2*tPg}xzs@|}V9lS)VucsdalOG6hu0)t9eF7w zsmtx3d7(A0NuOuxJ1lhbn8u^jz4swvFLFyMZ`}+knozQb-|iD@ z~fS6*E$GdQ6J)M-(yMSvey!T>BBQ?BP&07lr73Q1S#X z9Dg+{vw=Jn;DS+xg&8$WVJ#<3_F&B_LtX}Ua2{Lk!Yw7}9z=LJz8%CkiE$6!a~}0b z`q5849=m6148`%xJT<9lViaHSQ0tHXo)Lz|vmz(o#diI?$l;f9d)lOV%ySCgX;6#3 z27e&LusyKh+Qd9tHK2CLF3hd#uA>r`{ibqD`IKcDQwQuYzO~||3TOxe6;Kgdc;YRQ z&th@pdsP$TA8*k{nR(<<7x^s1vTV#}v4+y;`nj0wib}A7hVM-tl{2s+ z5<^k0{+fAAQ=VO6_FIMFti%*D4WO{8$jmd->`yA75ua?;m6*S#4AX2agIV4*mSe&k zFb3U00n=rS)0OUN@<^s~(}nB?R`6|O$1KY)yTdesWf6@3_aLH{|N9_L_}FpFq6u@# b&=ZDEW}Izh;E>_$Qd7_DezO;xnO^@5U$|tB delta 11404 zcmZu%d2E%}m5-|?N+z8V9i>VlWi&*SQKwb^V5Dd?f3zwi&1fN3D zNr>8yCQvZ8v5jq>7x1$IW45pbLmC2(X%fH~jCbRO=Y7Y!!9dzGzu&pv{ob?FPde{z z=bm%!x$8OSZL9m?wz?mld~*015t#{?9Rjmhmv(9>eFidwa6lfQJS}zPAJXzh9{byJ zlv3Q589@EuaTG`+&qet=pb;K>bO^O4e82v2*!urQj=YKiQ@-BYcctu1ubkN4EQP;o zkgRnzlDW28A!}W=*4NMqW^@y>W;>X-J_(61cSQ-%OQKlVMD*eF})T$L?zNq zXIiT1)|Xjt>R2Can~L|z|6YaMH|1P<>vVYnU^L4WCGGQ}U-=TA4EvEo?i7C8tJfty z{JJD9r~p(-BGOPuTKN6C&~}^OI_H|#FD)5vYiL z?=`3lI~P{TA6|h>NRt*2Dov_oH?MWfR|@eJ*KN1`6X%;TX}gPm*lCuLUY)QfEbln1 z$`_4VC`Qg3^-6N*!fK?|k}wbJV(v}NlT;Ac&a%M^)?1uvNt|B^^|EiUj``$8rth9N zZiUX*qZBC`;{3uvW)^^oQ;glQRTBSdxq;V^5>Vdu$_?52a+ykzyr>40U07IS>X3%k zC9@1*f6ZI`sp+%YY44j2VE%wI*pIvY1qY=WfB-G`NhU0m<=Fc_6WY*NrR=3R=^WYT;>wU_3<(`cD9^+TlRevQm* zmiS#h(x(Jd7u7345;b5L>~smTF8Y9)-w+B#Vm8eGgmdrE)elxNrTC|Y0}&plv&pRMzz=A>b9Z&XWF>!<#Mls?VK>TN`}pzrF_MZ_9+J$Pm$+$ zcPsZaN(V?=0_g!vS(iNDsLYm`QgxLfb5kNzn)W*xxd+xTf3?s&;?q0K_o|fF4g*jt-~*DjB3pTiU1x zo?fK5G`HiZ*|`7*pVE~+0+hd*?!)wjEZnunH|$C8B1>~@L<^z z{*B~*P$orLb%0vQe($Pmez_PoL4^(=ZE=$W=Gc|EIXBO_D4FkEmeal(g`j)~p!k-z zDikFK-(nmAXw9@#4}Z`hnZS#G?S|YNf2h^hi?E*|xNlh-+S&na!XwsxR6m7qvRB^L ztA>xnZF8?k!iq~+C7l*ePA9a<$@oA>8N?zCQ3_Y=4<3cG>ergO>SodPh4(5Rk0p9g-DcfLFNvK2OJ-<8(>J(=BcF zHMw!V>wAlD1XGh9Q7=WQEw-ENS=XR+GnTaCh;;&Tc2}F%n6;)BM^>|(&uP`2aL(5x zDGM4TV{xna*9D27!)zC{@BWQOK>r3^)mm5ZQN0c-JEBu8@TqIRqSRK|^%$nTE02S1)QY*Q^`z#xT=3gkiy%jSi}x39tKnXA9&oEt=u=yIB`1dS4(*}O?xihK zw5!$Y=U?9}IV-vV-8zuag_!#$&yL&DUhUWsOz0xoy|_)a;Zd13f_)p>B?lwOUePJF zB_oQ))xYl}JlE%!DR5nIXYu1!^}pVUIlpbK2PwgT6Java0?KgKlw<$5OHmqj1orYD zL)4u6Mw2^&9x&{hvXwXmJY?E&c=2PJu_PeACGFD(z!g7Jk??bTGlY-mmOU$ar68_L zoY14PyLnUsm3J(zreEg|O6JlI@hxqa#w)=Qe{-TmYQOEBviiA*&+Q5xW_jxalD)iJ z_C{i=Fb!Y&dOh0tZ?)i@Xnkqg2wJf9I_1#&!NrCiGu^{SJD*MJm8@l*N;EgBUyg0+ zrJA{zpT2rzNcKebpsi2gz*}8V`vYC*WyeP_@-8`>7!<<)I@SlYVeot9Y(lW@U|ctj zfObzlUKKtZY8=P~U%xjH%Ek{BdIVRT1E1wxvUf$F049jO*26P(9 zC}(*OXqiEuPZ>e`knD}>(^HuDo1S^h@J!$cD$bgzq|`=!9f2QZ>2JBOcqFI-McolU zAUASAM@2#X(F0)Gi+YR@FeN3P4#ur#*;#?nag<1jOoD_xkX}#B_Dm-g8u{&YF8rKjw0EaF?LLeVgeN%uq!9T*%~_zG&fDf#-Yv@vx^Bhw5TbR}AlE_d8)%j7?F`n@YcnnOxHS$|(;V_N^QN z)e$49@8;uSKYdm;2!?D5!l>l?zDDzN{z%i5A3dUL{$ghEOr=XPXfjhpT+v4LB|SyaWL9at z3#9ZIP8RHAYGnUPaE~^WnI{KR9@jj}`&W(20f5ctN1>lBvya(LCJ4xj&AD4Zoi`r6@6DkT*%g)r--n5VQKa|3_9^Hlql$(+AV9Y&8 zcMqKIYI-mhc`X~R+UDYTxV8`RLfULzrHq+)R(C}KR1>lwIp4D&o%%VWP|%Ot=pdxy zNyFX;VO5Nrsr7(6&Kx|QHUr=_2SQw*{CAn zhXx~Gsl4{M-U^|c!r1!;VtlXsQ`pa6Jmi)S#Xj)b$dRjjFAkxjAL0cQ+>azpuk$Y* zWTF7x`Y(mumcIrQgcFO?ZiYc-B>X_g;Lz%aQ&ZF();iR$Htj}0s0lQ5cUo}c!G zak&5Kpq9_e06@Qy{s*|X@HD^^gOkBwbMcX?a)^DX9Le?Y>*Q)Vo8jA`vu{o$9$KI^ zZTJB&n(3a~2uu8zgEH^TWPtZk^*kJMV>|SX#0#F9(QWS?1$R+4CllMaTXXPuFf!*1McjS+KE77i!_#;Ddd~<#9HtM{(ee zVbtl-Vo;!Cb?uLkA=MLrAv7^8=Ec0i{5nrQx?sgxq#T!Yc;ILKu!=IkvS`k6CoCIg%%_7v~@|8xR0U<%I$9t*(TBunaD{ zGT9Dq8eyAFwH{@-^H^7~sa~p!y*oHS&COZ@LL`rQzAudK>m!T|frk6VLEIOjQJ}u04`XJm#>RUR=F&|WjOj4^WZQyy#xRUK)3#+q z)qs{UDnWKLm^>GY40WOnsmrUq&<;a1AHRga9*Ed@ho)`_$L53Whz79D4dv{H3(hk0 z-YYY}D8PN?TX{>JEjH{z#|u5}bfD5#5?XKa2atmT~em54Yut-s5xcT70LVXrew$6kR9;cQy2{it-2UwYMQdiRy zydA1vE*~C%&!a`sBCDkC@{~Z7lz-WUjan%_c$1kA{f6K>&4}ezs8wG}Xxr^dTr_Xg zsT0?_F?}l=6i}k#K+zsW^?}I*prSr zU$k*oHkFRRW!0)a3TlPhhbJuK8C>ZM59CA5H(bB!#oR9aa|7wn<;lK`0&&zWRU;a5 zU<8| zmjepi0W(R7Bd2GD?jU+-Xbl#|(N_Tvj(T%UJ=^J*Qh{7gjOP@Kg6|7;>d$gLLZn53 zC;;zMzC2wmJLIcA2BhWvt@l-R#+76|HheHzR}UY1EHkAL))Arts}IC@Z%lvYt=9&A z3J5{}*Fft7Omi1pPn=dq0_Gr84~;HF+9f2YMl*a9hKTiFAa(s0)>T0NgzPDa(to{%IbQlU_?vB}l?g&9N3_^;dY)+Z2br&UeC_O0~{fPlyk^bz`$ z_u%5{IIwO&sTzCT*oJEPTKip7*E;s^tyHyKY4Xv6B_2}1ftYbQ3>}5IO!bk<7w|?h;ni_^3;eLy zG!)AkqsBV3Hlu4t!eX=bBu!#27p{U3E6u^<^cPbe?6oDMa%QX9l^m~{gVrcTs?tJg zYjhUA-HWw1O4<17gZvm^J@Wj8Buuf#R0hO3Czz1sLeDh%=eE!4g2MDC=)LQ zQ0#&qDeGvzJohiiL=SaM1}^#$BYU@AUr@X7M?mKJf4Y0)-v!tK$WJ-QY;L@YIKtXG zylO)J$fkcrGk;o3hehCCCR26?iPU^Gtgjuuc9U-)7Jst90Qe@{6;UZAe!U^7uDnAZI2}@Yx?66SPDmF*QU~7;=nJLp zt1&743}GZZE&^qD@!G-v5)kDZzqhaO_=k z-s=&IOt4A*DaV+z~nH>Lf0L*#Is<{>2a(xn=(1;VkI2(;aQV%Zdd~<-& zK{<+;mcD+JIoAO{5OY|6_{}lz=rZ0Y_=4O`fBZM&(o!}FRB=o%{-&KD`WtSTaLRP^ s#9jI9cVi%f_Y1BX=D$5KEF(R-VWPRaxcz3Z1Hca`b|D!lchrLa2RxK$m;e9( From 0d6adf160bcff5da41e5e2262b7e223163670179 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 27 May 2024 14:20:28 +0900 Subject: [PATCH 062/119] Share scale factor with hit target --- .../Skinning/Legacy/LegacyKiaiGlow.cs | 8 +++----- .../Skinning/Legacy/TaikoLegacyHitTarget.cs | 10 ++++++++-- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyKiaiGlow.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyKiaiGlow.cs index 487106d879..9877efa127 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyKiaiGlow.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyKiaiGlow.cs @@ -21,8 +21,6 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy private Sprite sprite = null!; - private const float base_scale = 0.8f; - [BackgroundDependencyLoader(true)] private void load(ISkinSource skin, HealthProcessor? healthProcessor) { @@ -32,7 +30,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy Origin = Anchor.Centre, Anchor = Anchor.Centre, Alpha = 0, - Scale = new Vector2(base_scale), + Scale = new Vector2(TaikoLegacyHitTarget.SCALE), Colour = new Colour4(255, 228, 0, 255), }; @@ -60,8 +58,8 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy if (!result.IsHit || !isKiaiActive) return; - sprite.ScaleTo(base_scale + 0.15f).Then() - .ScaleTo(base_scale, 80, Easing.OutQuad); + sprite.ScaleTo(TaikoLegacyHitTarget.SCALE + 0.15f).Then() + .ScaleTo(TaikoLegacyHitTarget.SCALE, 80, Easing.OutQuad); } } } diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacyHitTarget.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacyHitTarget.cs index 0b43f1c845..2a008d81d9 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacyHitTarget.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacyHitTarget.cs @@ -12,6 +12,12 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy { public partial class TaikoLegacyHitTarget : CompositeDrawable { + /// + /// In stable this is 0.7f (see https://github.com/peppy/osu-stable-reference/blob/7519cafd1823f1879c0d9c991ba0e5c7fd3bfa02/osu!/GameModes/Play/Rulesets/Taiko/RulesetTaiko.cs#L592) + /// but for whatever reason this doesn't match visually. + /// + public const float SCALE = 0.8f; + [BackgroundDependencyLoader] private void load(ISkinSource skin) { @@ -22,7 +28,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy new Sprite { Texture = skin.GetTexture("approachcircle"), - Scale = new Vector2(0.83f), + Scale = new Vector2(SCALE + 0.03f), Alpha = 0.47f, // eyeballed to match stable Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -30,7 +36,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy new Sprite { Texture = skin.GetTexture("taikobigcircle"), - Scale = new Vector2(0.8f), + Scale = new Vector2(SCALE), Alpha = 0.22f, // eyeballed to match stable Anchor = Anchor.Centre, Origin = Anchor.Centre, From 11c3d11db9b66097d61f877cb2e9b602bf90b5e5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 27 May 2024 14:38:43 +0900 Subject: [PATCH 063/119] Fix cinema mod not hiding playfield skin layer --- osu.Game/Rulesets/Mods/ModCinema.cs | 2 ++ osu.Game/Screens/Play/HUDOverlay.cs | 15 +++++++++------ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModCinema.cs b/osu.Game/Rulesets/Mods/ModCinema.cs index 7c88a8a588..0c00eb6ae0 100644 --- a/osu.Game/Rulesets/Mods/ModCinema.cs +++ b/osu.Game/Rulesets/Mods/ModCinema.cs @@ -36,6 +36,8 @@ namespace osu.Game.Rulesets.Mods { overlay.ShowHud.Value = false; overlay.ShowHud.Disabled = true; + + overlay.PlayfieldSkinLayer.Hide(); } public void ApplyToPlayer(Player player) diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 9d7a05bc90..16dfff8c19 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -109,7 +109,10 @@ namespace osu.Game.Screens.Play private readonly List hideTargets; - private readonly Drawable playfieldComponents; + /// + /// The container for skin components attached to + /// + internal readonly Drawable PlayfieldSkinLayer; public HUDOverlay([CanBeNull] DrawableRuleset drawableRuleset, IReadOnlyList mods, bool alwaysShowLeaderboard = true) { @@ -129,7 +132,7 @@ namespace osu.Game.Screens.Play drawableRuleset != null ? (rulesetComponents = new HUDComponentsContainer(drawableRuleset.Ruleset.RulesetInfo) { AlwaysPresent = true, }) : Empty(), - playfieldComponents = drawableRuleset != null + PlayfieldSkinLayer = drawableRuleset != null ? new SkinComponentsContainer(new SkinComponentsContainerLookup(SkinComponentsContainerLookup.TargetArea.Playfield, drawableRuleset.Ruleset.RulesetInfo)) { AlwaysPresent = true, } : Empty(), topRightElements = new FillFlowContainer @@ -247,10 +250,10 @@ namespace osu.Game.Screens.Play { Quad playfieldScreenSpaceDrawQuad = drawableRuleset.Playfield.SkinnableComponentScreenSpaceDrawQuad; - playfieldComponents.Position = ToLocalSpace(playfieldScreenSpaceDrawQuad.TopLeft); - playfieldComponents.Width = (ToLocalSpace(playfieldScreenSpaceDrawQuad.TopRight) - ToLocalSpace(playfieldScreenSpaceDrawQuad.TopLeft)).Length; - playfieldComponents.Height = (ToLocalSpace(playfieldScreenSpaceDrawQuad.BottomLeft) - ToLocalSpace(playfieldScreenSpaceDrawQuad.TopLeft)).Length; - playfieldComponents.Rotation = drawableRuleset.Playfield.Rotation; + PlayfieldSkinLayer.Position = ToLocalSpace(playfieldScreenSpaceDrawQuad.TopLeft); + PlayfieldSkinLayer.Width = (ToLocalSpace(playfieldScreenSpaceDrawQuad.TopRight) - ToLocalSpace(playfieldScreenSpaceDrawQuad.TopLeft)).Length; + PlayfieldSkinLayer.Height = (ToLocalSpace(playfieldScreenSpaceDrawQuad.BottomLeft) - ToLocalSpace(playfieldScreenSpaceDrawQuad.TopLeft)).Length; + PlayfieldSkinLayer.Rotation = drawableRuleset.Playfield.Rotation; } float? lowestTopScreenSpaceLeft = null; From bdfce4b9dafc90314e6b55f448e899d164ca1d97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 27 May 2024 08:26:00 +0200 Subject: [PATCH 064/119] Fix xmldoc reference --- osu.Game/Screens/Play/HUDOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 16dfff8c19..0c0941573c 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -110,7 +110,7 @@ namespace osu.Game.Screens.Play private readonly List hideTargets; /// - /// The container for skin components attached to + /// The container for skin components attached to /// internal readonly Drawable PlayfieldSkinLayer; From b6471f0b9cd4ebb3dee155990e89b44e1e2a14f8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 27 May 2024 17:09:35 +0900 Subject: [PATCH 065/119] Allow previewing audio of playlist items --- .../Drawables/Cards/BeatmapCardThumbnail.cs | 5 +- osu.Game/Overlays/BeatmapSet/BasicStats.cs | 5 +- .../Overlays/BeatmapSet/Buttons/PlayButton.cs | 5 +- .../BeatmapSet/Buttons/PreviewButton.cs | 6 +-- osu.Game/Overlays/BeatmapSet/Details.cs | 1 + .../OnlinePlay/DrawableRoomPlaylistItem.cs | 49 ++++++++++++++++--- 6 files changed, 51 insertions(+), 20 deletions(-) diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs index cd498c474a..e70d115715 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs @@ -8,7 +8,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Beatmaps.Drawables.Cards.Buttons; -using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; using osu.Framework.Graphics.UserInterface; using osuTK; @@ -36,14 +35,14 @@ namespace osu.Game.Beatmaps.Drawables.Cards [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; - public BeatmapCardThumbnail(APIBeatmapSet beatmapSetInfo) + public BeatmapCardThumbnail(IBeatmapSetInfo beatmapSetInfo) { InternalChildren = new Drawable[] { new UpdateableOnlineBeatmapSetCover(BeatmapSetCoverType.List) { RelativeSizeAxes = Axes.Both, - OnlineInfo = beatmapSetInfo + OnlineInfo = beatmapSetInfo as IBeatmapSetOnlineInfo }, background = new Box { diff --git a/osu.Game/Overlays/BeatmapSet/BasicStats.cs b/osu.Game/Overlays/BeatmapSet/BasicStats.cs index 0b1befe7b9..364874cdf7 100644 --- a/osu.Game/Overlays/BeatmapSet/BasicStats.cs +++ b/osu.Game/Overlays/BeatmapSet/BasicStats.cs @@ -16,7 +16,6 @@ using osu.Game.Beatmaps; using osu.Game.Extensions; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; -using osu.Game.Online.API.Requests.Responses; using osu.Game.Resources.Localisation.Web; using osuTK; @@ -26,9 +25,9 @@ namespace osu.Game.Overlays.BeatmapSet { private readonly Statistic length, bpm, circleCount, sliderCount; - private APIBeatmapSet beatmapSet; + private IBeatmapSetInfo beatmapSet; - public APIBeatmapSet BeatmapSet + public IBeatmapSetInfo BeatmapSet { get => beatmapSet; set diff --git a/osu.Game/Overlays/BeatmapSet/Buttons/PlayButton.cs b/osu.Game/Overlays/BeatmapSet/Buttons/PlayButton.cs index 5f9cdf5065..921f136de9 100644 --- a/osu.Game/Overlays/BeatmapSet/Buttons/PlayButton.cs +++ b/osu.Game/Overlays/BeatmapSet/Buttons/PlayButton.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; using osu.Game.Audio; +using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API.Requests.Responses; @@ -28,9 +29,9 @@ namespace osu.Game.Overlays.BeatmapSet.Buttons [CanBeNull] public PreviewTrack Preview { get; private set; } - private APIBeatmapSet beatmapSet; + private IBeatmapSetInfo beatmapSet; - public APIBeatmapSet BeatmapSet + public IBeatmapSetInfo BeatmapSet { get => beatmapSet; set diff --git a/osu.Game/Overlays/BeatmapSet/Buttons/PreviewButton.cs b/osu.Game/Overlays/BeatmapSet/Buttons/PreviewButton.cs index 2254514a44..1eff4a7c11 100644 --- a/osu.Game/Overlays/BeatmapSet/Buttons/PreviewButton.cs +++ b/osu.Game/Overlays/BeatmapSet/Buttons/PreviewButton.cs @@ -8,9 +8,9 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Game.Audio; +using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Containers; -using osu.Game.Online.API.Requests.Responses; using osuTK; namespace osu.Game.Overlays.BeatmapSet.Buttons @@ -24,7 +24,7 @@ namespace osu.Game.Overlays.BeatmapSet.Buttons public IBindable Playing => playButton.Playing; - public APIBeatmapSet BeatmapSet + public IBeatmapSetInfo BeatmapSet { get => playButton.BeatmapSet; set => playButton.BeatmapSet = value; @@ -32,8 +32,6 @@ namespace osu.Game.Overlays.BeatmapSet.Buttons public PreviewButton() { - Height = 42; - Children = new Drawable[] { background = new Box diff --git a/osu.Game/Overlays/BeatmapSet/Details.cs b/osu.Game/Overlays/BeatmapSet/Details.cs index d656a6b14b..7d69cb7329 100644 --- a/osu.Game/Overlays/BeatmapSet/Details.cs +++ b/osu.Game/Overlays/BeatmapSet/Details.cs @@ -68,6 +68,7 @@ namespace osu.Game.Overlays.BeatmapSet preview = new PreviewButton { RelativeSizeAxes = Axes.X, + Height = 42, }, new DetailBox { diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs index 1b8e2d8be6..b28269c6e6 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs @@ -22,6 +22,7 @@ using osu.Framework.Localisation; using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; +using osu.Game.Beatmaps.Drawables.Cards; using osu.Game.Collections; using osu.Game.Database; using osu.Game.Graphics; @@ -32,6 +33,7 @@ using osu.Game.Online.Chat; using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Overlays.BeatmapSet; +using osu.Game.Overlays.BeatmapSet.Buttons; using osu.Game.Resources.Localisation.Web; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; @@ -81,7 +83,7 @@ namespace osu.Game.Screens.OnlinePlay private Mod[] requiredMods = Array.Empty(); private Container maskingContainer; - private Container difficultyIconContainer; + private FillFlowContainer difficultyIconContainer; private LinkFlowContainer beatmapText; private LinkFlowContainer authorText; private ExplicitContentBeatmapBadge explicitContent; @@ -93,6 +95,7 @@ namespace osu.Game.Screens.OnlinePlay private Drawable removeButton; private PanelBackground panelBackground; private FillFlowContainer mainFillFlow; + private BeatmapCardThumbnail thumbnail; [Resolved] private RealmAccess realm { get; set; } @@ -282,10 +285,23 @@ namespace osu.Game.Screens.OnlinePlay if (beatmap != null) { - difficultyIconContainer.Child = new DifficultyIcon(beatmap, ruleset, requiredMods) + difficultyIconContainer.Children = new Drawable[] { - Size = new Vector2(icon_height), - TooltipType = DifficultyIconTooltipType.Extended, + thumbnail = new BeatmapCardThumbnail(beatmap.BeatmapSet!) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Width = 60, + RelativeSizeAxes = Axes.Y, + Dimmed = { Value = IsHovered } + }, + new DifficultyIcon(beatmap, ruleset, requiredMods) + { + Size = new Vector2(icon_height), + TooltipType = DifficultyIconTooltipType.Extended, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, }; } else @@ -329,7 +345,7 @@ namespace osu.Game.Screens.OnlinePlay protected override Drawable CreateContent() { - Action fontParameters = s => s.Font = OsuFont.Default.With(weight: FontWeight.SemiBold); + Action fontParameters = s => s.Font = OsuFont.Default.With(size: 14, weight: FontWeight.SemiBold); return maskingContainer = new Container { @@ -364,12 +380,15 @@ namespace osu.Game.Screens.OnlinePlay { new Drawable[] { - difficultyIconContainer = new Container + difficultyIconContainer = new FillFlowContainer { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - AutoSizeAxes = Axes.Both, - Margin = new MarginPadding { Left = 8, Right = 8 }, + AutoSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Y, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(4), + Margin = new MarginPadding { Right = 8 }, }, mainFillFlow = new MainFlow(() => SelectedItem.Value == Model || !AllowSelection) { @@ -484,6 +503,20 @@ namespace osu.Game.Screens.OnlinePlay }, }; + protected override bool OnHover(HoverEvent e) + { + if (thumbnail != null) + thumbnail.Dimmed.Value = true; + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + if (thumbnail != null) + thumbnail.Dimmed.Value = false; + base.OnHoverLost(e); + } + protected override bool OnClick(ClickEvent e) { if (AllowSelection && valid.Value) From 1e2cac3e92f3cec02d28639a1314db80ae0bf022 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 27 May 2024 11:44:55 +0200 Subject: [PATCH 066/119] Remove unused using directive --- osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs index b28269c6e6..090236d6e2 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs @@ -33,7 +33,6 @@ using osu.Game.Online.Chat; using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Overlays.BeatmapSet; -using osu.Game.Overlays.BeatmapSet.Buttons; using osu.Game.Resources.Localisation.Web; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; From 1f41261fc7d5e0c0f5a77cc064a1ba3c1d525ff5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 27 May 2024 12:01:02 +0200 Subject: [PATCH 067/119] Fix test failure --- .../Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs index 6446ebd35f..bd62a8b131 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs @@ -16,6 +16,7 @@ using osu.Framework.Platform; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; +using osu.Game.Beatmaps.Drawables.Cards; using osu.Game.Database; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Cursor; @@ -317,13 +318,13 @@ namespace osu.Game.Tests.Visual.Multiplayer p.RequestResults = _ => resultsRequested = true; }); + AddUntilStep("wait for load", () => playlist.ChildrenOfType().Any() && playlist.ChildrenOfType().First().DrawWidth > 0); AddStep("move mouse to first item title", () => { var drawQuad = playlist.ChildrenOfType().First().ScreenSpaceDrawQuad; var location = (drawQuad.TopLeft + drawQuad.BottomLeft) / 2 + new Vector2(drawQuad.Width * 0.2f, 0); InputManager.MoveMouseTo(location); }); - AddUntilStep("wait for text load", () => playlist.ChildrenOfType().Any()); AddAssert("first item title not hovered", () => playlist.ChildrenOfType().First().IsHovered, () => Is.False); AddStep("click left mouse", () => InputManager.Click(MouseButton.Left)); AddUntilStep("first item selected", () => playlist.ChildrenOfType().First().IsSelectedItem, () => Is.True); From d97622491297efa4ee4699b06748588b2ea84a07 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 27 May 2024 19:59:25 +0900 Subject: [PATCH 068/119] Standardise padding on both sides of difficulty icon --- osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs index 090236d6e2..72866d1694 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs @@ -387,7 +387,7 @@ namespace osu.Game.Screens.OnlinePlay RelativeSizeAxes = Axes.Y, Direction = FillDirection.Horizontal, Spacing = new Vector2(4), - Margin = new MarginPadding { Right = 8 }, + Margin = new MarginPadding { Right = 4 }, }, mainFillFlow = new MainFlow(() => SelectedItem.Value == Model || !AllowSelection) { From 75d961e6f2cc74b631a1980be91f3525e4551b80 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 27 May 2024 20:03:46 +0900 Subject: [PATCH 069/119] Pass the same thing in twice for better maybe --- .../Visual/Beatmaps/TestSceneBeatmapCardThumbnail.cs | 2 +- osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs | 2 +- osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNormal.cs | 2 +- osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs | 4 ++-- osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCardThumbnail.cs b/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCardThumbnail.cs index f44fe2b90c..f5f9d121cc 100644 --- a/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCardThumbnail.cs +++ b/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCardThumbnail.cs @@ -34,7 +34,7 @@ namespace osu.Game.Tests.Visual.Beatmaps var beatmapSet = CreateAPIBeatmapSet(Ruleset.Value); beatmapSet.OnlineID = 241526; // ID hardcoded to ensure that the preview track exists online. - Child = thumbnail = new BeatmapCardThumbnail(beatmapSet) + Child = thumbnail = new BeatmapCardThumbnail(beatmapSet, beatmapSet) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs index 175c15ea7b..2c2761ff0c 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs @@ -61,7 +61,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards RelativeSizeAxes = Axes.Both, Children = new Drawable[] { - thumbnail = new BeatmapCardThumbnail(BeatmapSet) + thumbnail = new BeatmapCardThumbnail(BeatmapSet, BeatmapSet) { Name = @"Left (icon) area", Size = new Vector2(height), diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNormal.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNormal.cs index 18e1584a98..c6ba4f234a 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNormal.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNormal.cs @@ -62,7 +62,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards RelativeSizeAxes = Axes.Both, Children = new Drawable[] { - thumbnail = new BeatmapCardThumbnail(BeatmapSet) + thumbnail = new BeatmapCardThumbnail(BeatmapSet, BeatmapSet) { Name = @"Left (icon) area", Size = new Vector2(height), diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs index e70d115715..5d2717a787 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs @@ -35,14 +35,14 @@ namespace osu.Game.Beatmaps.Drawables.Cards [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; - public BeatmapCardThumbnail(IBeatmapSetInfo beatmapSetInfo) + public BeatmapCardThumbnail(IBeatmapSetInfo beatmapSetInfo, IBeatmapSetOnlineInfo onlineInfo) { InternalChildren = new Drawable[] { new UpdateableOnlineBeatmapSetCover(BeatmapSetCoverType.List) { RelativeSizeAxes = Axes.Both, - OnlineInfo = beatmapSetInfo as IBeatmapSetOnlineInfo + OnlineInfo = onlineInfo }, background = new Box { diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs index 72866d1694..e9126a1404 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs @@ -286,7 +286,7 @@ namespace osu.Game.Screens.OnlinePlay { difficultyIconContainer.Children = new Drawable[] { - thumbnail = new BeatmapCardThumbnail(beatmap.BeatmapSet!) + thumbnail = new BeatmapCardThumbnail(beatmap.BeatmapSet!, (IBeatmapSetOnlineInfo)beatmap.BeatmapSet!) { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, From aad0982e26ba3fe57caf1df7999d7d582c413097 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 27 May 2024 20:33:24 +0900 Subject: [PATCH 070/119] Adjust size of play button / progress to match `BeatmapCardThumbnail` usage --- osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs index 5d2717a787..7b668d7dc4 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs @@ -61,7 +61,6 @@ namespace osu.Game.Beatmaps.Drawables.Cards { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Size = new Vector2(50), InnerRadius = 0.2f }, content = new Container @@ -92,6 +91,9 @@ namespace osu.Game.Beatmaps.Drawables.Cards { base.Update(); progress.Progress = playButton.Progress.Value; + + playButton.Scale = new Vector2(DrawWidth / 100); + progress.Size = new Vector2(50 * DrawWidth / 100); } private void updateState() From 405c72c0d66ef895cbb5d34aa6c32e5d81dd5d2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 27 May 2024 13:36:44 +0200 Subject: [PATCH 071/119] Fix mod display not being aligned with mapper text --- osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs index e9126a1404..ab32ca2558 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs @@ -416,6 +416,8 @@ namespace osu.Game.Screens.OnlinePlay new FillFlowContainer { AutoSizeAxes = Axes.Both, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, Direction = FillDirection.Horizontal, Spacing = new Vector2(10f, 0), Children = new Drawable[] @@ -438,7 +440,8 @@ namespace osu.Game.Screens.OnlinePlay Child = modDisplay = new ModDisplay { Scale = new Vector2(0.4f), - ExpansionMode = ExpansionMode.AlwaysExpanded + ExpansionMode = ExpansionMode.AlwaysExpanded, + Margin = new MarginPadding { Vertical = -6 }, } } } From d81be56adf67a46c0302695b273ac7c0f6c8e212 Mon Sep 17 00:00:00 2001 From: Aurelian Date: Mon, 27 May 2024 19:35:33 +0200 Subject: [PATCH 072/119] Test for accuracy of perfect curves --- .../Editor/TestSliderScaling.cs | 125 ++++++++++++++---- 1 file changed, 99 insertions(+), 26 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSliderScaling.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSliderScaling.cs index ef3824b5b0..2ffde0d3a3 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSliderScaling.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSliderScaling.cs @@ -3,8 +3,10 @@ #nullable disable +using System; using System.Linq; using NUnit.Framework; +using osu.Framework.Graphics.Primitives; using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Beatmaps; @@ -72,48 +74,119 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor private void moveMouse(Vector2 pos) => AddStep($"move mouse to {pos}", () => InputManager.MoveMouseTo(playfield.ToScreenSpace(pos))); } + [TestFixture] public class TestSliderNearLinearScaling { + private readonly Random rng = new Random(1337); + [Test] public void TestScalingSliderFlat() { - Slider sliderPerfect = new Slider - { - Position = new Vector2(300), - Path = new SliderPath( - [ - new PathControlPoint(new Vector2(0), PathType.PERFECT_CURVE), - new PathControlPoint(new Vector2(50, 25)), - new PathControlPoint(new Vector2(25, 100)), - ]) - }; + SliderPath sliderPathPerfect = new SliderPath( + [ + new PathControlPoint(new Vector2(0), PathType.PERFECT_CURVE), + new PathControlPoint(new Vector2(50, 25)), + new PathControlPoint(new Vector2(25, 100)), + ]); - Slider sliderBezier = new Slider - { - Position = new Vector2(300), - Path = new SliderPath( - [ - new PathControlPoint(new Vector2(0), PathType.BEZIER), - new PathControlPoint(new Vector2(50, 25)), - new PathControlPoint(new Vector2(25, 100)), - ]) - }; + SliderPath sliderPathBezier = new SliderPath( + [ + new PathControlPoint(new Vector2(0), PathType.BEZIER), + new PathControlPoint(new Vector2(50, 25)), + new PathControlPoint(new Vector2(25, 100)), + ]); - scaleSlider(sliderPerfect, new Vector2(0.000001f, 1)); - scaleSlider(sliderBezier, new Vector2(0.000001f, 1)); + scaleSlider(sliderPathPerfect, new Vector2(0.000001f, 1)); + scaleSlider(sliderPathBezier, new Vector2(0.000001f, 1)); for (int i = 0; i < 100; i++) { - Assert.True(Precision.AlmostEquals(sliderPerfect.Path.PositionAt(i / 100.0f), sliderBezier.Path.PositionAt(i / 100.0f))); + Assert.True(Precision.AlmostEquals(sliderPathPerfect.PositionAt(i / 100.0f), sliderPathBezier.PositionAt(i / 100.0f))); } } - private void scaleSlider(Slider slider, Vector2 scale) + [Test] + public void TestPerfectCurveMatchesTheoretical() { - for (int i = 0; i < slider.Path.ControlPoints.Count; i++) + for (int i = 0; i < 20000; i++) { - slider.Path.ControlPoints[i].Position *= scale; + //Only test points that are in the screen's bounds + float p1X = 640.0f * (float)rng.NextDouble(); + float p2X = 640.0f * (float)rng.NextDouble(); + + float p1Y = 480.0f * (float)rng.NextDouble(); + float p2Y = 480.0f * (float)rng.NextDouble(); + SliderPath sliderPathPerfect = new SliderPath( + [ + new PathControlPoint(new Vector2(0, 0), PathType.PERFECT_CURVE), + new PathControlPoint(new Vector2(p1X, p1Y)), + new PathControlPoint(new Vector2(p2X, p2Y)), + ]); + + assertMatchesPerfectCircle(sliderPathPerfect); + + scaleSlider(sliderPathPerfect, new Vector2(0.00001f, 1)); + + assertMatchesPerfectCircle(sliderPathPerfect); + } + } + + private void assertMatchesPerfectCircle(SliderPath path) + { + if (path.ControlPoints.Count != 3) + return; + + //Replication of PathApproximator.CircularArcToPiecewiseLinear + CircularArcProperties circularArcProperties = new CircularArcProperties(path.ControlPoints.Select(x => x.Position).ToArray()); + + if (!circularArcProperties.IsValid) + return; + + //Addresses cases where circularArcProperties.ThetaRange>0.5 + //Occurs in code in PathControlPointVisualiser.ensureValidPathType + RectangleF boundingBox = PathApproximator.CircularArcBoundingBox(path.ControlPoints.Select(x => x.Position).ToArray()); + if (boundingBox.Width >= 640 || boundingBox.Height >= 480) + return; + + int subpoints = (2f * circularArcProperties.Radius <= 0.1f) ? 2 : Math.Max(2, (int)Math.Ceiling(circularArcProperties.ThetaRange / (2.0 * Math.Acos(1f - 0.1f / circularArcProperties.Radius)))); + + //ignore cases where subpoints is int.MaxValue, result will be garbage + //as well, having this many subpoints will cause an out of memory error, so can't happen during normal useage + if (subpoints == int.MaxValue) + return; + + for (int i = 0; i < Math.Min(subpoints, 100); i++) + { + float progress = (float)rng.NextDouble(); + + //To avoid errors from interpolating points, ensure we check only positions that would be subpoints. + progress = (float)Math.Ceiling(progress * (subpoints - 1)) / (subpoints - 1); + + //Special case - if few subpoints, ensure checking every single one rather than randomly + if (subpoints < 100) + progress = i / (float)(subpoints - 1); + + //edge points cause issue with interpolation, so ignore the last two points and first + if (progress == 0.0f || progress >= (subpoints - 2) / (float)(subpoints - 1)) + continue; + + double theta = circularArcProperties.ThetaStart + circularArcProperties.Direction * progress * circularArcProperties.ThetaRange; + Vector2 vector = new Vector2((float)Math.Cos(theta), (float)Math.Sin(theta)) * circularArcProperties.Radius; + + Assert.True(Precision.AlmostEquals(circularArcProperties.Centre + vector, path.PositionAt(progress), 0.01f), + "A perfect circle with points " + string.Join(", ", path.ControlPoints.Select(x => x.Position)) + " and radius" + circularArcProperties.Radius + "from SliderPath does not almost equal a theoretical perfect circle with " + subpoints + " subpoints" + + ": " + (circularArcProperties.Centre + vector) + " - " + path.PositionAt(progress) + + " = " + (circularArcProperties.Centre + vector - path.PositionAt(progress)) + ); + } + } + + private void scaleSlider(SliderPath path, Vector2 scale) + { + for (int i = 0; i < path.ControlPoints.Count; i++) + { + path.ControlPoints[i].Position *= scale; } } } From 172cfdf88d4fb6ffb1b4dd35c5183b272407629e Mon Sep 17 00:00:00 2001 From: Aurelian Date: Mon, 27 May 2024 19:41:38 +0200 Subject: [PATCH 073/119] Added missing brackets for formulas --- osu.Game.Rulesets.Osu.Tests/Editor/TestSliderScaling.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSliderScaling.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSliderScaling.cs index 2ffde0d3a3..52a170b84e 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSliderScaling.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSliderScaling.cs @@ -149,7 +149,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor if (boundingBox.Width >= 640 || boundingBox.Height >= 480) return; - int subpoints = (2f * circularArcProperties.Radius <= 0.1f) ? 2 : Math.Max(2, (int)Math.Ceiling(circularArcProperties.ThetaRange / (2.0 * Math.Acos(1f - 0.1f / circularArcProperties.Radius)))); + int subpoints = (2f * circularArcProperties.Radius <= 0.1f) ? 2 : Math.Max(2, (int)Math.Ceiling(circularArcProperties.ThetaRange / (2.0 * Math.Acos(1f - (0.1f / circularArcProperties.Radius))))); //ignore cases where subpoints is int.MaxValue, result will be garbage //as well, having this many subpoints will cause an out of memory error, so can't happen during normal useage @@ -171,7 +171,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor if (progress == 0.0f || progress >= (subpoints - 2) / (float)(subpoints - 1)) continue; - double theta = circularArcProperties.ThetaStart + circularArcProperties.Direction * progress * circularArcProperties.ThetaRange; + double theta = circularArcProperties.ThetaStart + (circularArcProperties.Direction * progress * circularArcProperties.ThetaRange); Vector2 vector = new Vector2((float)Math.Cos(theta), (float)Math.Sin(theta)) * circularArcProperties.Radius; Assert.True(Precision.AlmostEquals(circularArcProperties.Centre + vector, path.PositionAt(progress), 0.01f), From 6c4def1c097298abbda31be5338ba791cd12dc1d Mon Sep 17 00:00:00 2001 From: Aurelian Date: Mon, 27 May 2024 20:32:18 +0200 Subject: [PATCH 074/119] Added check for infinite subpoints for PerfectCurve --- osu.Game/Rulesets/Objects/SliderPath.cs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/osu.Game/Rulesets/Objects/SliderPath.cs b/osu.Game/Rulesets/Objects/SliderPath.cs index eca14269fe..aa2570c336 100644 --- a/osu.Game/Rulesets/Objects/SliderPath.cs +++ b/osu.Game/Rulesets/Objects/SliderPath.cs @@ -330,11 +330,18 @@ namespace osu.Game.Rulesets.Objects if (subControlPoints.Length != 3) break; - //If a curve's theta range almost equals zero, the radius needed to have more than a - //floating point error difference is very large and results in a nearly straight path. - //Calculate it via a bezier aproximation instead. - //0.0005 corresponds with a radius of 8000 to have a more than 0.001 shift in the X value - if (Math.Abs(new CircularArcProperties(subControlPoints).ThetaRange) <= 0.0005d) + CircularArcProperties circularArcProperties = new CircularArcProperties(subControlPoints); + + //if false, we'll end up breaking anyways when calculating subPath + if (!circularArcProperties.IsValid) + break; + + //Coppied from PathApproximator.CircularArcToPiecewiseLinear + int subPoints = (2f * circularArcProperties.Radius <= 0.1f) ? 2 : Math.Max(2, (int)Math.Ceiling(circularArcProperties.ThetaRange / (2.0 * Math.Acos(1f - (0.1f / circularArcProperties.Radius))))); + + //if the amount of subpoints is int.MaxValue, causes an out of memory issue, so we default to bezier + //this only occurs for very large radii, so the result should be essentially a straight line anyways + if (subPoints == int.MaxValue) break; List subPath = PathApproximator.CircularArcToPiecewiseLinear(subControlPoints); From 96af0e1ec3c6c84ecd28178b5ae1b8ff6f4d3b43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 28 May 2024 13:07:11 +0200 Subject: [PATCH 075/119] Add failing test case for catch conversion Test is an abridged / cropped version of https://osu.ppy.sh/beatmapsets/971028#fruits/2062131 to demonstrate the specific failure case (unfortunately can't use the whole beatmap due to other conversion failures). --- .../CatchBeatmapConversionTest.cs | 1 + ...tiplier-precision-expected-conversion.json | 1 + .../high-speed-multiplier-precision.osu | 238 ++++++++++++++++++ 3 files changed, 240 insertions(+) create mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/high-speed-multiplier-precision-expected-conversion.json create mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/high-speed-multiplier-precision.osu diff --git a/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs index 81e0675aaa..f4c36d5188 100644 --- a/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs +++ b/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs @@ -54,6 +54,7 @@ namespace osu.Game.Rulesets.Catch.Tests [TestCase("3949367", new[] { typeof(CatchModDoubleTime), typeof(CatchModEasy) })] [TestCase("112643")] [TestCase("1041052", new[] { typeof(CatchModHardRock) })] + [TestCase("high-speed-multiplier-precision")] public new void Test(string name, params Type[] mods) => base.Test(name, mods); protected override IEnumerable CreateConvertValue(HitObject hitObject) diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/high-speed-multiplier-precision-expected-conversion.json b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/high-speed-multiplier-precision-expected-conversion.json new file mode 100644 index 0000000000..a562074fe9 --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/high-speed-multiplier-precision-expected-conversion.json @@ -0,0 +1 @@ +{"Mappings":[{"StartTime":265568.0,"Objects":[{"StartTime":265568.0,"Position":486.0,"HyperDash":false},{"StartTime":265658.0,"Position":465.1873,"HyperDash":false},{"StartTime":265749.0,"Position":463.208435,"HyperDash":false},{"StartTime":265840.0,"Position":465.146484,"HyperDash":false},{"StartTime":265967.0,"Position":459.5862,"HyperDash":false}]}]} \ No newline at end of file diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/high-speed-multiplier-precision.osu b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/high-speed-multiplier-precision.osu new file mode 100644 index 0000000000..ff641d9b0a --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/high-speed-multiplier-precision.osu @@ -0,0 +1,238 @@ +osu file format v14 + +[General] +AudioFilename: audio.mp3 +AudioLeadIn: 0 +PreviewTime: 226943 +Countdown: 0 +SampleSet: Soft +StackLeniency: 0.7 +Mode: 2 +LetterboxInBreaks: 0 +WidescreenStoryboard: 1 + +[Editor] +Bookmarks: 85568,86768,90968,265568 +DistanceSpacing: 0.9 +BeatDivisor: 12 +GridSize: 16 +TimelineZoom: 1 + +[Metadata] +Title:Snow +TitleUnicode:Snow +Artist:Ricky Montgomery +ArtistUnicode:Ricky Montgomery +Creator:Crowley +Version:Bury Me Six Feet in Snow +Source: +Tags:indie the honeysticks alternative english +BeatmapID:2062131 +BeatmapSetID:971028 + +[Difficulty] +HPDrainRate:6 +CircleSize:4.2 +OverallDifficulty:8.3 +ApproachRate:8.3 +SliderMultiplier:3.59999990463257 +SliderTickRate:1 + +[Events] +//Background and Video events +0,0,"me.jpg",0,0 +//Break Periods +//Storyboard Layer 0 (Background) +//Storyboard Layer 1 (Fail) +//Storyboard Layer 2 (Pass) +//Storyboard Layer 3 (Foreground) +//Storyboard Layer 4 (Overlay) +//Storyboard Sound Samples + +[TimingPoints] +368,1200,2,2,1,30,1,0 +368,-66.6666666666667,2,2,1,30,0,0 +29168,-58.8235294117647,2,2,1,40,0,0 +30368,-58.8235294117647,2,2,2,40,0,0 +30568,-58.8235294117647,2,2,1,40,0,0 +31368,-58.8235294117647,2,2,2,40,0,0 +31568,-58.8235294117647,2,2,1,40,0,0 +32768,-58.8235294117647,2,2,2,40,0,0 +33568,-58.8235294117647,2,2,2,40,0,0 +33968,-58.8235294117647,2,2,1,40,0,0 +35168,-58.8235294117647,2,2,2,40,0,0 +35968,-58.8235294117647,2,2,1,40,0,0 +36168,-58.8235294117647,2,2,2,40,0,0 +36368,-58.8235294117647,2,2,1,40,0,0 +37568,-58.8235294117647,2,2,2,40,0,0 +37968,-58.8235294117647,2,2,1,40,0,0 +38368,-58.8235294117647,2,2,2,40,0,0 +38768,-58.8235294117647,2,2,1,40,0,0 +39968,-58.8235294117647,2,2,2,40,0,0 +40168,-58.8235294117647,2,2,1,40,0,0 +40968,-58.8235294117647,2,2,2,40,0,0 +41168,-58.8235294117647,2,2,1,40,0,0 +42368,-58.8235294117647,2,2,2,40,0,0 +43168,-58.8235294117647,2,2,2,40,0,0 +43568,-58.8235294117647,2,2,1,40,0,0 +44768,-58.8235294117647,2,2,2,40,0,0 +45768,-58.8235294117647,2,2,2,40,0,0 +45968,-58.8235294117647,2,2,1,50,0,0 +47168,-58.8235294117647,2,2,2,50,0,0 +48368,-62.5,2,2,1,50,0,0 +67568,-58.8235294117647,2,2,1,70,0,1 +84668,-58.8235294117647,2,2,1,5,0,1 +84768,-58.8235294117647,2,2,1,70,0,1 +85068,-58.8235294117647,2,2,1,5,0,1 +85168,-58.8235294117647,2,2,1,70,0,1 +85468,-58.8235294117647,2,2,1,5,0,1 +85568,-58.8235294117647,2,2,1,70,0,1 +86768,-58.8235294117647,2,2,1,30,0,0 +91168,-58.8235294117647,2,2,1,50,0,0 +91568,1200,2,2,1,50,1,0 +91568,-58.8235294117647,2,2,1,50,0,1 +91643,-58.8235294117647,2,2,1,50,0,0 +92768,-58.8235294117647,2,2,2,50,0,0 +92968,-58.8235294117647,2,2,1,50,0,0 +95168,-58.8235294117647,2,2,2,50,0,0 +95368,-58.8235294117647,2,2,1,50,0,0 +97568,-58.8235294117647,2,2,2,50,0,0 +97768,-58.8235294117647,2,2,1,50,0,0 +99968,-58.8235294117647,2,2,2,50,0,0 +100168,-58.8235294117647,2,2,1,50,0,0 +100768,-58.8235294117647,2,2,2,50,0,0 +101168,-58.8235294117647,2,2,1,50,0,0 +102368,-58.8235294117647,2,2,2,50,0,0 +102568,-58.8235294117647,2,2,1,50,0,0 +104768,-58.8235294117647,2,2,2,50,0,0 +104968,-58.8235294117647,2,2,1,50,0,0 +107168,-58.8235294117647,2,2,2,50,0,0 +107368,-58.8235294117647,2,2,1,50,0,0 +108968,-58.8235294117647,2,2,2,50,0,0 +109168,-58.8235294117647,2,2,1,50,0,0 +109568,-58.8235294117647,2,2,2,50,0,0 +109968,-58.8235294117647,2,2,1,50,0,0 +110368,-58.8235294117647,2,2,2,50,0,0 +110768,-100,2,2,1,40,0,0 +127568,-62.5,2,2,2,50,0,0 +127968,-62.5,2,2,1,50,0,0 +128168,-62.5,2,2,2,50,0,0 +129968,-58.8235294117647,2,2,1,50,0,0 +131168,-58.8235294117647,2,2,2,50,0,0 +131368,-58.8235294117647,2,2,1,50,0,0 +133568,-58.8235294117647,2,2,2,50,0,0 +133768,-58.8235294117647,2,2,1,50,0,0 +135968,-58.8235294117647,2,2,2,50,0,0 +136168,-58.8235294117647,2,2,1,50,0,0 +138368,-58.8235294117647,2,2,2,50,0,0 +138568,-58.8235294117647,2,2,1,50,0,0 +139168,-58.8235294117647,2,2,2,50,0,0 +139368,-58.8235294117647,2,2,1,50,0,0 +139568,-58.8235294117647,2,2,1,50,0,0 +140768,-58.8235294117647,2,2,2,50,0,0 +140968,-58.8235294117647,2,2,1,50,0,0 +143168,-58.8235294117647,2,2,2,50,0,0 +143368,-58.8235294117647,2,2,1,50,0,0 +145568,-58.8235294117647,2,2,2,50,0,0 +145768,-58.8235294117647,2,2,1,50,0,0 +147368,-58.8235294117647,2,2,2,50,0,0 +147768,-58.8235294117647,2,2,1,50,0,0 +147968,-58.8235294117647,2,2,1,60,0,0 +148768,-58.8235294117647,2,2,2,60,0,0 +149168,-58.8235294117647,2,2,1,70,0,1 +158268,-58.8235294117647,2,2,2,70,0,1 +158568,-58.8235294117647,2,2,1,70,0,1 +166268,-58.8235294117647,2,2,1,5,0,1 +166368,-58.8235294117647,2,2,1,70,0,1 +166668,-58.8235294117647,2,2,1,5,0,1 +166768,-58.8235294117647,2,2,1,70,0,1 +167068,-58.8235294117647,2,2,1,5,0,1 +167168,-58.8235294117647,2,2,1,70,0,1 +168368,-62.5,2,2,1,50,0,0 +172368,-62.5,2,2,1,50,0,1 +173168,-62.5,2,2,1,50,0,0 +185168,-62.5,2,2,1,60,0,0 +185468,-62.5,2,2,1,5,0,0 +185568,-62.5,2,2,1,60,0,0 +185868,-62.5,2,2,1,5,0,0 +185968,-62.5,2,2,1,60,0,0 +186268,-62.5,2,2,1,5,0,0 +186368,-62.5,2,2,1,60,0,0 +186668,-62.5,2,2,1,5,0,0 +186768,-52.6315789473684,2,2,1,60,0,0 +187068,-62.5,2,2,1,5,0,0 +187168,-62.5,2,2,1,60,0,0 +187468,-62.5,2,2,1,5,0,0 +187568,-62.5,2,2,1,20,0,0 +187768,-62.5,2,2,1,24,0,0 +187968,-62.5,2,2,1,28,0,0 +188168,-62.5,2,2,1,32,0,0 +188368,-62.5,2,2,1,36,0,0 +188568,-62.5,2,2,1,40,0,0 +188768,1200,2,2,1,50,1,1 +188768,-58.8235294117647,2,2,1,50,0,1 +188843,-58.8235294117647,2,2,1,50,0,0 +189968,-58.8235294117647,2,2,2,50,0,0 +190168,-58.8235294117647,2,2,1,50,0,0 +192368,-58.8235294117647,2,2,2,50,0,0 +192568,-58.8235294117647,2,2,1,50,0,0 +194768,-58.8235294117647,2,2,2,50,0,0 +194968,-58.8235294117647,2,2,1,50,0,0 +196568,-58.8235294117647,2,2,2,50,0,0 +196768,-58.8235294117647,2,2,1,50,0,0 +197168,-58.8235294117647,2,2,2,50,0,0 +197368,-58.8235294117647,2,2,1,50,0,0 +197568,-58.8235294117647,2,2,2,50,0,0 +197968,-58.8235294117647,2,2,1,50,0,0 +198368,-58.8235294117647,2,2,1,50,0,0 +199568,-58.8235294117647,2,2,2,50,0,0 +199768,-58.8235294117647,2,2,1,50,0,0 +201968,-58.8235294117647,2,2,2,50,0,0 +202168,-58.8235294117647,2,2,1,50,0,0 +204368,-58.8235294117647,2,2,2,50,0,0 +204568,-58.8235294117647,2,2,1,50,0,0 +206768,-58.8235294117647,2,2,1,60,0,0 +207168,-58.8235294117647,2,2,2,60,0,0 +207968,-58.8235294117647,2,2,1,70,0,1 +216968,-58.8235294117647,2,2,2,70,0,1 +217168,-58.8235294117647,2,2,1,70,0,1 +217368,-58.8235294117647,2,2,2,70,0,1 +217568,-58.8235294117647,2,2,1,70,0,1 +225068,-58.8235294117647,2,2,1,5,0,1 +225168,-58.8235294117647,2,2,1,70,0,1 +225468,-58.8235294117647,2,2,1,5,0,1 +225568,-58.8235294117647,2,2,1,70,0,1 +225868,-58.8235294117647,2,2,1,5,0,1 +225968,-58.8235294117647,2,2,1,70,0,1 +227168,-58.8235294117647,2,2,1,30,0,0 +234368,-58.8235294117647,2,2,1,40,0,0 +236768,-58.8235294117647,2,2,1,70,0,1 +255968,-58.8235294117647,2,2,1,70,0,1 +261168,-58.8235294117647,2,2,1,70,0,1 +263068,-58.8235294117647,2,2,1,70,0,0 +263168,-58.8235294117647,2,2,1,60,0,1 +263243,-58.8235294117647,2,2,1,60,0,0 +264368,-58.8235294117647,2,2,1,60,0,1 +264443,-58.8235294117647,2,2,1,60,0,0 +265568,-444.444444444444,2,2,1,50,0,1 +265643,-444.444444444444,2,2,1,50,0,0 +266768,-444.444444444444,2,2,1,40,0,0 +267968,-444.444444444444,2,2,1,30,0,0 +269168,-444.444444444444,2,2,1,20,0,0 +270368,-444.444444444444,2,2,1,10,0,0 +271168,-444.444444444444,2,2,1,9,0,0 +271568,-444.444444444444,2,2,1,8,0,0 +271968,-444.444444444444,2,2,1,7,0,0 +272368,-444.444444444444,2,2,1,6,0,0 +272768,-444.444444444444,2,2,1,5,0,0 +275168,-444.444444444444,2,2,1,5,0,0 + + +[Colours] +Combo1 : 255,128,128 +Combo2 : 72,72,255 +Combo3 : 192,192,192 +Combo4 : 255,136,79 + +[HitObjects] +486,179,265568,6,0,P|461:174|454:174,1,26.999997997284,6|0,1:2|0:0,0:0:0:0: From a3b849375101c5ff4461fb78b92adb9e290af03a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 28 May 2024 13:07:13 +0200 Subject: [PATCH 076/119] Remove rounding of slider velocity multiplier on juice streams Compare: https://github.com/ppy/osu/pull/26616 This came up elsewhere, namely in https://github.com/ppy/osu/pull/28277#issuecomment-2133505958. As it turns out, at least one beatmap among those whose scores had unexpected changes in total score, namely https://osu.ppy.sh/beatmapsets/971028#fruits/2062131, was using slider velocity multipliers that were not a multiple of 0.01 (the specific value used was 0.225x). This meant that due to the rounding applied to `SliderVelocityMultiplierBindable` via `Precision`, the raw value was being incorrectly rounded, resulting in incorrect conversion. The "direct" change that revealed this is most likely https://github.com/ppy/osu-framework/pull/6249, by the virtue of shuffling the `BindableNumber` rounding code around and accidentally changing midpoint rounding semantics in the process. But it was not at fault here, as rounding was just as wrong before that change as after in this specific context. --- osu.Game.Rulesets.Catch/Objects/JuiceStream.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs index 671291ef0e..dade129038 100644 --- a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs +++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs @@ -29,7 +29,6 @@ namespace osu.Game.Rulesets.Catch.Objects public BindableNumber SliderVelocityMultiplierBindable { get; } = new BindableDouble(1) { - Precision = 0.01, MinValue = 0.1, MaxValue = 10 }; From c2e7cdfdccf1aaa83c5e9ab78cdf778547580951 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 28 May 2024 21:29:29 +0900 Subject: [PATCH 077/119] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 1f241c6db5..3a20dd2fdb 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + diff --git a/osu.iOS.props b/osu.iOS.props index eba9abd3b8..2f64fcefa5 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -23,6 +23,6 @@ iossimulator-x64 - + From bf0040447cfaefb6f5a56a399391ba18242f2549 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 28 May 2024 15:24:37 +0200 Subject: [PATCH 078/119] Fix legacy mania note body animation not resetting sometimes Hopefully closes https://github.com/ppy/osu/issues/28284. As far as I can tell this is a somewhat difficult one to reproduce because it relies on a specific set of circumstances (at least the reproduction case that I found does). The reset to frame 0 would previously be called explicitly when `isHitting` changed: https://github.com/ppy/osu/blob/182ca145c78432f4b832c8ea407e107dfeaaa8ad/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs#L144 However, it can be the case that `bodyAnimation` is not loaded at the point of this call. This is significant because `SkinnableTextureAnimation` contains this logic: https://github.com/ppy/osu/blob/182ca145c78432f4b832c8ea407e107dfeaaa8ad/osu.Game/Skinning/LegacySkinExtensions.cs#L192-L211 which cannot be moved any earlier (because any earlier the `Clock` may no longer be correct), and also causes the animation to be seeked forward while it is stopped. I can't figure out a decent way to layer this otherwise (by scheduling or whatever), so this commit is just applying the nuclear option of just seeking back to frame 0 on every update frame in which the body piece is not being hit. --- osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs index a8200e0144..00054f6be2 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs @@ -140,10 +140,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy private void onIsHittingChanged(ValueChangedEvent isHitting) { if (bodySprite is TextureAnimation bodyAnimation) - { - bodyAnimation.GotoFrame(0); bodyAnimation.IsPlaying = isHitting.NewValue; - } if (lightContainer == null) return; @@ -219,6 +216,9 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy { base.Update(); + if (!isHitting.Value) + (bodySprite as TextureAnimation)?.GotoFrame(0); + if (holdNote.Body.HasHoldBreak) missFadeTime.Value = holdNote.Body.Result.TimeAbsolute; From 36453f621513ee8e7ad5971f3bc7c1a39404d700 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Tue, 28 May 2024 15:56:59 +0200 Subject: [PATCH 079/119] Change scale hotkey to Ctrl+T --- osu.Game/Input/Bindings/GlobalActionContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index 394cb98089..fd8e6fd6d0 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -142,7 +142,7 @@ namespace osu.Game.Input.Bindings new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.MouseWheelRight }, GlobalAction.EditorCyclePreviousBeatSnapDivisor), new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.MouseWheelLeft }, GlobalAction.EditorCycleNextBeatSnapDivisor), new KeyBinding(new[] { InputKey.Control, InputKey.R }, GlobalAction.EditorToggleRotateControl), - new KeyBinding(new[] { InputKey.S }, GlobalAction.EditorToggleScaleControl), + new KeyBinding(new[] { InputKey.Control, InputKey.T }, GlobalAction.EditorToggleScaleControl), }; private static IEnumerable inGameKeyBindings => new[] From a89ba33b475a4f73167f0c81e53eec8f082c16ec Mon Sep 17 00:00:00 2001 From: OliBomby Date: Tue, 28 May 2024 16:14:16 +0200 Subject: [PATCH 080/119] rename CanScaleSelectionOrigin/PlayfieldOrigin to make clear its not the origin being scaled --- .../Edit/OsuSelectionRotationHandler.cs | 4 ++-- osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs | 4 ++-- osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs | 2 +- osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs | 2 +- osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs | 10 +++++----- .../Visual/Editing/TestSceneComposeSelectBox.cs | 2 +- .../SkinEditor/SkinSelectionRotationHandler.cs | 2 +- .../Screens/Edit/Compose/Components/SelectionBox.cs | 2 +- .../Compose/Components/SelectionRotationHandler.cs | 4 ++-- .../Edit/Compose/Components/SelectionScaleHandler.cs | 4 ++-- 10 files changed, 18 insertions(+), 18 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionRotationHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionRotationHandler.cs index d48bc6a90b..b581e3fdea 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionRotationHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionRotationHandler.cs @@ -41,8 +41,8 @@ namespace osu.Game.Rulesets.Osu.Edit private void updateState() { var quad = GeometryUtils.GetSurroundingQuad(selectedMovableObjects); - CanRotateSelectionOrigin.Value = quad.Width > 0 || quad.Height > 0; - CanRotatePlayfieldOrigin.Value = selectedMovableObjects.Any(); + CanRotateFromSelectionOrigin.Value = quad.Width > 0 || quad.Height > 0; + CanRotateFromPlayfieldOrigin.Value = selectedMovableObjects.Any(); } private OsuHitObject[]? objectsInRotation; diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs index e45494977f..a9cbc1b8f1 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs @@ -53,8 +53,8 @@ namespace osu.Game.Rulesets.Osu.Edit CanScaleX.Value = quad.Width > 0; CanScaleY.Value = quad.Height > 0; CanScaleDiagonally.Value = CanScaleX.Value && CanScaleY.Value; - CanScaleSelectionOrigin.Value = CanScaleX.Value || CanScaleY.Value; - CanScalePlayfieldOrigin.Value = selectedMovableObjects.Any(); + CanScaleFromSelectionOrigin.Value = CanScaleX.Value || CanScaleY.Value; + CanScaleFromPlayfieldOrigin.Value = selectedMovableObjects.Any(); } private Dictionary? objectsInScale; diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs index 812d622ae5..ea6b28b215 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs @@ -86,7 +86,7 @@ namespace osu.Game.Rulesets.Osu.Edit angleInput.Current.BindValueChanged(angle => rotationInfo.Value = rotationInfo.Value with { Degrees = angle.NewValue }); rotationOrigin.Items.First().Select(); - rotationHandler.CanRotateSelectionOrigin.BindValueChanged(e => + rotationHandler.CanRotateFromSelectionOrigin.BindValueChanged(e => { selectionCentreButton.Selected.Disabled = !e.NewValue; }, true); diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs index b76d778b3d..124a79390a 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs @@ -118,7 +118,7 @@ namespace osu.Game.Rulesets.Osu.Edit xCheckBox.Current.BindValueChanged(x => setAxis(x.NewValue, yCheckBox.Current.Value)); yCheckBox.Current.BindValueChanged(y => setAxis(xCheckBox.Current.Value, y.NewValue)); - scaleHandler.CanScaleSelectionOrigin.BindValueChanged(e => + scaleHandler.CanScaleFromSelectionOrigin.BindValueChanged(e => { selectionCentreButton.Selected.Disabled = !e.NewValue; }, true); diff --git a/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs b/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs index e1f53846dc..67baf7d165 100644 --- a/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs +++ b/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs @@ -64,15 +64,15 @@ namespace osu.Game.Rulesets.Osu.Edit base.LoadComplete(); // aggregate two values into canRotate - canRotatePlayfieldOrigin = RotationHandler.CanRotatePlayfieldOrigin.GetBoundCopy(); + canRotatePlayfieldOrigin = RotationHandler.CanRotateFromPlayfieldOrigin.GetBoundCopy(); canRotatePlayfieldOrigin.BindValueChanged(_ => updateCanRotateAggregate()); - canRotateSelectionOrigin = RotationHandler.CanRotateSelectionOrigin.GetBoundCopy(); + canRotateSelectionOrigin = RotationHandler.CanRotateFromSelectionOrigin.GetBoundCopy(); canRotateSelectionOrigin.BindValueChanged(_ => updateCanRotateAggregate()); void updateCanRotateAggregate() { - canRotate.Value = RotationHandler.CanRotatePlayfieldOrigin.Value || RotationHandler.CanRotateSelectionOrigin.Value; + canRotate.Value = RotationHandler.CanRotateFromPlayfieldOrigin.Value || RotationHandler.CanRotateFromSelectionOrigin.Value; } // aggregate three values into canScale @@ -82,12 +82,12 @@ namespace osu.Game.Rulesets.Osu.Edit canScaleY = ScaleHandler.CanScaleY.GetBoundCopy(); canScaleY.BindValueChanged(_ => updateCanScaleAggregate()); - canScalePlayfieldOrigin = ScaleHandler.CanScalePlayfieldOrigin.GetBoundCopy(); + canScalePlayfieldOrigin = ScaleHandler.CanScaleFromPlayfieldOrigin.GetBoundCopy(); canScalePlayfieldOrigin.BindValueChanged(_ => updateCanScaleAggregate()); void updateCanScaleAggregate() { - canScale.Value = ScaleHandler.CanScaleX.Value || ScaleHandler.CanScaleY.Value || ScaleHandler.CanScalePlayfieldOrigin.Value; + canScale.Value = ScaleHandler.CanScaleX.Value || ScaleHandler.CanScaleY.Value || ScaleHandler.CanScaleFromPlayfieldOrigin.Value; } // bindings to `Enabled` on the buttons are decoupled on purpose diff --git a/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs b/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs index 6bd6c4a8c4..d12f7ebde9 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs @@ -69,7 +69,7 @@ namespace osu.Game.Tests.Visual.Editing { this.getTargetContainer = getTargetContainer; - CanRotateSelectionOrigin.Value = true; + CanRotateFromSelectionOrigin.Value = true; } [CanBeNull] diff --git a/osu.Game/Overlays/SkinEditor/SkinSelectionRotationHandler.cs b/osu.Game/Overlays/SkinEditor/SkinSelectionRotationHandler.cs index 7ecf116b68..3a3eb9457b 100644 --- a/osu.Game/Overlays/SkinEditor/SkinSelectionRotationHandler.cs +++ b/osu.Game/Overlays/SkinEditor/SkinSelectionRotationHandler.cs @@ -41,7 +41,7 @@ namespace osu.Game.Overlays.SkinEditor private void updateState() { - CanRotateSelectionOrigin.Value = selectedItems.Count > 0; + CanRotateFromSelectionOrigin.Value = selectedItems.Count > 0; } private Drawable[]? objectsInRotation; diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs index 31a5c30fff..9f709f8c64 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs @@ -127,7 +127,7 @@ namespace osu.Game.Screens.Edit.Compose.Components private void load() { if (rotationHandler != null) - canRotate.BindTo(rotationHandler.CanRotateSelectionOrigin); + canRotate.BindTo(rotationHandler.CanRotateFromSelectionOrigin); if (scaleHandler != null) { diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs index 459e4b0c41..8c35dc07b7 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs @@ -15,12 +15,12 @@ namespace osu.Game.Screens.Edit.Compose.Components /// /// Whether rotation anchored by the selection origin can currently be performed. /// - public Bindable CanRotateSelectionOrigin { get; private set; } = new BindableBool(); + public Bindable CanRotateFromSelectionOrigin { get; private set; } = new BindableBool(); /// /// Whether rotation anchored by the center of the playfield can currently be performed. /// - public Bindable CanRotatePlayfieldOrigin { get; private set; } = new BindableBool(); + public Bindable CanRotateFromPlayfieldOrigin { get; private set; } = new BindableBool(); /// /// Performs a single, instant, atomic rotation operation. diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionScaleHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionScaleHandler.cs index 495cce7ad6..b72c3406f1 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionScaleHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionScaleHandler.cs @@ -35,12 +35,12 @@ namespace osu.Game.Screens.Edit.Compose.Components /// /// Whether scaling anchored by the selection origin can currently be performed. /// - public Bindable CanScaleSelectionOrigin { get; private set; } = new BindableBool(); + public Bindable CanScaleFromSelectionOrigin { get; private set; } = new BindableBool(); /// /// Whether scaling anchored by the center of the playfield can currently be performed. /// - public Bindable CanScalePlayfieldOrigin { get; private set; } = new BindableBool(); + public Bindable CanScaleFromPlayfieldOrigin { get; private set; } = new BindableBool(); public Quad? OriginalSurroundingQuad { get; protected set; } From 8eb23f8a604818b6ce94dc0810a551c2a5455a4d Mon Sep 17 00:00:00 2001 From: OliBomby Date: Tue, 28 May 2024 16:19:57 +0200 Subject: [PATCH 081/119] remove redundant CanScaleFromSelectionOrigin --- .../Edit/OsuSelectionScaleHandler.cs | 1 - .../Edit/PreciseScalePopover.cs | 16 ++++++++++++---- .../Compose/Components/SelectionScaleHandler.cs | 5 ----- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs index a9cbc1b8f1..f120c8bd75 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs @@ -53,7 +53,6 @@ namespace osu.Game.Rulesets.Osu.Edit CanScaleX.Value = quad.Width > 0; CanScaleY.Value = quad.Height > 0; CanScaleDiagonally.Value = CanScaleX.Value && CanScaleY.Value; - CanScaleFromSelectionOrigin.Value = CanScaleX.Value || CanScaleY.Value; CanScaleFromPlayfieldOrigin.Value = selectedMovableObjects.Any(); } diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs index 124a79390a..dca262cf5a 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs @@ -32,6 +32,9 @@ namespace osu.Game.Rulesets.Osu.Edit private OsuCheckbox xCheckBox = null!; private OsuCheckbox yCheckBox = null!; + private Bindable canScaleX = null!; + private Bindable canScaleY = null!; + public PreciseScalePopover(SelectionScaleHandler scaleHandler) { this.scaleHandler = scaleHandler; @@ -118,10 +121,15 @@ namespace osu.Game.Rulesets.Osu.Edit xCheckBox.Current.BindValueChanged(x => setAxis(x.NewValue, yCheckBox.Current.Value)); yCheckBox.Current.BindValueChanged(y => setAxis(xCheckBox.Current.Value, y.NewValue)); - scaleHandler.CanScaleFromSelectionOrigin.BindValueChanged(e => - { - selectionCentreButton.Selected.Disabled = !e.NewValue; - }, true); + // aggregate two values into canScaleFromSelectionCentre + canScaleX = scaleHandler.CanScaleX.GetBoundCopy(); + canScaleX.BindValueChanged(_ => updateCanScaleFromSelectionCentre()); + + canScaleY = scaleHandler.CanScaleY.GetBoundCopy(); + canScaleY.BindValueChanged(_ => updateCanScaleFromSelectionCentre(), true); + + void updateCanScaleFromSelectionCentre() => + selectionCentreButton.Selected.Disabled = !(scaleHandler.CanScaleY.Value || scaleHandler.CanScaleFromPlayfieldOrigin.Value); scaleInfo.BindValueChanged(scale => { diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionScaleHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionScaleHandler.cs index b72c3406f1..2c8b413560 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionScaleHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionScaleHandler.cs @@ -32,11 +32,6 @@ namespace osu.Game.Screens.Edit.Compose.Components /// public Bindable CanScaleDiagonally { get; private set; } = new BindableBool(); - /// - /// Whether scaling anchored by the selection origin can currently be performed. - /// - public Bindable CanScaleFromSelectionOrigin { get; private set; } = new BindableBool(); - /// /// Whether scaling anchored by the center of the playfield can currently be performed. /// From 7cdc755c1614ba666d63d3eba5d8062a9313e1a4 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Tue, 28 May 2024 16:57:24 +0200 Subject: [PATCH 082/119] Bind axis checkbox disabled state to CanScaleX/Y --- .../Edit/PreciseScalePopover.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs index dca262cf5a..ac6e2849c9 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs @@ -35,6 +35,8 @@ namespace osu.Game.Rulesets.Osu.Edit private Bindable canScaleX = null!; private Bindable canScaleY = null!; + private bool scaleInProgress; + public PreciseScalePopover(SelectionScaleHandler scaleHandler) { this.scaleHandler = scaleHandler; @@ -124,15 +126,26 @@ namespace osu.Game.Rulesets.Osu.Edit // aggregate two values into canScaleFromSelectionCentre canScaleX = scaleHandler.CanScaleX.GetBoundCopy(); canScaleX.BindValueChanged(_ => updateCanScaleFromSelectionCentre()); + canScaleX.BindValueChanged(e => updateAxisCheckBoxEnabled(e.NewValue, xCheckBox.Current), true); canScaleY = scaleHandler.CanScaleY.GetBoundCopy(); canScaleY.BindValueChanged(_ => updateCanScaleFromSelectionCentre(), true); + canScaleY.BindValueChanged(e => updateAxisCheckBoxEnabled(e.NewValue, yCheckBox.Current), true); void updateCanScaleFromSelectionCentre() => selectionCentreButton.Selected.Disabled = !(scaleHandler.CanScaleY.Value || scaleHandler.CanScaleFromPlayfieldOrigin.Value); + void updateAxisCheckBoxEnabled(bool enabled, Bindable current) + { + current.Disabled = false; // enable the bindable to allow setting the value + current.Value = enabled; + current.Disabled = !enabled; + } + scaleInfo.BindValueChanged(scale => { + if (!scaleInProgress) return; + var newScale = new Vector2(scale.NewValue.XAxis ? scale.NewValue.Scale : 1, scale.NewValue.YAxis ? scale.NewValue.Scale : 1); scaleHandler.Update(newScale, getOriginPosition(scale.NewValue)); }); @@ -172,6 +185,7 @@ namespace osu.Game.Rulesets.Osu.Edit { base.PopIn(); scaleHandler.Begin(); + scaleInProgress = true; updateMaxScale(); } @@ -180,7 +194,10 @@ namespace osu.Game.Rulesets.Osu.Edit base.PopOut(); if (IsLoaded) + { scaleHandler.Commit(); + scaleInProgress = false; + } } } From d143a697d2efbd7d57fc7d7ff7d3d4e3cfc13a7d Mon Sep 17 00:00:00 2001 From: OliBomby Date: Tue, 28 May 2024 17:12:16 +0200 Subject: [PATCH 083/119] refactor CanScaleFromPlayfieldOrigin and GetClampedScale to derived class --- osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs | 2 +- .../Edit/OsuSelectionScaleHandler.cs | 13 ++++++++++++- osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs | 5 ++--- osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs | 2 +- .../Compose/Components/SelectionScaleHandler.cs | 13 ------------- 5 files changed, 16 insertions(+), 19 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index 6f3ed9730e..cc1d1fe89f 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -104,7 +104,7 @@ namespace osu.Game.Rulesets.Osu.Edit new TransformToolboxGroup { RotationHandler = BlueprintContainer.SelectionHandler.RotationHandler, - ScaleHandler = BlueprintContainer.SelectionHandler.ScaleHandler, + ScaleHandler = (OsuSelectionScaleHandler)BlueprintContainer.SelectionHandler.ScaleHandler, }, FreehandlSliderToolboxGroup } diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs index f120c8bd75..32057bd7fe 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs @@ -24,6 +24,11 @@ namespace osu.Game.Rulesets.Osu.Edit { public partial class OsuSelectionScaleHandler : SelectionScaleHandler { + /// + /// Whether scaling anchored by the center of the playfield can currently be performed. + /// + public Bindable CanScaleFromPlayfieldOrigin { get; private set; } = new BindableBool(); + [Resolved] private IEditorChangeHandler? changeHandler { get; set; } @@ -156,7 +161,13 @@ namespace osu.Game.Rulesets.Osu.Edit return (xInBounds, yInBounds); } - public override Vector2 GetClampedScale(Vector2 scale, Vector2? origin = null) + /// + /// Clamp scale for multi-object-scaling where selection does not exceed playfield bounds or flip. + /// + /// The origin from which the scale operation is performed + /// The scale to be clamped + /// The clamped scale vector + public Vector2 GetClampedScale(Vector2 scale, Vector2? origin = null) { //todo: this is not always correct for selections involving sliders. This approximation assumes each point is scaled independently, but sliderends move with the sliderhead. if (objectsInScale == null) diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs index ac6e2849c9..f14e166d3b 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs @@ -12,14 +12,13 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Rulesets.Osu.UI; using osu.Game.Screens.Edit.Components.RadioButtons; -using osu.Game.Screens.Edit.Compose.Components; using osuTK; namespace osu.Game.Rulesets.Osu.Edit { public partial class PreciseScalePopover : OsuPopover { - private readonly SelectionScaleHandler scaleHandler; + private readonly OsuSelectionScaleHandler scaleHandler; private readonly Bindable scaleInfo = new Bindable(new PreciseScaleInfo(1, ScaleOrigin.PlayfieldCentre, true, true)); @@ -37,7 +36,7 @@ namespace osu.Game.Rulesets.Osu.Edit private bool scaleInProgress; - public PreciseScalePopover(SelectionScaleHandler scaleHandler) + public PreciseScalePopover(OsuSelectionScaleHandler scaleHandler) { this.scaleHandler = scaleHandler; diff --git a/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs b/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs index 67baf7d165..e70be8d93c 100644 --- a/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs +++ b/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs @@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Osu.Edit private Bindable canScalePlayfieldOrigin = null!; public SelectionRotationHandler RotationHandler { get; init; } = null!; - public SelectionScaleHandler ScaleHandler { get; init; } = null!; + public OsuSelectionScaleHandler ScaleHandler { get; init; } = null!; public TransformToolboxGroup() : base("transform") diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionScaleHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionScaleHandler.cs index 2c8b413560..a96f627e56 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionScaleHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionScaleHandler.cs @@ -32,21 +32,8 @@ namespace osu.Game.Screens.Edit.Compose.Components /// public Bindable CanScaleDiagonally { get; private set; } = new BindableBool(); - /// - /// Whether scaling anchored by the center of the playfield can currently be performed. - /// - public Bindable CanScaleFromPlayfieldOrigin { get; private set; } = new BindableBool(); - public Quad? OriginalSurroundingQuad { get; protected set; } - /// - /// Clamp scale where selection does not exceed playfield bounds or flip. - /// - /// The origin from which the scale operation is performed - /// The scale to be clamped - /// The clamped scale vector - public virtual Vector2 GetClampedScale(Vector2 scale, Vector2? origin = null) => scale; - /// /// Performs a single, instant, atomic scale operation. /// From 9548585b15fe4154e7e7da80d21da9779486e898 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Tue, 28 May 2024 17:24:31 +0200 Subject: [PATCH 084/119] fix axis checkboxes being disabled in playfield origin scale --- .../Edit/PreciseScalePopover.cs | 35 +++++++++++++------ 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs index f14e166d3b..d45c4020b9 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs @@ -125,21 +125,14 @@ namespace osu.Game.Rulesets.Osu.Edit // aggregate two values into canScaleFromSelectionCentre canScaleX = scaleHandler.CanScaleX.GetBoundCopy(); canScaleX.BindValueChanged(_ => updateCanScaleFromSelectionCentre()); - canScaleX.BindValueChanged(e => updateAxisCheckBoxEnabled(e.NewValue, xCheckBox.Current), true); + canScaleX.BindValueChanged(e => updateAxisCheckBoxesEnabled()); canScaleY = scaleHandler.CanScaleY.GetBoundCopy(); canScaleY.BindValueChanged(_ => updateCanScaleFromSelectionCentre(), true); - canScaleY.BindValueChanged(e => updateAxisCheckBoxEnabled(e.NewValue, yCheckBox.Current), true); + canScaleY.BindValueChanged(e => updateAxisCheckBoxesEnabled(), true); void updateCanScaleFromSelectionCentre() => - selectionCentreButton.Selected.Disabled = !(scaleHandler.CanScaleY.Value || scaleHandler.CanScaleFromPlayfieldOrigin.Value); - - void updateAxisCheckBoxEnabled(bool enabled, Bindable current) - { - current.Disabled = false; // enable the bindable to allow setting the value - current.Value = enabled; - current.Disabled = !enabled; - } + selectionCentreButton.Selected.Disabled = !(scaleHandler.CanScaleX.Value || scaleHandler.CanScaleY.Value); scaleInfo.BindValueChanged(scale => { @@ -150,6 +143,27 @@ namespace osu.Game.Rulesets.Osu.Edit }); } + private void updateAxisCheckBoxesEnabled() + { + if (scaleInfo.Value.Origin == ScaleOrigin.PlayfieldCentre) + { + setBindableEnabled(true, xCheckBox.Current); + setBindableEnabled(true, yCheckBox.Current); + } + else + { + setBindableEnabled(canScaleX.Value, xCheckBox.Current); + setBindableEnabled(canScaleY.Value, yCheckBox.Current); + } + } + + private void setBindableEnabled(bool enabled, Bindable current) + { + current.Disabled = false; // enable the bindable to allow setting the value + current.Value = enabled; + current.Disabled = !enabled; + } + private void updateMaxScale() { if (!scaleHandler.OriginalSurroundingQuad.HasValue) @@ -170,6 +184,7 @@ namespace osu.Game.Rulesets.Osu.Edit { scaleInfo.Value = scaleInfo.Value with { Origin = origin }; updateMaxScale(); + updateAxisCheckBoxesEnabled(); } private Vector2? getOriginPosition(PreciseScaleInfo scale) => scale.Origin == ScaleOrigin.PlayfieldCentre ? OsuPlayfield.BASE_SIZE / 2 : null; From 9a18ba2699883a0278adba889b2141b3e21f044b Mon Sep 17 00:00:00 2001 From: OliBomby Date: Tue, 28 May 2024 18:27:01 +0200 Subject: [PATCH 085/119] disable playfield centre origin when scaling slider and simplify logic --- .../Edit/OsuSelectionScaleHandler.cs | 6 +++ .../Edit/PreciseScalePopover.cs | 42 +++++++------------ 2 files changed, 21 insertions(+), 27 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs index 32057bd7fe..9d1c3bc78f 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs @@ -29,6 +29,11 @@ namespace osu.Game.Rulesets.Osu.Edit /// public Bindable CanScaleFromPlayfieldOrigin { get; private set; } = new BindableBool(); + /// + /// Whether a single slider is currently selected, which results in a different scaling behaviour. + /// + public Bindable IsScalingSlider { get; private set; } = new BindableBool(); + [Resolved] private IEditorChangeHandler? changeHandler { get; set; } @@ -59,6 +64,7 @@ namespace osu.Game.Rulesets.Osu.Edit CanScaleY.Value = quad.Height > 0; CanScaleDiagonally.Value = CanScaleX.Value && CanScaleY.Value; CanScaleFromPlayfieldOrigin.Value = selectedMovableObjects.Any(); + IsScalingSlider.Value = selectedMovableObjects.Count() == 1 && selectedMovableObjects.First() is Slider; } private Dictionary? objectsInScale; diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs index d45c4020b9..b792baf428 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs @@ -26,16 +26,12 @@ namespace osu.Game.Rulesets.Osu.Edit private BindableNumber scaleInputBindable = null!; private EditorRadioButtonCollection scaleOrigin = null!; + private RadioButton playfieldCentreButton = null!; private RadioButton selectionCentreButton = null!; private OsuCheckbox xCheckBox = null!; private OsuCheckbox yCheckBox = null!; - private Bindable canScaleX = null!; - private Bindable canScaleY = null!; - - private bool scaleInProgress; - public PreciseScalePopover(OsuSelectionScaleHandler scaleHandler) { this.scaleHandler = scaleHandler; @@ -70,7 +66,7 @@ namespace osu.Game.Rulesets.Osu.Edit RelativeSizeAxes = Axes.X, Items = new[] { - new RadioButton("Playfield centre", + playfieldCentreButton = new RadioButton("Playfield centre", () => setOrigin(ScaleOrigin.PlayfieldCentre), () => new SpriteIcon { Icon = FontAwesome.Regular.Square }), selectionCentreButton = new RadioButton("Selection centre", @@ -101,6 +97,10 @@ namespace osu.Game.Rulesets.Osu.Edit }, } }; + playfieldCentreButton.Selected.DisabledChanged += isDisabled => + { + playfieldCentreButton.TooltipText = isDisabled ? "Select something other than a single slider to perform playfield-based scaling." : string.Empty; + }; selectionCentreButton.Selected.DisabledChanged += isDisabled => { selectionCentreButton.TooltipText = isDisabled ? "Select more than one object to perform selection-based scaling." : string.Empty; @@ -117,27 +117,20 @@ namespace osu.Game.Rulesets.Osu.Edit scaleInput.SelectAll(); }); scaleInput.Current.BindValueChanged(scale => scaleInfo.Value = scaleInfo.Value with { Scale = scale.NewValue }); - scaleOrigin.Items.First().Select(); xCheckBox.Current.BindValueChanged(x => setAxis(x.NewValue, yCheckBox.Current.Value)); yCheckBox.Current.BindValueChanged(y => setAxis(xCheckBox.Current.Value, y.NewValue)); - // aggregate two values into canScaleFromSelectionCentre - canScaleX = scaleHandler.CanScaleX.GetBoundCopy(); - canScaleX.BindValueChanged(_ => updateCanScaleFromSelectionCentre()); - canScaleX.BindValueChanged(e => updateAxisCheckBoxesEnabled()); + selectionCentreButton.Selected.Disabled = !(scaleHandler.CanScaleX.Value || scaleHandler.CanScaleY.Value); + playfieldCentreButton.Selected.Disabled = scaleHandler.IsScalingSlider.Value && !selectionCentreButton.Selected.Disabled; - canScaleY = scaleHandler.CanScaleY.GetBoundCopy(); - canScaleY.BindValueChanged(_ => updateCanScaleFromSelectionCentre(), true); - canScaleY.BindValueChanged(e => updateAxisCheckBoxesEnabled(), true); - - void updateCanScaleFromSelectionCentre() => - selectionCentreButton.Selected.Disabled = !(scaleHandler.CanScaleX.Value || scaleHandler.CanScaleY.Value); + if (playfieldCentreButton.Selected.Disabled) + scaleOrigin.Items.Last().Select(); + else + scaleOrigin.Items.First().Select(); scaleInfo.BindValueChanged(scale => { - if (!scaleInProgress) return; - var newScale = new Vector2(scale.NewValue.XAxis ? scale.NewValue.Scale : 1, scale.NewValue.YAxis ? scale.NewValue.Scale : 1); scaleHandler.Update(newScale, getOriginPosition(scale.NewValue)); }); @@ -152,8 +145,8 @@ namespace osu.Game.Rulesets.Osu.Edit } else { - setBindableEnabled(canScaleX.Value, xCheckBox.Current); - setBindableEnabled(canScaleY.Value, yCheckBox.Current); + setBindableEnabled(scaleHandler.CanScaleX.Value, xCheckBox.Current); + setBindableEnabled(scaleHandler.CanScaleY.Value, yCheckBox.Current); } } @@ -199,7 +192,6 @@ namespace osu.Game.Rulesets.Osu.Edit { base.PopIn(); scaleHandler.Begin(); - scaleInProgress = true; updateMaxScale(); } @@ -207,11 +199,7 @@ namespace osu.Game.Rulesets.Osu.Edit { base.PopOut(); - if (IsLoaded) - { - scaleHandler.Commit(); - scaleInProgress = false; - } + if (IsLoaded) scaleHandler.Commit(); } } From b74f66e3351457ef7233c277c88b18db7e174f2a Mon Sep 17 00:00:00 2001 From: Aurelian Date: Tue, 28 May 2024 19:38:33 +0200 Subject: [PATCH 086/119] SliderBall's rotation updates based on CurvePositionAt --- .../Objects/Drawables/DrawableSliderBall.cs | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs index 46f0231981..24c0d0fcf0 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs @@ -10,9 +10,7 @@ using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Skinning.Default; -using osu.Game.Screens.Play; using osu.Game.Skinning; -using osuTK; namespace osu.Game.Rulesets.Osu.Objects.Drawables { @@ -63,22 +61,20 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables base.ApplyTransformsAt(time, false); } - private Vector2? lastPosition; - public void UpdateProgress(double completionProgress) { - Position = drawableSlider.HitObject.CurvePositionAt(completionProgress); + Slider slider = drawableSlider.HitObject; + Position = slider.CurvePositionAt(completionProgress); - var diff = lastPosition.HasValue ? lastPosition.Value - Position : Position - drawableSlider.HitObject.CurvePositionAt(completionProgress + 0.01f); - - bool rewinding = (Clock as IGameplayClock)?.IsRewinding == true; + //0.1 / slider.Path.Distance is the additional progress needed to ensure the diff length is 0.1 + var diff = slider.CurvePositionAt(completionProgress) - slider.CurvePositionAt(Math.Min(1, completionProgress + 0.1 / slider.Path.Distance)); // Ensure the value is substantially high enough to allow for Atan2 to get a valid angle. + // Needed for when near completion, or in case of a very short slider. if (diff.LengthFast < 0.01f) return; - ball.Rotation = -90 + (float)(-Math.Atan2(diff.X, diff.Y) * 180 / Math.PI) + (rewinding ? 180 : 0); - lastPosition = Position; + ball.Rotation = -90 + (float)(-Math.Atan2(diff.X, diff.Y) * 180 / Math.PI); } } } From 542809a748072f8438b9289027bafd018a33c5f3 Mon Sep 17 00:00:00 2001 From: Aurelian Date: Wed, 29 May 2024 09:39:46 +0200 Subject: [PATCH 087/119] Reduced subpoints limit to be a more practical value --- osu.Game/Rulesets/Objects/SliderPath.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/Objects/SliderPath.cs b/osu.Game/Rulesets/Objects/SliderPath.cs index aa2570c336..730a2013b0 100644 --- a/osu.Game/Rulesets/Objects/SliderPath.cs +++ b/osu.Game/Rulesets/Objects/SliderPath.cs @@ -339,9 +339,10 @@ namespace osu.Game.Rulesets.Objects //Coppied from PathApproximator.CircularArcToPiecewiseLinear int subPoints = (2f * circularArcProperties.Radius <= 0.1f) ? 2 : Math.Max(2, (int)Math.Ceiling(circularArcProperties.ThetaRange / (2.0 * Math.Acos(1f - (0.1f / circularArcProperties.Radius))))); - //if the amount of subpoints is int.MaxValue, causes an out of memory issue, so we default to bezier - //this only occurs for very large radii, so the result should be essentially a straight line anyways - if (subPoints == int.MaxValue) + //theoretically can be int.MaxValue, but lets set this to a lower value anyways + //1000 requires an arc length of over 20 thousand to surpass this limit, which should be safe. + //See here for calculations https://www.desmos.com/calculator/210bwswkbb + if (subPoints >= 1000) break; List subPath = PathApproximator.CircularArcToPiecewiseLinear(subControlPoints); From 4c881b56331626feb5272e0b33442ec388765acf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 29 May 2024 09:40:29 +0200 Subject: [PATCH 088/119] Use better name if we're renaming --- osu.Game.Rulesets.Osu/Edit/OsuSelectionRotationHandler.cs | 4 ++-- osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs | 2 +- osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs | 6 +++--- osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs | 2 +- .../Overlays/SkinEditor/SkinSelectionRotationHandler.cs | 2 +- osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs | 2 +- .../Edit/Compose/Components/SelectionRotationHandler.cs | 4 ++-- 7 files changed, 11 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionRotationHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionRotationHandler.cs index b581e3fdea..7624b2f27e 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionRotationHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionRotationHandler.cs @@ -41,8 +41,8 @@ namespace osu.Game.Rulesets.Osu.Edit private void updateState() { var quad = GeometryUtils.GetSurroundingQuad(selectedMovableObjects); - CanRotateFromSelectionOrigin.Value = quad.Width > 0 || quad.Height > 0; - CanRotateFromPlayfieldOrigin.Value = selectedMovableObjects.Any(); + CanRotateAroundSelectionOrigin.Value = quad.Width > 0 || quad.Height > 0; + CanRotateAroundPlayfieldOrigin.Value = selectedMovableObjects.Any(); } private OsuHitObject[]? objectsInRotation; diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs index ea6b28b215..3a0d3c4763 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs @@ -86,7 +86,7 @@ namespace osu.Game.Rulesets.Osu.Edit angleInput.Current.BindValueChanged(angle => rotationInfo.Value = rotationInfo.Value with { Degrees = angle.NewValue }); rotationOrigin.Items.First().Select(); - rotationHandler.CanRotateFromSelectionOrigin.BindValueChanged(e => + rotationHandler.CanRotateAroundSelectionOrigin.BindValueChanged(e => { selectionCentreButton.Selected.Disabled = !e.NewValue; }, true); diff --git a/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs b/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs index e70be8d93c..4da1593fb7 100644 --- a/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs +++ b/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs @@ -64,15 +64,15 @@ namespace osu.Game.Rulesets.Osu.Edit base.LoadComplete(); // aggregate two values into canRotate - canRotatePlayfieldOrigin = RotationHandler.CanRotateFromPlayfieldOrigin.GetBoundCopy(); + canRotatePlayfieldOrigin = RotationHandler.CanRotateAroundPlayfieldOrigin.GetBoundCopy(); canRotatePlayfieldOrigin.BindValueChanged(_ => updateCanRotateAggregate()); - canRotateSelectionOrigin = RotationHandler.CanRotateFromSelectionOrigin.GetBoundCopy(); + canRotateSelectionOrigin = RotationHandler.CanRotateAroundSelectionOrigin.GetBoundCopy(); canRotateSelectionOrigin.BindValueChanged(_ => updateCanRotateAggregate()); void updateCanRotateAggregate() { - canRotate.Value = RotationHandler.CanRotateFromPlayfieldOrigin.Value || RotationHandler.CanRotateFromSelectionOrigin.Value; + canRotate.Value = RotationHandler.CanRotateAroundPlayfieldOrigin.Value || RotationHandler.CanRotateAroundSelectionOrigin.Value; } // aggregate three values into canScale diff --git a/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs b/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs index d12f7ebde9..79a808bbd2 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs @@ -69,7 +69,7 @@ namespace osu.Game.Tests.Visual.Editing { this.getTargetContainer = getTargetContainer; - CanRotateFromSelectionOrigin.Value = true; + CanRotateAroundSelectionOrigin.Value = true; } [CanBeNull] diff --git a/osu.Game/Overlays/SkinEditor/SkinSelectionRotationHandler.cs b/osu.Game/Overlays/SkinEditor/SkinSelectionRotationHandler.cs index 3a3eb9457b..6a118a73a8 100644 --- a/osu.Game/Overlays/SkinEditor/SkinSelectionRotationHandler.cs +++ b/osu.Game/Overlays/SkinEditor/SkinSelectionRotationHandler.cs @@ -41,7 +41,7 @@ namespace osu.Game.Overlays.SkinEditor private void updateState() { - CanRotateFromSelectionOrigin.Value = selectedItems.Count > 0; + CanRotateAroundSelectionOrigin.Value = selectedItems.Count > 0; } private Drawable[]? objectsInRotation; diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs index 9f709f8c64..fec3224fad 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs @@ -127,7 +127,7 @@ namespace osu.Game.Screens.Edit.Compose.Components private void load() { if (rotationHandler != null) - canRotate.BindTo(rotationHandler.CanRotateFromSelectionOrigin); + canRotate.BindTo(rotationHandler.CanRotateAroundSelectionOrigin); if (scaleHandler != null) { diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs index 8c35dc07b7..787716a38c 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs @@ -15,12 +15,12 @@ namespace osu.Game.Screens.Edit.Compose.Components /// /// Whether rotation anchored by the selection origin can currently be performed. /// - public Bindable CanRotateFromSelectionOrigin { get; private set; } = new BindableBool(); + public Bindable CanRotateAroundSelectionOrigin { get; private set; } = new BindableBool(); /// /// Whether rotation anchored by the center of the playfield can currently be performed. /// - public Bindable CanRotateFromPlayfieldOrigin { get; private set; } = new BindableBool(); + public Bindable CanRotateAroundPlayfieldOrigin { get; private set; } = new BindableBool(); /// /// Performs a single, instant, atomic rotation operation. From 4a8273b6ed8defecef61bcf6e77a937372a1de4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 29 May 2024 09:43:09 +0200 Subject: [PATCH 089/119] Rename another method --- osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs | 4 ++-- osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs index 9d1c3bc78f..de00aa6ad3 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs @@ -104,7 +104,7 @@ namespace osu.Game.Rulesets.Osu.Edit } else { - scale = GetClampedScale(scale, actualOrigin); + scale = ClampScaleToPlayfieldBounds(scale, actualOrigin); foreach (var (ho, originalState) in objectsInScale) { @@ -173,7 +173,7 @@ namespace osu.Game.Rulesets.Osu.Edit /// The origin from which the scale operation is performed /// The scale to be clamped /// The clamped scale vector - public Vector2 GetClampedScale(Vector2 scale, Vector2? origin = null) + public Vector2 ClampScaleToPlayfieldBounds(Vector2 scale, Vector2? origin = null) { //todo: this is not always correct for selections involving sliders. This approximation assumes each point is scaled independently, but sliderends move with the sliderhead. if (objectsInScale == null) diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs index b792baf428..b7202b9310 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs @@ -163,7 +163,7 @@ namespace osu.Game.Rulesets.Osu.Edit return; const float max_scale = 10; - var scale = scaleHandler.GetClampedScale(new Vector2(max_scale), getOriginPosition(scaleInfo.Value)); + var scale = scaleHandler.ClampScaleToPlayfieldBounds(new Vector2(max_scale), getOriginPosition(scaleInfo.Value)); if (!scaleInfo.Value.XAxis) scale.X = max_scale; From bd5060965f4a9e0fcbed9fc6044126e66e5d36d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 29 May 2024 09:49:16 +0200 Subject: [PATCH 090/119] Simplify toolbox button enable logic --- .../Edit/TransformToolboxGroup.cs | 38 +++++-------------- 1 file changed, 9 insertions(+), 29 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs b/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs index 4da1593fb7..278d38b2d9 100644 --- a/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs +++ b/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs @@ -18,8 +18,8 @@ namespace osu.Game.Rulesets.Osu.Edit { public partial class TransformToolboxGroup : EditorToolboxGroup, IKeyBindingHandler { - private readonly Bindable canRotate = new BindableBool(); - private readonly Bindable canScale = new BindableBool(); + private readonly AggregateBindable canRotate = new AggregateBindable((x, y) => x || y); + private readonly AggregateBindable canScale = new AggregateBindable((x, y) => x || y); private EditorToolButton rotateButton = null!; private EditorToolButton scaleButton = null!; @@ -63,37 +63,17 @@ namespace osu.Game.Rulesets.Osu.Edit { base.LoadComplete(); - // aggregate two values into canRotate - canRotatePlayfieldOrigin = RotationHandler.CanRotateAroundPlayfieldOrigin.GetBoundCopy(); - canRotatePlayfieldOrigin.BindValueChanged(_ => updateCanRotateAggregate()); + canRotate.AddSource(RotationHandler.CanRotateAroundPlayfieldOrigin); + canRotate.AddSource(RotationHandler.CanRotateAroundSelectionOrigin); - canRotateSelectionOrigin = RotationHandler.CanRotateAroundSelectionOrigin.GetBoundCopy(); - canRotateSelectionOrigin.BindValueChanged(_ => updateCanRotateAggregate()); - - void updateCanRotateAggregate() - { - canRotate.Value = RotationHandler.CanRotateAroundPlayfieldOrigin.Value || RotationHandler.CanRotateAroundSelectionOrigin.Value; - } - - // aggregate three values into canScale - canScaleX = ScaleHandler.CanScaleX.GetBoundCopy(); - canScaleX.BindValueChanged(_ => updateCanScaleAggregate()); - - canScaleY = ScaleHandler.CanScaleY.GetBoundCopy(); - canScaleY.BindValueChanged(_ => updateCanScaleAggregate()); - - canScalePlayfieldOrigin = ScaleHandler.CanScaleFromPlayfieldOrigin.GetBoundCopy(); - canScalePlayfieldOrigin.BindValueChanged(_ => updateCanScaleAggregate()); - - void updateCanScaleAggregate() - { - canScale.Value = ScaleHandler.CanScaleX.Value || ScaleHandler.CanScaleY.Value || ScaleHandler.CanScaleFromPlayfieldOrigin.Value; - } + canScale.AddSource(ScaleHandler.CanScaleX); + canScale.AddSource(ScaleHandler.CanScaleY); + canScale.AddSource(ScaleHandler.CanScaleFromPlayfieldOrigin); // bindings to `Enabled` on the buttons are decoupled on purpose // due to the weird `OsuButton` behaviour of resetting `Enabled` to `false` when `Action` is set. - canRotate.BindValueChanged(_ => rotateButton.Enabled.Value = canRotate.Value, true); - canScale.BindValueChanged(_ => scaleButton.Enabled.Value = canScale.Value, true); + canRotate.Result.BindValueChanged(rotate => rotateButton.Enabled.Value = rotate.NewValue, true); + canScale.Result.BindValueChanged(scale => scaleButton.Enabled.Value = scale.NewValue, true); } public bool OnPressed(KeyBindingPressEvent e) From 96a8bdf92064f04c00120a3274b451cf9ebd1ea9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 29 May 2024 09:59:19 +0200 Subject: [PATCH 091/119] Use more generic tooltip copy --- osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs index b7202b9310..ac1b40e235 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs @@ -99,11 +99,11 @@ namespace osu.Game.Rulesets.Osu.Edit }; playfieldCentreButton.Selected.DisabledChanged += isDisabled => { - playfieldCentreButton.TooltipText = isDisabled ? "Select something other than a single slider to perform playfield-based scaling." : string.Empty; + playfieldCentreButton.TooltipText = isDisabled ? "The current selection cannot be scaled relative to playfield centre." : string.Empty; }; selectionCentreButton.Selected.DisabledChanged += isDisabled => { - selectionCentreButton.TooltipText = isDisabled ? "Select more than one object to perform selection-based scaling." : string.Empty; + selectionCentreButton.TooltipText = isDisabled ? "The current selection cannot be scaled relative to its centre." : string.Empty; }; } From ba4073735649eaf4a581ce3f0ae1f566fab38ddc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 29 May 2024 10:01:04 +0200 Subject: [PATCH 092/119] Simplify logic --- osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs index ac1b40e235..d3e1b491b0 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs @@ -124,10 +124,7 @@ namespace osu.Game.Rulesets.Osu.Edit selectionCentreButton.Selected.Disabled = !(scaleHandler.CanScaleX.Value || scaleHandler.CanScaleY.Value); playfieldCentreButton.Selected.Disabled = scaleHandler.IsScalingSlider.Value && !selectionCentreButton.Selected.Disabled; - if (playfieldCentreButton.Selected.Disabled) - scaleOrigin.Items.Last().Select(); - else - scaleOrigin.Items.First().Select(); + scaleOrigin.Items.First(b => !b.Selected.Disabled).Select(); scaleInfo.BindValueChanged(scale => { From 9bd4b0d61303fb0dde3892d51b595174a61a2b4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 29 May 2024 10:04:49 +0200 Subject: [PATCH 093/119] Rename method --- .../Edit/PreciseScalePopover.cs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs index d3e1b491b0..a299eebbce 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs @@ -137,21 +137,23 @@ namespace osu.Game.Rulesets.Osu.Edit { if (scaleInfo.Value.Origin == ScaleOrigin.PlayfieldCentre) { - setBindableEnabled(true, xCheckBox.Current); - setBindableEnabled(true, yCheckBox.Current); + toggleAxisAvailable(xCheckBox.Current, true); + toggleAxisAvailable(yCheckBox.Current, true); } else { - setBindableEnabled(scaleHandler.CanScaleX.Value, xCheckBox.Current); - setBindableEnabled(scaleHandler.CanScaleY.Value, yCheckBox.Current); + toggleAxisAvailable(xCheckBox.Current, scaleHandler.CanScaleX.Value); + toggleAxisAvailable(yCheckBox.Current, scaleHandler.CanScaleY.Value); } } - private void setBindableEnabled(bool enabled, Bindable current) + private void toggleAxisAvailable(Bindable axisBindable, bool available) { - current.Disabled = false; // enable the bindable to allow setting the value - current.Value = enabled; - current.Disabled = !enabled; + // enable the bindable to allow setting the value + axisBindable.Disabled = false; + // restore the presumed default value given the axis's new availability state + axisBindable.Value = available; + axisBindable.Disabled = !available; } private void updateMaxScale() From 9477e3b67de6f33a6866d9761b14a2cda20785b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 29 May 2024 10:14:47 +0200 Subject: [PATCH 094/119] Change editor scale hotkey to Ctrl-E Forgot that Ctrl-T was taken by the game-global toolbar already, so it wasn't working. --- osu.Game/Input/Bindings/GlobalActionContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index fd8e6fd6d0..2af564d8ba 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -142,7 +142,7 @@ namespace osu.Game.Input.Bindings new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.MouseWheelRight }, GlobalAction.EditorCyclePreviousBeatSnapDivisor), new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.MouseWheelLeft }, GlobalAction.EditorCycleNextBeatSnapDivisor), new KeyBinding(new[] { InputKey.Control, InputKey.R }, GlobalAction.EditorToggleRotateControl), - new KeyBinding(new[] { InputKey.Control, InputKey.T }, GlobalAction.EditorToggleScaleControl), + new KeyBinding(new[] { InputKey.Control, InputKey.E }, GlobalAction.EditorToggleScaleControl), }; private static IEnumerable inGameKeyBindings => new[] From 84513343d6789cf38bd2d8e355a233e277382f19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 29 May 2024 10:18:22 +0200 Subject: [PATCH 095/119] Remove unused fields --- osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs b/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs index 278d38b2d9..28d0f8320f 100644 --- a/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs +++ b/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs @@ -24,13 +24,6 @@ namespace osu.Game.Rulesets.Osu.Edit private EditorToolButton rotateButton = null!; private EditorToolButton scaleButton = null!; - private Bindable canRotatePlayfieldOrigin = null!; - private Bindable canRotateSelectionOrigin = null!; - - private Bindable canScaleX = null!; - private Bindable canScaleY = null!; - private Bindable canScalePlayfieldOrigin = null!; - public SelectionRotationHandler RotationHandler { get; init; } = null!; public OsuSelectionScaleHandler ScaleHandler { get; init; } = null!; From 22a2adb5e6ca899487d0a09f3edbaade37c6338b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 29 May 2024 10:57:30 +0200 Subject: [PATCH 096/119] Revert unrelated changes --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs | 2 +- osu.Game.Rulesets.Osu/Objects/Slider.cs | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs index 0c7ba180f2..73c061afbd 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public const float DEFAULT_TICK_SIZE = 16; - public DrawableSlider DrawableSlider => (DrawableSlider)ParentHitObject; + protected DrawableSlider DrawableSlider => (DrawableSlider)ParentHitObject; private SkinnableDrawable scaleContainer; diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 248f40208a..cc3ffd376e 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -248,8 +248,6 @@ namespace osu.Game.Rulesets.Osu.Objects if (TailCircle != null) TailCircle.Position = EndPosition; - - // Positions of other nested hitobjects are not updated } protected void UpdateNestedSamples() From a6c776dac891203fa591426dc4c086eefcdf499c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 29 May 2024 11:11:43 +0200 Subject: [PATCH 097/119] Use hopefully safer implementation of anchoring judgements to objects --- .../TestSceneDrawableJudgement.cs | 2 +- .../Objects/Drawables/DrawableOsuJudgement.cs | 14 +++---------- .../Rulesets/Judgements/DrawableJudgement.cs | 20 ++++++++++++------- 3 files changed, 17 insertions(+), 19 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs index 5f5596cbb3..a239f671af 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs @@ -108,7 +108,7 @@ namespace osu.Game.Rulesets.Osu.Tests private partial class TestDrawableOsuJudgement : DrawableOsuJudgement { public new SkinnableSprite Lighting => base.Lighting; - public new SkinnableDrawable JudgementBody => base.JudgementBody; + public new SkinnableDrawable? JudgementBody => base.JudgementBody; } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs index ffbf45291f..98fb99609a 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Configuration; @@ -14,10 +12,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { public partial class DrawableOsuJudgement : DrawableJudgement { - internal SkinnableLighting Lighting { get; private set; } + internal SkinnableLighting Lighting { get; private set; } = null!; [Resolved] - private OsuConfigManager config { get; set; } + private OsuConfigManager config { get; set; } = null!; [BackgroundDependencyLoader] private void load() @@ -38,19 +36,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Lighting.ResetAnimation(); Lighting.SetColourFrom(JudgedObject, Result); - - if (JudgedObject is DrawableOsuHitObject osuObject) - { - Position = osuObject.ToSpaceOfOtherDrawable(osuObject.OriginPosition, Parent!); - Scale = new Vector2(osuObject.HitObject.Scale); - } } protected override void Update() { base.Update(); - if (JudgedObject is DrawableOsuHitObject osuObject && Parent != null && osuObject.HitObject != null) + if (JudgedObject is DrawableOsuHitObject osuObject && JudgedObject.IsInUse) { Position = osuObject.ToSpaceOfOtherDrawable(osuObject.OriginPosition, Parent!); Scale = new Vector2(osuObject.HitObject.Scale); diff --git a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs index b4686c52f3..37a9766b71 100644 --- a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs +++ b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs @@ -1,11 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Diagnostics; -using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -24,13 +21,13 @@ namespace osu.Game.Rulesets.Judgements { private const float judgement_size = 128; - public JudgementResult Result { get; private set; } + public JudgementResult? Result { get; private set; } - public DrawableHitObject JudgedObject { get; private set; } + public DrawableHitObject? JudgedObject { get; private set; } public override bool RemoveCompletedTransforms => false; - protected SkinnableDrawable JudgementBody { get; private set; } + protected SkinnableDrawable? JudgementBody { get; private set; } private readonly Container aboveHitObjectsContent; @@ -97,12 +94,19 @@ namespace osu.Game.Rulesets.Judgements /// /// The applicable judgement. /// The drawable object. - public void Apply([NotNull] JudgementResult result, [CanBeNull] DrawableHitObject judgedObject) + public void Apply(JudgementResult result, DrawableHitObject? judgedObject) { Result = result; JudgedObject = judgedObject; } + protected override void FreeAfterUse() + { + base.FreeAfterUse(); + + JudgedObject = null; + } + protected override void PrepareForUse() { base.PrepareForUse(); @@ -121,6 +125,8 @@ namespace osu.Game.Rulesets.Judgements ApplyTransformsAt(double.MinValue, true); ClearTransforms(true); + Debug.Assert(Result != null && JudgementBody != null); + LifetimeStart = Result.TimeAbsolute; using (BeginAbsoluteSequence(Result.TimeAbsolute)) From cc136556175b4c2e854aca51d3aebe35f0e5069f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 29 May 2024 13:31:15 +0200 Subject: [PATCH 098/119] Derive API response version from game version (Or local date, in the case of non-deployed builds). Came up when I was looking at https://github.com/ppy/osu-web/pull/11240 and found that we were still hardcoding this. Thankfully, this *should not* cause issues, since there don't seem to be any (documented or undocumented) API response version checks for versions newer than 20220705 in osu-web master. For clarity and possible debugging needs, the API response version is also logged. --- osu.Game/Online/API/APIAccess.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index d3707fe74d..6f84e98d2c 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -42,7 +42,7 @@ namespace osu.Game.Online.API public string WebsiteRootUrl { get; } - public int APIVersion => 20220705; // We may want to pull this from the game version eventually. + public int APIVersion { get; } public Exception LastLoginError { get; private set; } @@ -84,12 +84,23 @@ namespace osu.Game.Online.API this.config = config; this.versionHash = versionHash; + if (game.IsDeployedBuild) + APIVersion = game.AssemblyVersion.Major * 10000 + game.AssemblyVersion.Minor; + else + { + var now = DateTimeOffset.Now; + APIVersion = now.Year * 10000 + now.Month * 100 + now.Day; + } + APIEndpointUrl = endpointConfiguration.APIEndpointUrl; WebsiteRootUrl = endpointConfiguration.WebsiteRootUrl; NotificationsClient = setUpNotificationsClient(); authentication = new OAuth(endpointConfiguration.APIClientID, endpointConfiguration.APIClientSecret, APIEndpointUrl); + log = Logger.GetLogger(LoggingTarget.Network); + log.Add($@"API endpoint root: {APIEndpointUrl}"); + log.Add($@"API request version: {APIVersion}"); ProvidedUsername = config.Get(OsuSetting.Username); From ab01fa6d4572af4239527c0090ae438f549f55c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 29 May 2024 13:34:12 +0200 Subject: [PATCH 099/119] Add xmldoc to `APIAccess.APIVersion` --- osu.Game/Online/API/APIAccess.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index 6f84e98d2c..923f841bd8 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -42,6 +42,10 @@ namespace osu.Game.Online.API public string WebsiteRootUrl { get; } + /// + /// The API response version. + /// See: https://osu.ppy.sh/docs/index.html#api-versions + /// public int APIVersion { get; } public Exception LastLoginError { get; private set; } From 8916f08f8620d38b308f211006e6c75535138342 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 30 May 2024 09:03:02 +0200 Subject: [PATCH 100/119] Only take initial judgement position from object instead of following Looks less bad with mods like depth active. Co-authored-by: Dean Herbert --- .../Objects/Drawables/DrawableOsuJudgement.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs index 98fb99609a..6e252a53e2 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs @@ -17,6 +17,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables [Resolved] private OsuConfigManager config { get; set; } = null!; + private bool positionTransferred; + [BackgroundDependencyLoader] private void load() { @@ -36,16 +38,20 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Lighting.ResetAnimation(); Lighting.SetColourFrom(JudgedObject, Result); + + positionTransferred = false; } protected override void Update() { base.Update(); - if (JudgedObject is DrawableOsuHitObject osuObject && JudgedObject.IsInUse) + if (!positionTransferred && JudgedObject is DrawableOsuHitObject osuObject && JudgedObject.IsInUse) { Position = osuObject.ToSpaceOfOtherDrawable(osuObject.OriginPosition, Parent!); Scale = new Vector2(osuObject.HitObject.Scale); + + positionTransferred = true; } } From 2f2bc8e52eaa6bf2213357f8ca624e902cfcf2f4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 30 May 2024 17:37:55 +0900 Subject: [PATCH 101/119] Avoid `ChatAckRequest` failures flooding console in `OsuGameTestScene`s --- osu.Game.Tests/Chat/TestSceneChannelManager.cs | 2 +- osu.Game/Online/API/DummyAPIAccess.cs | 8 ++++++++ osu.Game/Online/API/Requests/Responses/ChatAckResponse.cs | 4 ++-- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Chat/TestSceneChannelManager.cs b/osu.Game.Tests/Chat/TestSceneChannelManager.cs index 95fd2669e5..ef4d4f683a 100644 --- a/osu.Game.Tests/Chat/TestSceneChannelManager.cs +++ b/osu.Game.Tests/Chat/TestSceneChannelManager.cs @@ -59,7 +59,7 @@ namespace osu.Game.Tests.Chat return true; case ChatAckRequest ack: - ack.TriggerSuccess(new ChatAckResponse { Silences = silencedUserIds.Select(u => new ChatSilence { UserId = u }).ToList() }); + ack.TriggerSuccess(new ChatAckResponse { Silences = silencedUserIds.Select(u => new ChatSilence { UserId = u }).ToArray() }); silencedUserIds.Clear(); return true; diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs index 4962838bd9..8d1e986577 100644 --- a/osu.Game/Online/API/DummyAPIAccess.cs +++ b/osu.Game/Online/API/DummyAPIAccess.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using osu.Framework.Bindables; @@ -84,6 +85,13 @@ namespace osu.Game.Online.API { if (HandleRequest?.Invoke(request) != true) { + // Noisy so let's silently allow these to succeed. + if (request is ChatAckRequest ack) + { + ack.TriggerSuccess(new ChatAckResponse()); + return; + } + request.Fail(new InvalidOperationException($@"{nameof(DummyAPIAccess)} cannot process this request.")); } }); diff --git a/osu.Game/Online/API/Requests/Responses/ChatAckResponse.cs b/osu.Game/Online/API/Requests/Responses/ChatAckResponse.cs index 6ed22a19b2..f68735d390 100644 --- a/osu.Game/Online/API/Requests/Responses/ChatAckResponse.cs +++ b/osu.Game/Online/API/Requests/Responses/ChatAckResponse.cs @@ -1,7 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Generic; +using System; using Newtonsoft.Json; namespace osu.Game.Online.API.Requests.Responses @@ -10,6 +10,6 @@ namespace osu.Game.Online.API.Requests.Responses public class ChatAckResponse { [JsonProperty("silences")] - public List Silences { get; set; } = null!; + public ChatSilence[] Silences { get; set; } = Array.Empty(); } } From 36d7775032c08f0983ebd2ea28960bcfb0db4968 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 30 May 2024 17:38:05 +0900 Subject: [PATCH 102/119] Fix typo in `IAPIProvider` xmldoc --- osu.Game/Online/API/IAPIProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/API/IAPIProvider.cs b/osu.Game/Online/API/IAPIProvider.cs index 66f124f7c3..7b95b68ec3 100644 --- a/osu.Game/Online/API/IAPIProvider.cs +++ b/osu.Game/Online/API/IAPIProvider.cs @@ -61,7 +61,7 @@ namespace osu.Game.Online.API string APIEndpointUrl { get; } /// - /// The root URL of of the website, excluding the trailing slash. + /// The root URL of the website, excluding the trailing slash. /// string WebsiteRootUrl { get; } From 50bd0897f653f64fa394abc490b691ac84170547 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 30 May 2024 10:38:22 +0200 Subject: [PATCH 103/119] Fix main menu button backgrounds not covering their entire width sometimes I thought I had fixed this already once but it still looks broken. Basically when hovering over main menu buttons every now and then it will look like their backgrounds are not covering their entire width when they expand. The removed X position set looks wrong to me when inspecting the draw visualiser with the element because the element looks to be off centre horizontally, and removing it fixes that. --- osu.Game/Screens/Menu/MainMenuButton.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/Menu/MainMenuButton.cs b/osu.Game/Screens/Menu/MainMenuButton.cs index 29a661066c..4df5e6d309 100644 --- a/osu.Game/Screens/Menu/MainMenuButton.cs +++ b/osu.Game/Screens/Menu/MainMenuButton.cs @@ -115,7 +115,6 @@ namespace osu.Game.Screens.Menu backgroundContent = CreateBackground(colour).With(bg => { bg.RelativeSizeAxes = Axes.Y; - bg.X = -ButtonSystem.WEDGE_WIDTH; bg.Anchor = Anchor.Centre; bg.Origin = Anchor.Centre; }), From f3bc944ac86c7f5bd37c04cc35c0c2fb71cebfba Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 30 May 2024 17:45:32 +0900 Subject: [PATCH 104/119] Remove using statement --- osu.Game/Online/API/DummyAPIAccess.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs index 8d1e986577..960941fc05 100644 --- a/osu.Game/Online/API/DummyAPIAccess.cs +++ b/osu.Game/Online/API/DummyAPIAccess.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using osu.Framework.Bindables; From f6a59bee9addebb75dc34b4d1253da84967b35bb Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 30 May 2024 19:17:18 +0900 Subject: [PATCH 105/119] Use delayed resume overlay for autopilot --- osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs | 2 -- osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs | 9 ++++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs index efcc728d55..b45b4fea13 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs @@ -67,8 +67,6 @@ namespace osu.Game.Rulesets.Osu.Mods // Generate the replay frames the cursor should follow replayFrames = new OsuAutoGenerator(drawableRuleset.Beatmap, drawableRuleset.Mods).Generate().Frames.Cast().ToList(); - - drawableRuleset.UseResumeOverlay = false; } } } diff --git a/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs b/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs index c3efd48053..f0390ad716 100644 --- a/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs @@ -13,6 +13,7 @@ using osu.Game.Replays; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Configuration; +using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Replays; using osu.Game.Rulesets.UI; @@ -45,7 +46,13 @@ namespace osu.Game.Rulesets.Osu.UI public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new OsuPlayfieldAdjustmentContainer { AlignWithStoryboard = true }; - protected override ResumeOverlay CreateResumeOverlay() => new OsuResumeOverlay(); + protected override ResumeOverlay CreateResumeOverlay() + { + if (Mods.Any(m => m is OsuModAutopilot)) + return new DelayedResumeOverlay { Scale = new Vector2(0.65f) }; + + return new OsuResumeOverlay(); + } protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new OsuFramedReplayInputHandler(replay); From 87a331fdde783dbfc3b5f395339d2d09cba1105e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 30 May 2024 16:34:52 +0900 Subject: [PATCH 106/119] Improve text on external link warning dialog --- osu.Game/Online/Chat/ExternalLinkOpener.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/osu.Game/Online/Chat/ExternalLinkOpener.cs b/osu.Game/Online/Chat/ExternalLinkOpener.cs index 56d24e35bb..08adf965da 100644 --- a/osu.Game/Online/Chat/ExternalLinkOpener.cs +++ b/osu.Game/Online/Chat/ExternalLinkOpener.cs @@ -10,6 +10,7 @@ using osu.Framework.Platform; using osu.Game.Configuration; using osu.Game.Overlays; using osu.Game.Overlays.Dialog; +using osu.Game.Resources.Localisation.Web; namespace osu.Game.Online.Chat { @@ -44,8 +45,8 @@ namespace osu.Game.Online.Chat { public ExternalLinkDialog(string url, Action openExternalLinkAction, Action copyExternalLinkAction) { - HeaderText = "Just checking..."; - BodyText = $"You are about to leave osu! and open the following link in a web browser:\n\n{url}"; + HeaderText = "You are leaving osu!"; + BodyText = $"Are you sure you want to open the following link in a web browser:\n\n{url}"; Icon = FontAwesome.Solid.ExclamationTriangle; @@ -53,17 +54,17 @@ namespace osu.Game.Online.Chat { new PopupDialogOkButton { - Text = @"Yes. Go for it.", + Text = @"Open in browser", Action = openExternalLinkAction }, new PopupDialogCancelButton { - Text = @"Copy URL to the clipboard instead.", + Text = @"Copy URL to the clipboard", Action = copyExternalLinkAction }, new PopupDialogCancelButton { - Text = @"No! Abort mission!" + Text = CommonStrings.ButtonsCancel, }, }; } From ed64bfff8d9c13c54b27b955a706e28d3cefb134 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 30 May 2024 16:39:53 +0900 Subject: [PATCH 107/119] Bypass external link dialog for links on the trusted osu! domain --- osu.Game/OsuGame.cs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index af01a1b1ac..29c040c597 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -485,10 +485,19 @@ namespace osu.Game } }); - public void OpenUrlExternally(string url, bool bypassExternalUrlWarning = false) => waitForReady(() => externalLinkOpener, _ => + public void OpenUrlExternally(string url, bool forceBypassExternalUrlWarning = false) => waitForReady(() => externalLinkOpener, _ => { + bool isTrustedDomain; + if (url.StartsWith('/')) - url = $"{API.APIEndpointUrl}{url}"; + { + url = $"{API.WebsiteRootUrl}{url}"; + isTrustedDomain = true; + } + else + { + isTrustedDomain = url.StartsWith(API.APIEndpointUrl, StringComparison.Ordinal); + } if (!url.CheckIsValidUrl()) { @@ -500,7 +509,7 @@ namespace osu.Game return; } - externalLinkOpener.OpenUrlExternally(url, bypassExternalUrlWarning); + externalLinkOpener.OpenUrlExternally(url, forceBypassExternalUrlWarning || isTrustedDomain); }); /// From 53b7c29488f6dd28ae30877e0df148d4c6be6d33 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 30 May 2024 17:35:42 +0900 Subject: [PATCH 108/119] Add test coverage of menu banner link opening --- .../Visual/Menus/TestSceneMainMenu.cs | 49 ++++++++++++++++++- osu.Game/Screens/Menu/OnlineMenuBanner.cs | 5 ++ 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs b/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs index e2a841d79a..240421b360 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs @@ -6,6 +6,7 @@ using NUnit.Framework; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Overlays; using osu.Game.Screens.Menu; using osuTK.Input; @@ -15,8 +16,14 @@ namespace osu.Game.Tests.Visual.Menus { private OnlineMenuBanner onlineMenuBanner => Game.ChildrenOfType().Single(); + public override void SetUpSteps() + { + base.SetUpSteps(); + AddStep("don't fetch online content", () => onlineMenuBanner.FetchOnlineContent = false); + } + [Test] - public void TestOnlineMenuBanner() + public void TestOnlineMenuBannerTrusted() { AddStep("set online content", () => onlineMenuBanner.Current.Value = new APIMenuContent { @@ -25,13 +32,51 @@ namespace osu.Game.Tests.Visual.Menus new APIMenuImage { Image = @"https://assets.ppy.sh/main-menu/project-loved-2@2x.png", - Url = @"https://osu.ppy.sh/home/news/2023-12-21-project-loved-december-2023", + Url = $@"{API.WebsiteRootUrl}/home/news/2023-12-21-project-loved-december-2023", } } }); AddAssert("system title not visible", () => onlineMenuBanner.State.Value, () => Is.EqualTo(Visibility.Hidden)); AddStep("enter menu", () => InputManager.Key(Key.Enter)); AddUntilStep("system title visible", () => onlineMenuBanner.State.Value, () => Is.EqualTo(Visibility.Visible)); + AddUntilStep("image loaded", () => onlineMenuBanner.ChildrenOfType().FirstOrDefault()?.IsLoaded, () => Is.True); + + AddStep("click banner", () => + { + InputManager.MoveMouseTo(onlineMenuBanner); + InputManager.Click(MouseButton.Left); + }); + + // Might not catch every occurrence due to async nature, but works in manual testing and saves annoying test setup. + AddAssert("no dialog", () => Game.ChildrenOfType().SingleOrDefault()?.CurrentDialog == null); + } + + [Test] + public void TestOnlineMenuBannerUntrustedDomain() + { + AddStep("set online content", () => onlineMenuBanner.Current.Value = new APIMenuContent + { + Images = new[] + { + new APIMenuImage + { + Image = @"https://assets.ppy.sh/main-menu/project-loved-2@2x.png", + Url = @"https://google.com", + } + } + }); + AddAssert("system title not visible", () => onlineMenuBanner.State.Value, () => Is.EqualTo(Visibility.Hidden)); + AddStep("enter menu", () => InputManager.Key(Key.Enter)); + AddUntilStep("system title visible", () => onlineMenuBanner.State.Value, () => Is.EqualTo(Visibility.Visible)); + AddUntilStep("image loaded", () => onlineMenuBanner.ChildrenOfType().FirstOrDefault()?.IsLoaded, () => Is.True); + + AddStep("click banner", () => + { + InputManager.MoveMouseTo(onlineMenuBanner); + InputManager.Click(MouseButton.Left); + }); + + AddUntilStep("wait for dialog", () => Game.ChildrenOfType().SingleOrDefault()?.CurrentDialog != null); } } } diff --git a/osu.Game/Screens/Menu/OnlineMenuBanner.cs b/osu.Game/Screens/Menu/OnlineMenuBanner.cs index b9d269c82a..edd34d0bfb 100644 --- a/osu.Game/Screens/Menu/OnlineMenuBanner.cs +++ b/osu.Game/Screens/Menu/OnlineMenuBanner.cs @@ -73,6 +73,9 @@ namespace osu.Game.Screens.Menu Task.Run(() => request.Perform()) .ContinueWith(r => { + if (!FetchOnlineContent) + return; + if (r.IsCompletedSuccessfully) Schedule(() => Current.Value = request.ResponseObject); @@ -170,6 +173,8 @@ namespace osu.Game.Screens.Menu private Sprite flash = null!; + public override bool IsPresent => base.IsPresent || Scheduler.HasPendingTasks; + private ScheduledDelegate? openUrlAction; public MenuImage(APIMenuImage image) From 474ff5b99d089cd5db38d26541598735cb65b9cb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 31 May 2024 10:46:30 +0900 Subject: [PATCH 109/119] Use question mark for more grammatical correctness Co-authored-by: Joseph Madamba --- osu.Game/Online/Chat/ExternalLinkOpener.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/Chat/ExternalLinkOpener.cs b/osu.Game/Online/Chat/ExternalLinkOpener.cs index 08adf965da..513ccd43af 100644 --- a/osu.Game/Online/Chat/ExternalLinkOpener.cs +++ b/osu.Game/Online/Chat/ExternalLinkOpener.cs @@ -46,7 +46,7 @@ namespace osu.Game.Online.Chat public ExternalLinkDialog(string url, Action openExternalLinkAction, Action copyExternalLinkAction) { HeaderText = "You are leaving osu!"; - BodyText = $"Are you sure you want to open the following link in a web browser:\n\n{url}"; + BodyText = $"Are you sure you want to open the following link in a web browser?\n\n{url}"; Icon = FontAwesome.Solid.ExclamationTriangle; From e52f524ea2ce15b6ef891900a3c7cee236f6d7cb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 31 May 2024 11:44:53 +0900 Subject: [PATCH 110/119] Use common header text --- osu.Game/Online/Chat/ExternalLinkOpener.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/Chat/ExternalLinkOpener.cs b/osu.Game/Online/Chat/ExternalLinkOpener.cs index 513ccd43af..2f046d8fd4 100644 --- a/osu.Game/Online/Chat/ExternalLinkOpener.cs +++ b/osu.Game/Online/Chat/ExternalLinkOpener.cs @@ -8,9 +8,10 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Platform; using osu.Game.Configuration; +using osu.Game.Localisation; using osu.Game.Overlays; using osu.Game.Overlays.Dialog; -using osu.Game.Resources.Localisation.Web; +using CommonStrings = osu.Game.Resources.Localisation.Web.CommonStrings; namespace osu.Game.Online.Chat { @@ -45,7 +46,7 @@ namespace osu.Game.Online.Chat { public ExternalLinkDialog(string url, Action openExternalLinkAction, Action copyExternalLinkAction) { - HeaderText = "You are leaving osu!"; + HeaderText = DeleteConfirmationDialogStrings.HeaderText; BodyText = $"Are you sure you want to open the following link in a web browser?\n\n{url}"; Icon = FontAwesome.Solid.ExclamationTriangle; From 5dfeaa3c4afa75dbfebffbd1c7a61af0dd93b6d4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 31 May 2024 11:46:32 +0900 Subject: [PATCH 111/119] Move dialog strings to more common class name --- ...{DeleteConfirmationDialogStrings.cs => DialogStrings.cs} | 6 +++--- osu.Game/Online/Chat/ExternalLinkOpener.cs | 2 +- osu.Game/Overlays/Dialog/DangerousActionDialog.cs | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) rename osu.Game/Localisation/{DeleteConfirmationDialogStrings.cs => DialogStrings.cs} (80%) diff --git a/osu.Game/Localisation/DeleteConfirmationDialogStrings.cs b/osu.Game/Localisation/DialogStrings.cs similarity index 80% rename from osu.Game/Localisation/DeleteConfirmationDialogStrings.cs rename to osu.Game/Localisation/DialogStrings.cs index 25997eadd3..cea543eb9f 100644 --- a/osu.Game/Localisation/DeleteConfirmationDialogStrings.cs +++ b/osu.Game/Localisation/DialogStrings.cs @@ -5,14 +5,14 @@ using osu.Framework.Localisation; namespace osu.Game.Localisation { - public static class DeleteConfirmationDialogStrings + public static class DialogStrings { - private const string prefix = @"osu.Game.Resources.Localisation.DeleteConfirmationDialog"; + private const string prefix = @"osu.Game.Resources.Localisation.DialogStrings"; /// /// "Caution" /// - public static LocalisableString HeaderText => new TranslatableString(getKey(@"header_text"), @"Caution"); + public static LocalisableString Caution => new TranslatableString(getKey(@"header_text"), @"Caution"); /// /// "Yes. Go for it." diff --git a/osu.Game/Online/Chat/ExternalLinkOpener.cs b/osu.Game/Online/Chat/ExternalLinkOpener.cs index 2f046d8fd4..82ad4215c2 100644 --- a/osu.Game/Online/Chat/ExternalLinkOpener.cs +++ b/osu.Game/Online/Chat/ExternalLinkOpener.cs @@ -46,7 +46,7 @@ namespace osu.Game.Online.Chat { public ExternalLinkDialog(string url, Action openExternalLinkAction, Action copyExternalLinkAction) { - HeaderText = DeleteConfirmationDialogStrings.HeaderText; + HeaderText = DialogStrings.Caution; BodyText = $"Are you sure you want to open the following link in a web browser?\n\n{url}"; Icon = FontAwesome.Solid.ExclamationTriangle; diff --git a/osu.Game/Overlays/Dialog/DangerousActionDialog.cs b/osu.Game/Overlays/Dialog/DangerousActionDialog.cs index 42a3ff827c..60f51e7e06 100644 --- a/osu.Game/Overlays/Dialog/DangerousActionDialog.cs +++ b/osu.Game/Overlays/Dialog/DangerousActionDialog.cs @@ -30,7 +30,7 @@ namespace osu.Game.Overlays.Dialog protected DangerousActionDialog() { - HeaderText = DeleteConfirmationDialogStrings.HeaderText; + HeaderText = DialogStrings.DialogCautionHeader; Icon = FontAwesome.Regular.TrashAlt; @@ -38,12 +38,12 @@ namespace osu.Game.Overlays.Dialog { new PopupDialogDangerousButton { - Text = DeleteConfirmationDialogStrings.Confirm, + Text = DialogStrings.DialogConfirm, Action = () => DangerousAction?.Invoke() }, new PopupDialogCancelButton { - Text = DeleteConfirmationDialogStrings.Cancel, + Text = DialogStrings.Cancel, Action = () => CancelAction?.Invoke() } }; From cb72630ce1894a3ed73f607112095c4845413349 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 31 May 2024 08:09:06 +0200 Subject: [PATCH 112/119] Fix compile failures --- osu.Game/Overlays/Dialog/DangerousActionDialog.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Dialog/DangerousActionDialog.cs b/osu.Game/Overlays/Dialog/DangerousActionDialog.cs index 60f51e7e06..31160d1832 100644 --- a/osu.Game/Overlays/Dialog/DangerousActionDialog.cs +++ b/osu.Game/Overlays/Dialog/DangerousActionDialog.cs @@ -30,7 +30,7 @@ namespace osu.Game.Overlays.Dialog protected DangerousActionDialog() { - HeaderText = DialogStrings.DialogCautionHeader; + HeaderText = DialogStrings.Caution; Icon = FontAwesome.Regular.TrashAlt; @@ -38,7 +38,7 @@ namespace osu.Game.Overlays.Dialog { new PopupDialogDangerousButton { - Text = DialogStrings.DialogConfirm, + Text = DialogStrings.Confirm, Action = () => DangerousAction?.Invoke() }, new PopupDialogCancelButton From 1a26a5dbdaf77256564930e69e5f20d3e9a2cd17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 31 May 2024 08:09:25 +0200 Subject: [PATCH 113/119] Fix mismatching localisation key prefix The `Strings` suffix is not supposed to be in here, judging by other localisation classes. --- osu.Game/Localisation/DialogStrings.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Localisation/DialogStrings.cs b/osu.Game/Localisation/DialogStrings.cs index cea543eb9f..043a3f5b4c 100644 --- a/osu.Game/Localisation/DialogStrings.cs +++ b/osu.Game/Localisation/DialogStrings.cs @@ -7,7 +7,7 @@ namespace osu.Game.Localisation { public static class DialogStrings { - private const string prefix = @"osu.Game.Resources.Localisation.DialogStrings"; + private const string prefix = @"osu.Game.Resources.Localisation.Dialog"; /// /// "Caution" From 9111da81d22c2a9e35533df964cb9e5c0e691807 Mon Sep 17 00:00:00 2001 From: Aurelian Date: Fri, 31 May 2024 01:30:26 +0200 Subject: [PATCH 114/119] Updated comments --- osu.Game/Rulesets/Objects/SliderPath.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/osu.Game/Rulesets/Objects/SliderPath.cs b/osu.Game/Rulesets/Objects/SliderPath.cs index 730a2013b0..5550815370 100644 --- a/osu.Game/Rulesets/Objects/SliderPath.cs +++ b/osu.Game/Rulesets/Objects/SliderPath.cs @@ -332,16 +332,15 @@ namespace osu.Game.Rulesets.Objects CircularArcProperties circularArcProperties = new CircularArcProperties(subControlPoints); - //if false, we'll end up breaking anyways when calculating subPath + // `PathApproximator` will already internally revert to B-spline if the arc isn't valid. if (!circularArcProperties.IsValid) break; - //Coppied from PathApproximator.CircularArcToPiecewiseLinear + // taken from https://github.com/ppy/osu-framework/blob/1201e641699a1d50d2f6f9295192dad6263d5820/osu.Framework/Utils/PathApproximator.cs#L181-L186 int subPoints = (2f * circularArcProperties.Radius <= 0.1f) ? 2 : Math.Max(2, (int)Math.Ceiling(circularArcProperties.ThetaRange / (2.0 * Math.Acos(1f - (0.1f / circularArcProperties.Radius))))); - //theoretically can be int.MaxValue, but lets set this to a lower value anyways - //1000 requires an arc length of over 20 thousand to surpass this limit, which should be safe. - //See here for calculations https://www.desmos.com/calculator/210bwswkbb + // 1000 subpoints requires an arc length of at least ~120 thousand to occur + // See here for calculations https://www.desmos.com/calculator/umj6jvmcz7 if (subPoints >= 1000) break; From c6a7082034da26c6399633ad18665a85ccc58f30 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 31 May 2024 15:38:26 +0900 Subject: [PATCH 115/119] Fix incorrect prefix check --- osu.Game/OsuGame.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 29c040c597..0833f52b1e 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -496,7 +496,7 @@ namespace osu.Game } else { - isTrustedDomain = url.StartsWith(API.APIEndpointUrl, StringComparison.Ordinal); + isTrustedDomain = url.StartsWith(API.WebsiteRootUrl, StringComparison.Ordinal); } if (!url.CheckIsValidUrl()) From 69990c35cb638d57e735cedd6785fe2a06bd4341 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 31 May 2024 08:47:19 +0200 Subject: [PATCH 116/119] Add commentary on presence of `IsPresent` override --- osu.Game/Screens/Menu/OnlineMenuBanner.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/Menu/OnlineMenuBanner.cs b/osu.Game/Screens/Menu/OnlineMenuBanner.cs index edd34d0bfb..49fc89c171 100644 --- a/osu.Game/Screens/Menu/OnlineMenuBanner.cs +++ b/osu.Game/Screens/Menu/OnlineMenuBanner.cs @@ -173,6 +173,9 @@ namespace osu.Game.Screens.Menu private Sprite flash = null!; + /// + /// Overridden as a safety for functioning correctly. + /// public override bool IsPresent => base.IsPresent || Scheduler.HasPendingTasks; private ScheduledDelegate? openUrlAction; From e3205fce4769afd2ac3cce60f6e1321f530ec029 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sat, 1 Jun 2024 14:29:23 +0900 Subject: [PATCH 117/119] Fix unable to drag-scroll on collections right-click menu --- osu.Game/Graphics/UserInterface/DrawableStatefulMenuItem.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Graphics/UserInterface/DrawableStatefulMenuItem.cs b/osu.Game/Graphics/UserInterface/DrawableStatefulMenuItem.cs index 686c490930..b9e81e1bf2 100644 --- a/osu.Game/Graphics/UserInterface/DrawableStatefulMenuItem.cs +++ b/osu.Game/Graphics/UserInterface/DrawableStatefulMenuItem.cs @@ -26,9 +26,12 @@ namespace osu.Game.Graphics.UserInterface // Right mouse button is a special case where we allow actioning without dismissing the menu. // This is achieved by not calling `Clicked` (as done by the base implementation in OnClick). if (IsActionable && e.Button == MouseButton.Right) + { Item.Action.Value?.Invoke(); + return true; + } - return true; + return false; } private partial class ToggleTextContainer : TextContainer From 091104764e677d4efecbe8958ca71b7f9cac03b8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 2 Jun 2024 17:33:06 +0900 Subject: [PATCH 118/119] Fix `AssemblyRulesetStore` not marking rulesets as available --- osu.Game/Rulesets/AssemblyRulesetStore.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/AssemblyRulesetStore.cs b/osu.Game/Rulesets/AssemblyRulesetStore.cs index 03554ef2db..935ef241dc 100644 --- a/osu.Game/Rulesets/AssemblyRulesetStore.cs +++ b/osu.Game/Rulesets/AssemblyRulesetStore.cs @@ -43,7 +43,12 @@ namespace osu.Game.Rulesets // add all legacy rulesets first to ensure they have exclusive choice of primary key. foreach (var r in instances.Where(r => r is ILegacyRuleset)) - availableRulesets.Add(new RulesetInfo(r.RulesetInfo.ShortName, r.RulesetInfo.Name, r.RulesetInfo.InstantiationInfo, r.RulesetInfo.OnlineID)); + { + availableRulesets.Add(new RulesetInfo(r.RulesetInfo.ShortName, r.RulesetInfo.Name, r.RulesetInfo.InstantiationInfo, r.RulesetInfo.OnlineID) + { + Available = true + }); + } } } } From 96514132c1ecc5e3efabf9427741148b7e283fd1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 3 Jun 2024 12:24:50 +0900 Subject: [PATCH 119/119] Fix occasional test failures on new menu content tests Scheduled data transfer could still overwrite test data. --- osu.Game/Screens/Menu/OnlineMenuBanner.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Menu/OnlineMenuBanner.cs b/osu.Game/Screens/Menu/OnlineMenuBanner.cs index 49fc89c171..aa73ce2136 100644 --- a/osu.Game/Screens/Menu/OnlineMenuBanner.cs +++ b/osu.Game/Screens/Menu/OnlineMenuBanner.cs @@ -77,7 +77,15 @@ namespace osu.Game.Screens.Menu return; if (r.IsCompletedSuccessfully) - Schedule(() => Current.Value = request.ResponseObject); + { + Schedule(() => + { + if (!FetchOnlineContent) + return; + + Current.Value = request.ResponseObject; + }); + } // if the request failed, "observe" the exception. // it isn't very important why this failed, as it's only for display.