From e85869e0b48e7d683933ed821f6ec4baa5ea57a3 Mon Sep 17 00:00:00 2001 From: Aurelian Date: Sun, 2 Jun 2024 16:38:58 +0200 Subject: [PATCH 001/120] Move already placed objects when adjusting offset/BPM --- .../Screens/Edit/Timing/TapTimingControl.cs | 56 ++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Timing/TapTimingControl.cs b/osu.Game/Screens/Edit/Timing/TapTimingControl.cs index 8cdbd97ecb7c..cc0d1956267b 100644 --- a/osu.Game/Screens/Edit/Timing/TapTimingControl.cs +++ b/osu.Game/Screens/Edit/Timing/TapTimingControl.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -9,11 +11,13 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; +using osu.Framework.Utils; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Overlays; +using osu.Game.Rulesets.Objects; using osuTK; namespace osu.Game.Screens.Edit.Timing @@ -33,6 +37,8 @@ public partial class TapTimingControl : CompositeDrawable private MetronomeDisplay metronome = null!; + private LabelledSwitchButton adjustPlacedNotes = null!; + [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider, OsuColour colours) { @@ -59,6 +65,7 @@ private void load(OverlayColourProvider colourProvider, OsuColour colours) { new Dimension(GridSizeMode.Absolute, 200), new Dimension(GridSizeMode.Absolute, 50), + new Dimension(GridSizeMode.Absolute, 50), new Dimension(GridSizeMode.Absolute, TapButton.SIZE + padding), }, Content = new[] @@ -116,6 +123,18 @@ private void load(OverlayColourProvider colourProvider, OsuColour colours) }, }, new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Bottom = padding, Horizontal = padding }, + Children = new Drawable[] + { + adjustPlacedNotes = new LabelledSwitchButton { Label = "Move already placed notes\nwhen changing the offset/BPM" }, + } + }, + }, + new Drawable[] { new Container { @@ -192,6 +211,17 @@ private void reset() editorClock.Seek(selectedGroup.Value.Time); } + private List hitObjectsInTimingRange(EditorBeatmap beatmap, ControlPointGroup selectedGroup) + { + // If the first group, we grab all hitobjects prior to the next, if the last group, we grab all remaining hitobjects + double firstGroupTime = beatmap.ControlPointInfo.Groups.Any(x => x.ControlPoints.Any(y => y is TimingControlPoint) && x.Time < selectedGroup.Time) ? selectedGroup.Time : double.MinValue; + double nextGroupTime = beatmap.ControlPointInfo.Groups.FirstOrDefault(x => x.ControlPoints.Any(y => y is TimingControlPoint) && x.Time > selectedGroup.Time)?.Time ?? double.MaxValue; + + var result = beatmap.HitObjects.Where(x => Precision.AlmostBigger(x.StartTime, firstGroupTime) && Precision.DefinitelyBigger(nextGroupTime, x.StartTime)).ToList(); + Console.WriteLine(firstGroupTime + ", " + nextGroupTime + ", " + result.Count); + return result; + } + private void adjustOffset(double adjust) { if (selectedGroup.Value == null) @@ -199,6 +229,8 @@ private void adjustOffset(double adjust) bool wasAtStart = editorClock.CurrentTimeAccurate == selectedGroup.Value.Time; + List hitObjectsInRange = hitObjectsInTimingRange(beatmap, selectedGroup.Value); + // VERY TEMPORARY var currentGroupItems = selectedGroup.Value.ControlPoints.ToArray(); @@ -212,6 +244,14 @@ private void adjustOffset(double adjust) // the control point might not necessarily exist yet, if currentGroupItems was empty. selectedGroup.Value = beatmap.ControlPointInfo.GroupAt(newOffset, true); + if (adjustPlacedNotes.Current.Value) + { + foreach (HitObject hitObject in hitObjectsInRange) + { + hitObject.StartTime += adjust; + } + } + if (!editorClock.IsRunning && wasAtStart) editorClock.Seek(newOffset); } @@ -223,7 +263,21 @@ private void adjustBpm(double adjust) if (timing == null) return; - timing.BeatLength = 60000 / (timing.BPM + adjust); + double newBeatLength = 60000 / (timing.BPM + adjust); + + List hitObjectsInRange = hitObjectsInTimingRange(beatmap, selectedGroup.Value!); + + if (adjustPlacedNotes.Current.Value) + { + foreach (HitObject hitObject in hitObjectsInRange) + { + double beat = (hitObject.StartTime - selectedGroup.Value!.Time) / timing.BeatLength; + + hitObject.StartTime = beat * newBeatLength + selectedGroup.Value.Time; + } + } + + timing.BeatLength = newBeatLength; } private partial class InlineButton : OsuButton From d02c291168af670e66612cda33d35842ec5f05cf Mon Sep 17 00:00:00 2001 From: Aurelian Date: Sun, 2 Jun 2024 16:42:06 +0200 Subject: [PATCH 002/120] Removed debugging information --- osu.Game/Screens/Edit/Timing/TapTimingControl.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/osu.Game/Screens/Edit/Timing/TapTimingControl.cs b/osu.Game/Screens/Edit/Timing/TapTimingControl.cs index cc0d1956267b..c4d67eaf1a97 100644 --- a/osu.Game/Screens/Edit/Timing/TapTimingControl.cs +++ b/osu.Game/Screens/Edit/Timing/TapTimingControl.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; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; @@ -217,9 +216,7 @@ private List hitObjectsInTimingRange(EditorBeatmap beatmap, ControlPo double firstGroupTime = beatmap.ControlPointInfo.Groups.Any(x => x.ControlPoints.Any(y => y is TimingControlPoint) && x.Time < selectedGroup.Time) ? selectedGroup.Time : double.MinValue; double nextGroupTime = beatmap.ControlPointInfo.Groups.FirstOrDefault(x => x.ControlPoints.Any(y => y is TimingControlPoint) && x.Time > selectedGroup.Time)?.Time ?? double.MaxValue; - var result = beatmap.HitObjects.Where(x => Precision.AlmostBigger(x.StartTime, firstGroupTime) && Precision.DefinitelyBigger(nextGroupTime, x.StartTime)).ToList(); - Console.WriteLine(firstGroupTime + ", " + nextGroupTime + ", " + result.Count); - return result; + return beatmap.HitObjects.Where(x => Precision.AlmostBigger(x.StartTime, firstGroupTime) && Precision.DefinitelyBigger(nextGroupTime, x.StartTime)).ToList(); } private void adjustOffset(double adjust) From 649cfb11fc76b70573fe865f2a4669b1e5a804e9 Mon Sep 17 00:00:00 2001 From: Aurelian Date: Sun, 2 Jun 2024 16:59:16 +0200 Subject: [PATCH 003/120] To satisfy CodeFactor --- osu.Game/Screens/Edit/Timing/TapTimingControl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Timing/TapTimingControl.cs b/osu.Game/Screens/Edit/Timing/TapTimingControl.cs index c4d67eaf1a97..952bdf0ccce0 100644 --- a/osu.Game/Screens/Edit/Timing/TapTimingControl.cs +++ b/osu.Game/Screens/Edit/Timing/TapTimingControl.cs @@ -270,7 +270,7 @@ private void adjustBpm(double adjust) { double beat = (hitObject.StartTime - selectedGroup.Value!.Time) / timing.BeatLength; - hitObject.StartTime = beat * newBeatLength + selectedGroup.Value.Time; + hitObject.StartTime = (beat * newBeatLength) + selectedGroup.Value.Time; } } From a940809fbf8694486f9b1f0c3fb0f6ce243026e7 Mon Sep 17 00:00:00 2001 From: Aurelian Date: Mon, 3 Jun 2024 14:36:53 +0200 Subject: [PATCH 004/120] Addressed some more code maintainability issues --- osu.Game/Screens/Edit/Timing/TapTimingControl.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Edit/Timing/TapTimingControl.cs b/osu.Game/Screens/Edit/Timing/TapTimingControl.cs index 952bdf0ccce0..6ef7bd1a4041 100644 --- a/osu.Game/Screens/Edit/Timing/TapTimingControl.cs +++ b/osu.Game/Screens/Edit/Timing/TapTimingControl.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; +using System.Runtime.CompilerServices; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -210,13 +211,13 @@ private void reset() editorClock.Seek(selectedGroup.Value.Time); } - private List hitObjectsInTimingRange(EditorBeatmap beatmap, ControlPointGroup selectedGroup) + private static List hitObjectsInTimingRange(EditorBeatmap beatmap, ControlPointGroup selectedGroup) { // If the first group, we grab all hitobjects prior to the next, if the last group, we grab all remaining hitobjects - double firstGroupTime = beatmap.ControlPointInfo.Groups.Any(x => x.ControlPoints.Any(y => y is TimingControlPoint) && x.Time < selectedGroup.Time) ? selectedGroup.Time : double.MinValue; - double nextGroupTime = beatmap.ControlPointInfo.Groups.FirstOrDefault(x => x.ControlPoints.Any(y => y is TimingControlPoint) && x.Time > selectedGroup.Time)?.Time ?? double.MaxValue; + double startTime = beatmap.ControlPointInfo.TimingPoints.Any(x => x.Time < selectedGroup.Time) ? selectedGroup.Time : double.MinValue; + double endTime = beatmap.ControlPointInfo.TimingPoints.FirstOrDefault(x => x.Time > selectedGroup.Time)?.Time ?? double.MaxValue; - return beatmap.HitObjects.Where(x => Precision.AlmostBigger(x.StartTime, firstGroupTime) && Precision.DefinitelyBigger(nextGroupTime, x.StartTime)).ToList(); + return beatmap.HitObjects.Where(x => Precision.AlmostBigger(x.StartTime, startTime) && Precision.DefinitelyBigger(endTime, x.StartTime)).ToList(); } private void adjustOffset(double adjust) From 09f3fb9eeea540197032c187688db03e036863bf Mon Sep 17 00:00:00 2001 From: Aurelian Date: Mon, 3 Jun 2024 23:02:57 +0200 Subject: [PATCH 005/120] Compatible IHasDuration hitobjects now scale with BPM changes Does not apply to hitobjects with IHasRepeats --- osu.Game/Screens/Edit/Timing/TapTimingControl.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Timing/TapTimingControl.cs b/osu.Game/Screens/Edit/Timing/TapTimingControl.cs index 6ef7bd1a4041..cffa31d117b3 100644 --- a/osu.Game/Screens/Edit/Timing/TapTimingControl.cs +++ b/osu.Game/Screens/Edit/Timing/TapTimingControl.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; -using System.Runtime.CompilerServices; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -18,6 +17,7 @@ using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Overlays; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; using osuTK; namespace osu.Game.Screens.Edit.Timing @@ -272,6 +272,9 @@ private void adjustBpm(double adjust) double beat = (hitObject.StartTime - selectedGroup.Value!.Time) / timing.BeatLength; hitObject.StartTime = (beat * newBeatLength) + selectedGroup.Value.Time; + + if (hitObject is not IHasRepeats && hitObject is IHasDuration hitObjectWithDuration) + hitObjectWithDuration.Duration *= newBeatLength / timing.BeatLength; } } From 3d5a04ac99d4b9f2002a24206106472c53da5c7b Mon Sep 17 00:00:00 2001 From: Aurelian Date: Wed, 5 Jun 2024 17:57:44 +0200 Subject: [PATCH 006/120] HitObject has defaults applied on bpm/offset adjustment --- osu.Game/Screens/Edit/Timing/TapTimingControl.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game/Screens/Edit/Timing/TapTimingControl.cs b/osu.Game/Screens/Edit/Timing/TapTimingControl.cs index cffa31d117b3..739b9894ce07 100644 --- a/osu.Game/Screens/Edit/Timing/TapTimingControl.cs +++ b/osu.Game/Screens/Edit/Timing/TapTimingControl.cs @@ -252,6 +252,9 @@ private void adjustOffset(double adjust) if (!editorClock.IsRunning && wasAtStart) editorClock.Seek(newOffset); + + foreach (HitObject hitObject in hitObjectsInRange) + hitObject.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty); } private void adjustBpm(double adjust) @@ -279,6 +282,9 @@ private void adjustBpm(double adjust) } timing.BeatLength = newBeatLength; + + foreach (HitObject hitObject in hitObjectsInRange) + hitObject.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty); } private partial class InlineButton : OsuButton From 2db55f9379d6171ac9b583a883301d07dc03446a Mon Sep 17 00:00:00 2001 From: Aurelian Date: Fri, 7 Jun 2024 08:21:09 +0200 Subject: [PATCH 007/120] Change HitObject update call to use established patterns --- osu.Game/Screens/Edit/Timing/TapTimingControl.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Timing/TapTimingControl.cs b/osu.Game/Screens/Edit/Timing/TapTimingControl.cs index 739b9894ce07..1c7f89c80d16 100644 --- a/osu.Game/Screens/Edit/Timing/TapTimingControl.cs +++ b/osu.Game/Screens/Edit/Timing/TapTimingControl.cs @@ -254,7 +254,7 @@ private void adjustOffset(double adjust) editorClock.Seek(newOffset); foreach (HitObject hitObject in hitObjectsInRange) - hitObject.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty); + beatmap.Update(hitObject); } private void adjustBpm(double adjust) @@ -284,7 +284,7 @@ private void adjustBpm(double adjust) timing.BeatLength = newBeatLength; foreach (HitObject hitObject in hitObjectsInRange) - hitObject.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty); + beatmap.Update(hitObject); } private partial class InlineButton : OsuButton From 559f94aa02a138154a136fbb1708ca01d83b3b5b Mon Sep 17 00:00:00 2001 From: Aurelian Date: Fri, 7 Jun 2024 21:57:12 +0200 Subject: [PATCH 008/120] Moved HitObject adjustments to TimingControlPoint --- .../ControlPoints/TimingControlPoint.cs | 35 +++++++++++++++ .../Screens/Edit/Timing/TapTimingControl.cs | 44 ++++--------------- 2 files changed, 43 insertions(+), 36 deletions(-) diff --git a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs index 4e69486e2d25..a3224a6ab9a0 100644 --- a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs @@ -2,9 +2,14 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; +using System.Linq; using osu.Framework.Bindables; +using osu.Framework.Utils; using osu.Game.Beatmaps.Timing; using osu.Game.Graphics; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; using osuTK.Graphics; namespace osu.Game.Beatmaps.ControlPoints @@ -105,5 +110,35 @@ public bool Equals(TimingControlPoint? other) && BeatLength.Equals(other.BeatLength); public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), TimeSignature, BeatLength, OmitFirstBarLine); + + public List HitObjectsInTimingRange(IBeatmap beatmap) + { + // If the first group, we grab all hitobjects prior to the next, if the last group, we grab all remaining hitobjects + double startTime = beatmap.ControlPointInfo.TimingPoints.Any(x => x.Time < Time) ? Time : double.MinValue; + double endTime = beatmap.ControlPointInfo.TimingPoints.FirstOrDefault(x => x.Time > Time)?.Time ?? double.MaxValue; + + return beatmap.HitObjects.Where(x => Precision.AlmostBigger(x.StartTime, startTime) && Precision.DefinitelyBigger(endTime, x.StartTime)).ToList(); + } + + public void AdjustHitObjectOffset(IBeatmap beatmap, double adjust) + { + foreach (HitObject hitObject in HitObjectsInTimingRange(beatmap)) + { + hitObject.StartTime += adjust; + } + } + + public void SetHitObjectBPM(IBeatmap beatmap, double newBeatLength) + { + foreach (HitObject hitObject in HitObjectsInTimingRange(beatmap)) + { + double beat = (hitObject.StartTime - Time) / BeatLength; + + hitObject.StartTime = (beat * newBeatLength) + Time; + + if (hitObject is not IHasRepeats && hitObject is IHasDuration hitObjectWithDuration) + hitObjectWithDuration.Duration *= newBeatLength / BeatLength; + } + } } } diff --git a/osu.Game/Screens/Edit/Timing/TapTimingControl.cs b/osu.Game/Screens/Edit/Timing/TapTimingControl.cs index 1c7f89c80d16..49d3df4aefa7 100644 --- a/osu.Game/Screens/Edit/Timing/TapTimingControl.cs +++ b/osu.Game/Screens/Edit/Timing/TapTimingControl.cs @@ -211,15 +211,6 @@ private void reset() editorClock.Seek(selectedGroup.Value.Time); } - private static List hitObjectsInTimingRange(EditorBeatmap beatmap, ControlPointGroup selectedGroup) - { - // If the first group, we grab all hitobjects prior to the next, if the last group, we grab all remaining hitobjects - double startTime = beatmap.ControlPointInfo.TimingPoints.Any(x => x.Time < selectedGroup.Time) ? selectedGroup.Time : double.MinValue; - double endTime = beatmap.ControlPointInfo.TimingPoints.FirstOrDefault(x => x.Time > selectedGroup.Time)?.Time ?? double.MaxValue; - - return beatmap.HitObjects.Where(x => Precision.AlmostBigger(x.StartTime, startTime) && Precision.DefinitelyBigger(endTime, x.StartTime)).ToList(); - } - private void adjustOffset(double adjust) { if (selectedGroup.Value == null) @@ -227,8 +218,6 @@ private void adjustOffset(double adjust) bool wasAtStart = editorClock.CurrentTimeAccurate == selectedGroup.Value.Time; - List hitObjectsInRange = hitObjectsInTimingRange(beatmap, selectedGroup.Value); - // VERY TEMPORARY var currentGroupItems = selectedGroup.Value.ControlPoints.ToArray(); @@ -237,26 +226,22 @@ private void adjustOffset(double adjust) double newOffset = selectedGroup.Value.Time + adjust; foreach (var cp in currentGroupItems) + { + if (adjustPlacedNotes.Current.Value && cp is TimingControlPoint tp) + tp.AdjustHitObjectOffset(beatmap, adjust); beatmap.ControlPointInfo.Add(newOffset, cp); + } // the control point might not necessarily exist yet, if currentGroupItems was empty. selectedGroup.Value = beatmap.ControlPointInfo.GroupAt(newOffset, true); - if (adjustPlacedNotes.Current.Value) - { - foreach (HitObject hitObject in hitObjectsInRange) - { - hitObject.StartTime += adjust; - } - } - if (!editorClock.IsRunning && wasAtStart) editorClock.Seek(newOffset); - foreach (HitObject hitObject in hitObjectsInRange) - beatmap.Update(hitObject); + beatmap.UpdateAllHitObjects(); } + private void adjustBpm(double adjust) { var timing = selectedGroup.Value?.ControlPoints.OfType().FirstOrDefault(); @@ -266,25 +251,12 @@ private void adjustBpm(double adjust) double newBeatLength = 60000 / (timing.BPM + adjust); - List hitObjectsInRange = hitObjectsInTimingRange(beatmap, selectedGroup.Value!); - if (adjustPlacedNotes.Current.Value) - { - foreach (HitObject hitObject in hitObjectsInRange) - { - double beat = (hitObject.StartTime - selectedGroup.Value!.Time) / timing.BeatLength; - - hitObject.StartTime = (beat * newBeatLength) + selectedGroup.Value.Time; - - if (hitObject is not IHasRepeats && hitObject is IHasDuration hitObjectWithDuration) - hitObjectWithDuration.Duration *= newBeatLength / timing.BeatLength; - } - } + timing.SetHitObjectBPM(beatmap, newBeatLength); timing.BeatLength = newBeatLength; - foreach (HitObject hitObject in hitObjectsInRange) - beatmap.Update(hitObject); + beatmap.UpdateAllHitObjects(); } private partial class InlineButton : OsuButton From ac0c425e2921a2b84467b4703eacff35d380dba7 Mon Sep 17 00:00:00 2001 From: Aurelian Date: Sun, 9 Jun 2024 11:27:53 +0200 Subject: [PATCH 009/120] Moved setting to the menu bar --- .../ControlPoints/TimingControlPoint.cs | 8 +++--- osu.Game/Localisation/EditorStrings.cs | 5 ++++ osu.Game/Screens/Edit/Editor.cs | 6 ++++- osu.Game/Screens/Edit/EditorBeatmap.cs | 2 ++ .../Screens/Edit/Timing/TapTimingControl.cs | 25 +++---------------- 5 files changed, 20 insertions(+), 26 deletions(-) diff --git a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs index a3224a6ab9a0..234a34dc878c 100644 --- a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs @@ -128,16 +128,16 @@ public void AdjustHitObjectOffset(IBeatmap beatmap, double adjust) } } - public void SetHitObjectBPM(IBeatmap beatmap, double newBeatLength) + public void SetHitObjectBPM(IBeatmap beatmap, double oldBeatLength) { foreach (HitObject hitObject in HitObjectsInTimingRange(beatmap)) { - double beat = (hitObject.StartTime - Time) / BeatLength; + double beat = (hitObject.StartTime - Time) / oldBeatLength; - hitObject.StartTime = (beat * newBeatLength) + Time; + hitObject.StartTime = (beat * BeatLength) + Time; if (hitObject is not IHasRepeats && hitObject is IHasDuration hitObjectWithDuration) - hitObjectWithDuration.Duration *= newBeatLength / BeatLength; + hitObjectWithDuration.Duration *= BeatLength / oldBeatLength; } } } diff --git a/osu.Game/Localisation/EditorStrings.cs b/osu.Game/Localisation/EditorStrings.cs index 6ad12f54df2e..b604fc388974 100644 --- a/osu.Game/Localisation/EditorStrings.cs +++ b/osu.Game/Localisation/EditorStrings.cs @@ -39,6 +39,11 @@ public static class EditorStrings /// public static LocalisableString SetPreviewPointToCurrent => new TranslatableString(getKey(@"set_preview_point_to_current"), @"Set preview point to current time"); + /// + /// "Move already placed notes when changing the offset / BPM" + /// + public static LocalisableString AdjustNotesOnOffsetBPMChange => new TranslatableString(getKey(@"adjust_notes_on_offset_bpm_change"), @"Move already placed notes when changing the offset / BPM"); + /// /// "For editing (.olz)" /// diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 07c32983f5f3..af099413fd17 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -367,7 +367,11 @@ private void load(OsuConfigManager config) { Items = new MenuItem[] { - new EditorMenuItem(EditorStrings.SetPreviewPointToCurrent, MenuItemType.Standard, SetPreviewPointToCurrentTime) + new EditorMenuItem(EditorStrings.SetPreviewPointToCurrent, MenuItemType.Standard, SetPreviewPointToCurrentTime), + new ToggleMenuItem(EditorStrings.AdjustNotesOnOffsetBPMChange) + { + State = { BindTarget = editorBeatmap.AdjustNotesOnOffsetBPMChange }, + } } } } diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index 7a3ea474fb91..42a3a6d3b225 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -88,6 +88,8 @@ public partial class EditorBeatmap : TransactionalCommitComponent, IBeatmap, IBe public BindableInt PreviewTime { get; } + public Bindable AdjustNotesOnOffsetBPMChange { get; } = new Bindable(false); + private readonly IBeatmapProcessor beatmapProcessor; private readonly Dictionary> startTimeBindables = new Dictionary>(); diff --git a/osu.Game/Screens/Edit/Timing/TapTimingControl.cs b/osu.Game/Screens/Edit/Timing/TapTimingControl.cs index 49d3df4aefa7..30e591941182 100644 --- a/osu.Game/Screens/Edit/Timing/TapTimingControl.cs +++ b/osu.Game/Screens/Edit/Timing/TapTimingControl.cs @@ -37,8 +37,6 @@ public partial class TapTimingControl : CompositeDrawable private MetronomeDisplay metronome = null!; - private LabelledSwitchButton adjustPlacedNotes = null!; - [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider, OsuColour colours) { @@ -65,7 +63,6 @@ private void load(OverlayColourProvider colourProvider, OsuColour colours) { new Dimension(GridSizeMode.Absolute, 200), new Dimension(GridSizeMode.Absolute, 50), - new Dimension(GridSizeMode.Absolute, 50), new Dimension(GridSizeMode.Absolute, TapButton.SIZE + padding), }, Content = new[] @@ -123,18 +120,6 @@ private void load(OverlayColourProvider colourProvider, OsuColour colours) }, }, new Drawable[] - { - new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Bottom = padding, Horizontal = padding }, - Children = new Drawable[] - { - adjustPlacedNotes = new LabelledSwitchButton { Label = "Move already placed notes\nwhen changing the offset/BPM" }, - } - }, - }, - new Drawable[] { new Container { @@ -227,7 +212,7 @@ private void adjustOffset(double adjust) foreach (var cp in currentGroupItems) { - if (adjustPlacedNotes.Current.Value && cp is TimingControlPoint tp) + if (beatmap.AdjustNotesOnOffsetBPMChange.Value && cp is TimingControlPoint tp) tp.AdjustHitObjectOffset(beatmap, adjust); beatmap.ControlPointInfo.Add(newOffset, cp); } @@ -249,12 +234,10 @@ private void adjustBpm(double adjust) if (timing == null) return; - double newBeatLength = 60000 / (timing.BPM + adjust); - - if (adjustPlacedNotes.Current.Value) - timing.SetHitObjectBPM(beatmap, newBeatLength); + timing.BeatLength = 60000 / (timing.BPM + adjust); - timing.BeatLength = newBeatLength; + if (beatmap.AdjustNotesOnOffsetBPMChange.Value) + timing.SetHitObjectBPM(beatmap, 60000 / (timing.BPM - adjust)); beatmap.UpdateAllHitObjects(); } From 33d0d4c8f22b284c784a55ab8e143781a580ed16 Mon Sep 17 00:00:00 2001 From: Aurelian Date: Sun, 9 Jun 2024 11:29:25 +0200 Subject: [PATCH 010/120] Fixed certain UI elements not working for HitObject BPM/Offset adjustment --- osu.Game/Screens/Edit/Timing/GroupSection.cs | 7 +++++++ osu.Game/Screens/Edit/Timing/TapTimingControl.cs | 5 ----- osu.Game/Screens/Edit/Timing/TimingSection.cs | 11 +++++++++++ 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Edit/Timing/GroupSection.cs b/osu.Game/Screens/Edit/Timing/GroupSection.cs index 487a871881e7..7d0eab1f7fc9 100644 --- a/osu.Game/Screens/Edit/Timing/GroupSection.cs +++ b/osu.Game/Screens/Edit/Timing/GroupSection.cs @@ -109,11 +109,18 @@ private void changeSelectedGroupTime(in double time) Beatmap.ControlPointInfo.RemoveGroup(SelectedGroup.Value); foreach (var cp in currentGroupItems) + { + // Only adjust hit object offsets if the group contains a timing control point + if (Beatmap.AdjustNotesOnOffsetBPMChange.Value && cp is TimingControlPoint tp) + tp.AdjustHitObjectOffset(Beatmap, time - SelectedGroup.Value.Time); Beatmap.ControlPointInfo.Add(time, cp); + } // the control point might not necessarily exist yet, if currentGroupItems was empty. SelectedGroup.Value = Beatmap.ControlPointInfo.GroupAt(time, true); + Beatmap.UpdateAllHitObjects(); + changeHandler?.EndChange(); } } diff --git a/osu.Game/Screens/Edit/Timing/TapTimingControl.cs b/osu.Game/Screens/Edit/Timing/TapTimingControl.cs index 30e591941182..937235c7bcc5 100644 --- a/osu.Game/Screens/Edit/Timing/TapTimingControl.cs +++ b/osu.Game/Screens/Edit/Timing/TapTimingControl.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.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -10,14 +9,11 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; -using osu.Framework.Utils; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Overlays; -using osu.Game.Rulesets.Objects; -using osu.Game.Rulesets.Objects.Types; using osuTK; namespace osu.Game.Screens.Edit.Timing @@ -226,7 +222,6 @@ private void adjustOffset(double adjust) beatmap.UpdateAllHitObjects(); } - private void adjustBpm(double adjust) { var timing = selectedGroup.Value?.ControlPoints.OfType().FirstOrDefault(); diff --git a/osu.Game/Screens/Edit/Timing/TimingSection.cs b/osu.Game/Screens/Edit/Timing/TimingSection.cs index 2757753b0729..24cf865ce39e 100644 --- a/osu.Game/Screens/Edit/Timing/TimingSection.cs +++ b/osu.Game/Screens/Edit/Timing/TimingSection.cs @@ -42,6 +42,17 @@ void saveChanges() { if (!isRebinding) ChangeHandler?.SaveState(); } + + bpmTextEntry.Bindable.BindValueChanged(val => + { + if (ControlPoint.Value == null) + return; + + ChangeHandler?.BeginChange(); + ControlPoint.Value.SetHitObjectBPM(Beatmap, val.OldValue); + Beatmap.UpdateAllHitObjects(); + ChangeHandler?.EndChange(); + }); } private bool isRebinding; From 101887d3154306e53021352f47c609f90362c913 Mon Sep 17 00:00:00 2001 From: Aurelian Date: Sun, 9 Jun 2024 18:04:27 +0200 Subject: [PATCH 011/120] Notes aren't adjusted if setting is off --- osu.Game/Screens/Edit/Timing/TimingSection.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Timing/TimingSection.cs b/osu.Game/Screens/Edit/Timing/TimingSection.cs index 24cf865ce39e..1ae85e6c9ea1 100644 --- a/osu.Game/Screens/Edit/Timing/TimingSection.cs +++ b/osu.Game/Screens/Edit/Timing/TimingSection.cs @@ -45,7 +45,7 @@ void saveChanges() bpmTextEntry.Bindable.BindValueChanged(val => { - if (ControlPoint.Value == null) + if (!Beatmap.AdjustNotesOnOffsetBPMChange.Value || ControlPoint.Value == null) return; ChangeHandler?.BeginChange(); From 9906ab3449719c0d01b3f847edbabe73359a3d95 Mon Sep 17 00:00:00 2001 From: Aurelian Date: Wed, 12 Jun 2024 19:25:48 +0200 Subject: [PATCH 012/120] Fixed double adjustment of hitobject beatlength --- osu.Game/Screens/Edit/Timing/TapTimingControl.cs | 3 --- osu.Game/Screens/Edit/Timing/TimingSection.cs | 2 -- 2 files changed, 5 deletions(-) diff --git a/osu.Game/Screens/Edit/Timing/TapTimingControl.cs b/osu.Game/Screens/Edit/Timing/TapTimingControl.cs index 937235c7bcc5..55404702acf3 100644 --- a/osu.Game/Screens/Edit/Timing/TapTimingControl.cs +++ b/osu.Game/Screens/Edit/Timing/TapTimingControl.cs @@ -231,9 +231,6 @@ private void adjustBpm(double adjust) timing.BeatLength = 60000 / (timing.BPM + adjust); - if (beatmap.AdjustNotesOnOffsetBPMChange.Value) - timing.SetHitObjectBPM(beatmap, 60000 / (timing.BPM - adjust)); - beatmap.UpdateAllHitObjects(); } diff --git a/osu.Game/Screens/Edit/Timing/TimingSection.cs b/osu.Game/Screens/Edit/Timing/TimingSection.cs index 1ae85e6c9ea1..5be1de467d49 100644 --- a/osu.Game/Screens/Edit/Timing/TimingSection.cs +++ b/osu.Game/Screens/Edit/Timing/TimingSection.cs @@ -48,10 +48,8 @@ void saveChanges() if (!Beatmap.AdjustNotesOnOffsetBPMChange.Value || ControlPoint.Value == null) return; - ChangeHandler?.BeginChange(); ControlPoint.Value.SetHitObjectBPM(Beatmap, val.OldValue); Beatmap.UpdateAllHitObjects(); - ChangeHandler?.EndChange(); }); } From 9b076a8b03f7f7258e2c20b237ab94490c245e42 Mon Sep 17 00:00:00 2001 From: Aurelian Date: Wed, 12 Jun 2024 19:57:17 +0200 Subject: [PATCH 013/120] Moved HitObject adjustment methods to a static helper class --- .../ControlPoints/TimingControlPoint.cs | 35 ---------------- osu.Game/Screens/Edit/Timing/GroupSection.cs | 2 +- .../Screens/Edit/Timing/TapTimingControl.cs | 2 +- osu.Game/Screens/Edit/Timing/TimingSection.cs | 41 ++++++++++++++++++- 4 files changed, 42 insertions(+), 38 deletions(-) diff --git a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs index 234a34dc878c..4e69486e2d25 100644 --- a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs @@ -2,14 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; -using System.Linq; using osu.Framework.Bindables; -using osu.Framework.Utils; using osu.Game.Beatmaps.Timing; using osu.Game.Graphics; -using osu.Game.Rulesets.Objects; -using osu.Game.Rulesets.Objects.Types; using osuTK.Graphics; namespace osu.Game.Beatmaps.ControlPoints @@ -110,35 +105,5 @@ public bool Equals(TimingControlPoint? other) && BeatLength.Equals(other.BeatLength); public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), TimeSignature, BeatLength, OmitFirstBarLine); - - public List HitObjectsInTimingRange(IBeatmap beatmap) - { - // If the first group, we grab all hitobjects prior to the next, if the last group, we grab all remaining hitobjects - double startTime = beatmap.ControlPointInfo.TimingPoints.Any(x => x.Time < Time) ? Time : double.MinValue; - double endTime = beatmap.ControlPointInfo.TimingPoints.FirstOrDefault(x => x.Time > Time)?.Time ?? double.MaxValue; - - return beatmap.HitObjects.Where(x => Precision.AlmostBigger(x.StartTime, startTime) && Precision.DefinitelyBigger(endTime, x.StartTime)).ToList(); - } - - public void AdjustHitObjectOffset(IBeatmap beatmap, double adjust) - { - foreach (HitObject hitObject in HitObjectsInTimingRange(beatmap)) - { - hitObject.StartTime += adjust; - } - } - - public void SetHitObjectBPM(IBeatmap beatmap, double oldBeatLength) - { - foreach (HitObject hitObject in HitObjectsInTimingRange(beatmap)) - { - double beat = (hitObject.StartTime - Time) / oldBeatLength; - - hitObject.StartTime = (beat * BeatLength) + Time; - - if (hitObject is not IHasRepeats && hitObject is IHasDuration hitObjectWithDuration) - hitObjectWithDuration.Duration *= BeatLength / oldBeatLength; - } - } } } diff --git a/osu.Game/Screens/Edit/Timing/GroupSection.cs b/osu.Game/Screens/Edit/Timing/GroupSection.cs index 7d0eab1f7fc9..c5a4422192a5 100644 --- a/osu.Game/Screens/Edit/Timing/GroupSection.cs +++ b/osu.Game/Screens/Edit/Timing/GroupSection.cs @@ -112,7 +112,7 @@ private void changeSelectedGroupTime(in double time) { // Only adjust hit object offsets if the group contains a timing control point if (Beatmap.AdjustNotesOnOffsetBPMChange.Value && cp is TimingControlPoint tp) - tp.AdjustHitObjectOffset(Beatmap, time - SelectedGroup.Value.Time); + TimingSectionAdjustments.AdjustHitObjectOffset(Beatmap, tp, time - SelectedGroup.Value.Time); Beatmap.ControlPointInfo.Add(time, cp); } diff --git a/osu.Game/Screens/Edit/Timing/TapTimingControl.cs b/osu.Game/Screens/Edit/Timing/TapTimingControl.cs index 55404702acf3..f2e37369d1f8 100644 --- a/osu.Game/Screens/Edit/Timing/TapTimingControl.cs +++ b/osu.Game/Screens/Edit/Timing/TapTimingControl.cs @@ -209,7 +209,7 @@ private void adjustOffset(double adjust) foreach (var cp in currentGroupItems) { if (beatmap.AdjustNotesOnOffsetBPMChange.Value && cp is TimingControlPoint tp) - tp.AdjustHitObjectOffset(beatmap, adjust); + TimingSectionAdjustments.AdjustHitObjectOffset(beatmap, tp, adjust); beatmap.ControlPointInfo.Add(newOffset, cp); } diff --git a/osu.Game/Screens/Edit/Timing/TimingSection.cs b/osu.Game/Screens/Edit/Timing/TimingSection.cs index 5be1de467d49..ba335d55eb24 100644 --- a/osu.Game/Screens/Edit/Timing/TimingSection.cs +++ b/osu.Game/Screens/Edit/Timing/TimingSection.cs @@ -1,11 +1,17 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Utils; +using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; namespace osu.Game.Screens.Edit.Timing { @@ -48,7 +54,7 @@ void saveChanges() if (!Beatmap.AdjustNotesOnOffsetBPMChange.Value || ControlPoint.Value == null) return; - ControlPoint.Value.SetHitObjectBPM(Beatmap, val.OldValue); + TimingSectionAdjustments.SetHitObjectBPM(Beatmap, ControlPoint.Value, val.OldValue); Beatmap.UpdateAllHitObjects(); }); } @@ -128,4 +134,37 @@ public Bindable Bindable private static double beatLengthToBpm(double beatLength) => 60000 / beatLength; } + + public static class TimingSectionAdjustments + { + public static List HitObjectsInTimingRange(IBeatmap beatmap, double time) + { + // If the first group, we grab all hitobjects prior to the next, if the last group, we grab all remaining hitobjects + double startTime = beatmap.ControlPointInfo.TimingPoints.Any(x => x.Time < time) ? time : double.MinValue; + double endTime = beatmap.ControlPointInfo.TimingPoints.FirstOrDefault(x => x.Time > time)?.Time ?? double.MaxValue; + + return beatmap.HitObjects.Where(x => Precision.AlmostBigger(x.StartTime, startTime) && Precision.DefinitelyBigger(endTime, x.StartTime)).ToList(); + } + + public static void AdjustHitObjectOffset(IBeatmap beatmap, TimingControlPoint timingControlPoint, double adjust) + { + foreach (HitObject hitObject in HitObjectsInTimingRange(beatmap, timingControlPoint.Time)) + { + hitObject.StartTime += adjust; + } + } + + public static void SetHitObjectBPM(IBeatmap beatmap, TimingControlPoint timingControlPoint, double oldBeatLength) + { + foreach (HitObject hitObject in HitObjectsInTimingRange(beatmap, timingControlPoint.Time)) + { + double beat = (hitObject.StartTime - timingControlPoint.Time) / oldBeatLength; + + hitObject.StartTime = (beat * timingControlPoint.BeatLength) + timingControlPoint.Time; + + if (hitObject is not IHasRepeats && hitObject is IHasDuration hitObjectWithDuration) + hitObjectWithDuration.Duration *= timingControlPoint.BeatLength / oldBeatLength; + } + } + } } From ca2dc702e612986ccb40d7dcfe9fdb5acdb2d79b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 2 Sep 2024 09:52:00 +0200 Subject: [PATCH 014/120] Move helper class out to separate file --- osu.Game/Screens/Edit/Timing/TimingSection.cs | 39 ---------------- .../Edit/Timing/TimingSectionAdjustments.cs | 46 +++++++++++++++++++ 2 files changed, 46 insertions(+), 39 deletions(-) create mode 100644 osu.Game/Screens/Edit/Timing/TimingSectionAdjustments.cs diff --git a/osu.Game/Screens/Edit/Timing/TimingSection.cs b/osu.Game/Screens/Edit/Timing/TimingSection.cs index 4b47ecaab623..139f53a961a9 100644 --- a/osu.Game/Screens/Edit/Timing/TimingSection.cs +++ b/osu.Game/Screens/Edit/Timing/TimingSection.cs @@ -1,17 +1,11 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Generic; -using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Utils; -using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics.UserInterfaceV2; -using osu.Game.Rulesets.Objects; -using osu.Game.Rulesets.Objects.Types; namespace osu.Game.Screens.Edit.Timing { @@ -135,37 +129,4 @@ public Bindable Bindable private static double beatLengthToBpm(double beatLength) => 60000 / beatLength; } - - public static class TimingSectionAdjustments - { - public static List HitObjectsInTimingRange(IBeatmap beatmap, double time) - { - // If the first group, we grab all hitobjects prior to the next, if the last group, we grab all remaining hitobjects - double startTime = beatmap.ControlPointInfo.TimingPoints.Any(x => x.Time < time) ? time : double.MinValue; - double endTime = beatmap.ControlPointInfo.TimingPoints.FirstOrDefault(x => x.Time > time)?.Time ?? double.MaxValue; - - return beatmap.HitObjects.Where(x => Precision.AlmostBigger(x.StartTime, startTime) && Precision.DefinitelyBigger(endTime, x.StartTime)).ToList(); - } - - public static void AdjustHitObjectOffset(IBeatmap beatmap, TimingControlPoint timingControlPoint, double adjust) - { - foreach (HitObject hitObject in HitObjectsInTimingRange(beatmap, timingControlPoint.Time)) - { - hitObject.StartTime += adjust; - } - } - - public static void SetHitObjectBPM(IBeatmap beatmap, TimingControlPoint timingControlPoint, double oldBeatLength) - { - foreach (HitObject hitObject in HitObjectsInTimingRange(beatmap, timingControlPoint.Time)) - { - double beat = (hitObject.StartTime - timingControlPoint.Time) / oldBeatLength; - - hitObject.StartTime = (beat * timingControlPoint.BeatLength) + timingControlPoint.Time; - - if (hitObject is not IHasRepeats && hitObject is IHasDuration hitObjectWithDuration) - hitObjectWithDuration.Duration *= timingControlPoint.BeatLength / oldBeatLength; - } - } - } } diff --git a/osu.Game/Screens/Edit/Timing/TimingSectionAdjustments.cs b/osu.Game/Screens/Edit/Timing/TimingSectionAdjustments.cs new file mode 100644 index 000000000000..11c4be4790cb --- /dev/null +++ b/osu.Game/Screens/Edit/Timing/TimingSectionAdjustments.cs @@ -0,0 +1,46 @@ +// 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.Utils; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; + +namespace osu.Game.Screens.Edit.Timing +{ + public static class TimingSectionAdjustments + { + public static List HitObjectsInTimingRange(IBeatmap beatmap, double time) + { + // If the first group, we grab all hitobjects prior to the next, if the last group, we grab all remaining hitobjects + double startTime = beatmap.ControlPointInfo.TimingPoints.Any(x => x.Time < time) ? time : double.MinValue; + double endTime = beatmap.ControlPointInfo.TimingPoints.FirstOrDefault(x => x.Time > time)?.Time ?? double.MaxValue; + + return beatmap.HitObjects.Where(x => Precision.AlmostBigger(x.StartTime, startTime) && Precision.DefinitelyBigger(endTime, x.StartTime)).ToList(); + } + + public static void AdjustHitObjectOffset(IBeatmap beatmap, TimingControlPoint timingControlPoint, double adjust) + { + foreach (HitObject hitObject in HitObjectsInTimingRange(beatmap, timingControlPoint.Time)) + { + hitObject.StartTime += adjust; + } + } + + public static void SetHitObjectBPM(IBeatmap beatmap, TimingControlPoint timingControlPoint, double oldBeatLength) + { + foreach (HitObject hitObject in HitObjectsInTimingRange(beatmap, timingControlPoint.Time)) + { + double beat = (hitObject.StartTime - timingControlPoint.Time) / oldBeatLength; + + hitObject.StartTime = (beat * timingControlPoint.BeatLength) + timingControlPoint.Time; + + if (hitObject is not IHasRepeats && hitObject is IHasDuration hitObjectWithDuration) + hitObjectWithDuration.Duration *= timingControlPoint.BeatLength / oldBeatLength; + } + } + } +} From e61fd080c14602c489d3ef5b4937bc441b2bbc86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 2 Sep 2024 09:59:19 +0200 Subject: [PATCH 015/120] Retouch & document helper methods --- .../Editing/TimingSectionAdjustmentsTest.cs | 10 ++++++++ .../Edit/Timing/TimingSectionAdjustments.cs | 23 +++++++++++++------ 2 files changed, 26 insertions(+), 7 deletions(-) create mode 100644 osu.Game.Tests/Editing/TimingSectionAdjustmentsTest.cs diff --git a/osu.Game.Tests/Editing/TimingSectionAdjustmentsTest.cs b/osu.Game.Tests/Editing/TimingSectionAdjustmentsTest.cs new file mode 100644 index 000000000000..2cb9e72c1ffd --- /dev/null +++ b/osu.Game.Tests/Editing/TimingSectionAdjustmentsTest.cs @@ -0,0 +1,10 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Tests.Editing +{ + public class TimingSectionAdjustmentsTest + { + + } +} diff --git a/osu.Game/Screens/Edit/Timing/TimingSectionAdjustments.cs b/osu.Game/Screens/Edit/Timing/TimingSectionAdjustments.cs index 11c4be4790cb..65edc47ff526 100644 --- a/osu.Game/Screens/Edit/Timing/TimingSectionAdjustments.cs +++ b/osu.Game/Screens/Edit/Timing/TimingSectionAdjustments.cs @@ -13,26 +13,35 @@ namespace osu.Game.Screens.Edit.Timing { public static class TimingSectionAdjustments { - public static List HitObjectsInTimingRange(IBeatmap beatmap, double time) + /// + /// Returns all objects from which are affected by the supplied . + /// + public static List HitObjectsInTimingRange(IBeatmap beatmap, TimingControlPoint timingControlPoint) { // If the first group, we grab all hitobjects prior to the next, if the last group, we grab all remaining hitobjects - double startTime = beatmap.ControlPointInfo.TimingPoints.Any(x => x.Time < time) ? time : double.MinValue; - double endTime = beatmap.ControlPointInfo.TimingPoints.FirstOrDefault(x => x.Time > time)?.Time ?? double.MaxValue; + double startTime = beatmap.ControlPointInfo.TimingPoints.Any(x => x.Time < timingControlPoint.Time) ? timingControlPoint.Time : double.MinValue; + double endTime = beatmap.ControlPointInfo.TimingPoints.FirstOrDefault(x => x.Time > timingControlPoint.Time)?.Time ?? double.MaxValue; return beatmap.HitObjects.Where(x => Precision.AlmostBigger(x.StartTime, startTime) && Precision.DefinitelyBigger(endTime, x.StartTime)).ToList(); } - public static void AdjustHitObjectOffset(IBeatmap beatmap, TimingControlPoint timingControlPoint, double adjust) + /// + /// Moves all relevant objects after 's offset has been changed by . + /// + public static void AdjustHitObjectOffset(IBeatmap beatmap, TimingControlPoint timingControlPoint, double adjustment) { - foreach (HitObject hitObject in HitObjectsInTimingRange(beatmap, timingControlPoint.Time)) + foreach (HitObject hitObject in HitObjectsInTimingRange(beatmap, timingControlPoint)) { - hitObject.StartTime += adjust; + hitObject.StartTime += adjustment; } } + /// + /// Ensures all relevant objects are still snapped to the same beats after 's beat length / BPM has been changed. + /// public static void SetHitObjectBPM(IBeatmap beatmap, TimingControlPoint timingControlPoint, double oldBeatLength) { - foreach (HitObject hitObject in HitObjectsInTimingRange(beatmap, timingControlPoint.Time)) + foreach (HitObject hitObject in HitObjectsInTimingRange(beatmap, timingControlPoint)) { double beat = (hitObject.StartTime - timingControlPoint.Time) / oldBeatLength; From db608159cf6a6b1dcbcd56a668765d94a9593c77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 2 Sep 2024 10:27:40 +0200 Subject: [PATCH 016/120] Add test coverage --- .../Editing/TimingSectionAdjustmentsTest.cs | 153 +++++++++++++++++- 1 file changed, 152 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Editing/TimingSectionAdjustmentsTest.cs b/osu.Game.Tests/Editing/TimingSectionAdjustmentsTest.cs index 2cb9e72c1ffd..5f5a1760ea8e 100644 --- a/osu.Game.Tests/Editing/TimingSectionAdjustmentsTest.cs +++ b/osu.Game.Tests/Editing/TimingSectionAdjustmentsTest.cs @@ -1,10 +1,161 @@ // 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 NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Screens.Edit.Timing; + namespace osu.Game.Tests.Editing { + [TestFixture] public class TimingSectionAdjustmentsTest { - + [Test] + public void TestOffsetAdjustment() + { + var controlPoints = new ControlPointInfo(); + + controlPoints.Add(100, new TimingControlPoint { BeatLength = 100 }); + controlPoints.Add(50_000, new TimingControlPoint { BeatLength = 200 }); + controlPoints.Add(100_000, new TimingControlPoint { BeatLength = 50 }); + + var beatmap = new Beatmap + { + ControlPointInfo = controlPoints, + HitObjects = new List + { + new HitCircle { StartTime = 0 }, + new HitCircle { StartTime = 200 }, + new HitCircle { StartTime = 49_900 }, + new HitCircle { StartTime = 50_000 }, + new HitCircle { StartTime = 50_200 }, + new HitCircle { StartTime = 99_800 }, + new HitCircle { StartTime = 100_000 }, + new HitCircle { StartTime = 100_050 }, + new HitCircle { StartTime = 100_550 }, + } + }; + + moveTimingPoint(beatmap, 100, -50); + + Assert.Multiple(() => + { + Assert.That(beatmap.HitObjects[0].StartTime, Is.EqualTo(-50)); + Assert.That(beatmap.HitObjects[1].StartTime, Is.EqualTo(150)); + Assert.That(beatmap.HitObjects[2].StartTime, Is.EqualTo(49_850)); + Assert.That(beatmap.HitObjects[3].StartTime, Is.EqualTo(50_000)); + }); + + moveTimingPoint(beatmap, 50_000, 1_000); + + Assert.Multiple(() => + { + Assert.That(beatmap.HitObjects[2].StartTime, Is.EqualTo(49_850)); + Assert.That(beatmap.HitObjects[3].StartTime, Is.EqualTo(51_000)); + Assert.That(beatmap.HitObjects[4].StartTime, Is.EqualTo(51_200)); + Assert.That(beatmap.HitObjects[5].StartTime, Is.EqualTo(100_800)); + Assert.That(beatmap.HitObjects[6].StartTime, Is.EqualTo(100_000)); + }); + + moveTimingPoint(beatmap, 100_000, 10_000); + + Assert.Multiple(() => + { + Assert.That(beatmap.HitObjects[4].StartTime, Is.EqualTo(51_200)); + Assert.That(beatmap.HitObjects[5].StartTime, Is.EqualTo(110_800)); + Assert.That(beatmap.HitObjects[6].StartTime, Is.EqualTo(110_000)); + Assert.That(beatmap.HitObjects[7].StartTime, Is.EqualTo(110_050)); + Assert.That(beatmap.HitObjects[8].StartTime, Is.EqualTo(110_550)); + }); + } + + [Test] + public void TestBPMAdjustment() + { + var controlPoints = new ControlPointInfo(); + + controlPoints.Add(100, new TimingControlPoint { BeatLength = 100 }); + controlPoints.Add(50_000, new TimingControlPoint { BeatLength = 200 }); + controlPoints.Add(100_000, new TimingControlPoint { BeatLength = 50 }); + + var beatmap = new Beatmap + { + ControlPointInfo = controlPoints, + HitObjects = new List + { + new HitCircle { StartTime = 0 }, + new HitCircle { StartTime = 200 }, + new Spinner { StartTime = 500, EndTime = 1000 }, + new HitCircle { StartTime = 49_900 }, + new HitCircle { StartTime = 50_000 }, + new HitCircle { StartTime = 50_200 }, + new HitCircle { StartTime = 99_800 }, + new HitCircle { StartTime = 100_000 }, + new HitCircle { StartTime = 100_050 }, + new HitCircle { StartTime = 100_550 }, + } + }; + + adjustBeatLength(beatmap, 100, 50); + + Assert.Multiple(() => + { + Assert.That(beatmap.HitObjects[0].StartTime, Is.EqualTo(50)); + Assert.That(beatmap.HitObjects[1].StartTime, Is.EqualTo(150)); + Assert.That(beatmap.HitObjects[2].StartTime, Is.EqualTo(300)); + Assert.That(beatmap.HitObjects[2].GetEndTime(), Is.EqualTo(550)); + Assert.That(beatmap.HitObjects[3].StartTime, Is.EqualTo(25_000)); + Assert.That(beatmap.HitObjects[4].StartTime, Is.EqualTo(50_000)); + }); + + adjustBeatLength(beatmap, 50_000, 400); + + Assert.Multiple(() => + { + Assert.That(beatmap.HitObjects[2].StartTime, Is.EqualTo(300)); + Assert.That(beatmap.HitObjects[2].GetEndTime(), Is.EqualTo(550)); + Assert.That(beatmap.HitObjects[3].StartTime, Is.EqualTo(25_000)); + Assert.That(beatmap.HitObjects[4].StartTime, Is.EqualTo(50_000)); + Assert.That(beatmap.HitObjects[5].StartTime, Is.EqualTo(50_400)); + Assert.That(beatmap.HitObjects[6].StartTime, Is.EqualTo(149_600)); + Assert.That(beatmap.HitObjects[7].StartTime, Is.EqualTo(100_000)); + }); + + adjustBeatLength(beatmap, 100_000, 100); + + Assert.Multiple(() => + { + Assert.That(beatmap.HitObjects[5].StartTime, Is.EqualTo(50_400)); + Assert.That(beatmap.HitObjects[6].StartTime, Is.EqualTo(199_200)); + Assert.That(beatmap.HitObjects[7].StartTime, Is.EqualTo(100_000)); + Assert.That(beatmap.HitObjects[8].StartTime, Is.EqualTo(100_100)); + Assert.That(beatmap.HitObjects[9].StartTime, Is.EqualTo(101_100)); + }); + } + + private static void moveTimingPoint(IBeatmap beatmap, double originalTime, double adjustment) + { + var controlPoints = beatmap.ControlPointInfo; + var controlPointGroup = controlPoints.GroupAt(originalTime); + var timingPoint = controlPointGroup.ControlPoints.OfType().Single(); + controlPoints.RemoveGroup(controlPointGroup); + TimingSectionAdjustments.AdjustHitObjectOffset(beatmap, timingPoint, adjustment); + controlPoints.Add(originalTime - adjustment, timingPoint); + } + + private static void adjustBeatLength(IBeatmap beatmap, double groupTime, double newBeatLength) + { + var controlPoints = beatmap.ControlPointInfo; + var controlPointGroup = controlPoints.GroupAt(groupTime); + var timingPoint = controlPointGroup.ControlPoints.OfType().Single(); + double oldBeatLength = timingPoint.BeatLength; + timingPoint.BeatLength = newBeatLength; + TimingSectionAdjustments.SetHitObjectBPM(beatmap, timingPoint, oldBeatLength); + } } } From 3eaffbb70a0ca053fb3c3443b8a846d817968a70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 2 Sep 2024 10:40:55 +0200 Subject: [PATCH 017/120] Make application of offset/BPM object adjustments more sane --- osu.Game/Screens/Edit/Timing/GroupSection.cs | 6 ++++-- .../Screens/Edit/Timing/TapTimingControl.cs | 17 ++++++++++++++--- osu.Game/Screens/Edit/Timing/TimingSection.cs | 2 ++ 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Edit/Timing/GroupSection.cs b/osu.Game/Screens/Edit/Timing/GroupSection.cs index 18a222f4147c..abcdf7e4ff19 100644 --- a/osu.Game/Screens/Edit/Timing/GroupSection.cs +++ b/osu.Game/Screens/Edit/Timing/GroupSection.cs @@ -113,15 +113,17 @@ private void changeSelectedGroupTime(in double time) { // Only adjust hit object offsets if the group contains a timing control point if (Beatmap.AdjustNotesOnOffsetBPMChange.Value && cp is TimingControlPoint tp) + { TimingSectionAdjustments.AdjustHitObjectOffset(Beatmap, tp, time - SelectedGroup.Value.Time); + Beatmap.UpdateAllHitObjects(); + } + Beatmap.ControlPointInfo.Add(time, cp); } // the control point might not necessarily exist yet, if currentGroupItems was empty. SelectedGroup.Value = Beatmap.ControlPointInfo.GroupAt(time, true); - Beatmap.UpdateAllHitObjects(); - changeHandler?.EndChange(); } } diff --git a/osu.Game/Screens/Edit/Timing/TapTimingControl.cs b/osu.Game/Screens/Edit/Timing/TapTimingControl.cs index f2e37369d1f8..91a0a43d62d0 100644 --- a/osu.Game/Screens/Edit/Timing/TapTimingControl.cs +++ b/osu.Game/Screens/Edit/Timing/TapTimingControl.cs @@ -202,6 +202,7 @@ private void adjustOffset(double adjust) // VERY TEMPORARY var currentGroupItems = selectedGroup.Value.ControlPoints.ToArray(); + beatmap.BeginChange(); beatmap.ControlPointInfo.RemoveGroup(selectedGroup.Value); double newOffset = selectedGroup.Value.Time + adjust; @@ -209,17 +210,20 @@ private void adjustOffset(double adjust) foreach (var cp in currentGroupItems) { if (beatmap.AdjustNotesOnOffsetBPMChange.Value && cp is TimingControlPoint tp) + { TimingSectionAdjustments.AdjustHitObjectOffset(beatmap, tp, adjust); + beatmap.UpdateAllHitObjects(); + } + beatmap.ControlPointInfo.Add(newOffset, cp); } // the control point might not necessarily exist yet, if currentGroupItems was empty. selectedGroup.Value = beatmap.ControlPointInfo.GroupAt(newOffset, true); + beatmap.EndChange(); if (!editorClock.IsRunning && wasAtStart) editorClock.Seek(newOffset); - - beatmap.UpdateAllHitObjects(); } private void adjustBpm(double adjust) @@ -229,9 +233,16 @@ private void adjustBpm(double adjust) if (timing == null) return; + double oldBeatLength = timing.BeatLength; timing.BeatLength = 60000 / (timing.BPM + adjust); - beatmap.UpdateAllHitObjects(); + if (beatmap.AdjustNotesOnOffsetBPMChange.Value) + { + beatmap.BeginChange(); + TimingSectionAdjustments.SetHitObjectBPM(beatmap, timing, oldBeatLength); + beatmap.UpdateAllHitObjects(); + beatmap.EndChange(); + } } private partial class InlineButton : OsuButton diff --git a/osu.Game/Screens/Edit/Timing/TimingSection.cs b/osu.Game/Screens/Edit/Timing/TimingSection.cs index 139f53a961a9..e1567d65aaa3 100644 --- a/osu.Game/Screens/Edit/Timing/TimingSection.cs +++ b/osu.Game/Screens/Edit/Timing/TimingSection.cs @@ -48,8 +48,10 @@ void saveChanges() if (!Beatmap.AdjustNotesOnOffsetBPMChange.Value || ControlPoint.Value == null) return; + Beatmap.BeginChange(); TimingSectionAdjustments.SetHitObjectBPM(Beatmap, ControlPoint.Value, val.OldValue); Beatmap.UpdateAllHitObjects(); + Beatmap.EndChange(); }); } From 57f1259a336163aac5439e34c88f5c7a1d35b8ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 2 Sep 2024 10:49:31 +0200 Subject: [PATCH 018/120] Fix weirdness around spurious adjustments firing due to overloaded bindable --- osu.Game/Screens/Edit/Timing/TimingSection.cs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Edit/Timing/TimingSection.cs b/osu.Game/Screens/Edit/Timing/TimingSection.cs index e1567d65aaa3..e668120d0d4d 100644 --- a/osu.Game/Screens/Edit/Timing/TimingSection.cs +++ b/osu.Game/Screens/Edit/Timing/TimingSection.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 osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -43,16 +44,16 @@ void saveChanges() if (!isRebinding) ChangeHandler?.SaveState(); } - bpmTextEntry.Bindable.BindValueChanged(val => + bpmTextEntry.OnCommit = (oldBeatLength, _) => { if (!Beatmap.AdjustNotesOnOffsetBPMChange.Value || ControlPoint.Value == null) return; Beatmap.BeginChange(); - TimingSectionAdjustments.SetHitObjectBPM(Beatmap, ControlPoint.Value, val.OldValue); + TimingSectionAdjustments.SetHitObjectBPM(Beatmap, ControlPoint.Value, oldBeatLength); Beatmap.UpdateAllHitObjects(); Beatmap.EndChange(); - }); + }; } private bool isRebinding; @@ -85,6 +86,8 @@ protected override TimingControlPoint CreatePoint() private partial class BPMTextBox : LabelledTextBox { + public new Action? OnCommit { get; set; } + private readonly BindableNumber beatLengthBindable = new TimingControlPoint().BeatLengthBindable; public BPMTextBox() @@ -92,10 +95,12 @@ public BPMTextBox() Label = "BPM"; SelectAllOnFocus = true; - OnCommit += (_, isNew) => + base.OnCommit += (_, isNew) => { if (!isNew) return; + double oldBeatLength = beatLengthBindable.Value; + try { if (double.TryParse(Current.Value, out double doubleVal) && doubleVal > 0) @@ -109,6 +114,7 @@ public BPMTextBox() // This is run regardless of parsing success as the parsed number may not actually trigger a change // due to bindable clamping. Even in such a case we want to update the textbox to a sane visual state. beatLengthBindable.TriggerChange(); + OnCommit?.Invoke(oldBeatLength, beatLengthBindable.Value); }; beatLengthBindable.BindValueChanged(val => From e4cfa7c86f793042c311e706ec1c70fcf86a1a9f Mon Sep 17 00:00:00 2001 From: OliBomby Date: Tue, 17 Sep 2024 11:27:23 +0200 Subject: [PATCH 019/120] Add view menu toggle for sample points --- osu.Game/Configuration/OsuConfigManager.cs | 4 +++- osu.Game/Localisation/EditorStrings.cs | 5 +++++ .../Components/Timeline/SamplePointPiece.cs | 15 ++++++++++++++- osu.Game/Screens/Edit/Editor.cs | 6 ++++++ 4 files changed, 28 insertions(+), 2 deletions(-) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 8d6c244b350c..ae2025fb50c2 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -205,6 +205,7 @@ protected override void InitialiseDefaults() SetDefault(OsuSetting.EditorTimelineShowTimingChanges, true); SetDefault(OsuSetting.EditorTimelineShowTicks, true); + SetDefault(OsuSetting.EditorTimelineShowSamples, true); SetDefault(OsuSetting.AlwaysShowHoldForMenuButton, false); } @@ -431,6 +432,7 @@ public enum OsuSetting HideCountryFlags, EditorTimelineShowTimingChanges, EditorTimelineShowTicks, - AlwaysShowHoldForMenuButton + AlwaysShowHoldForMenuButton, + EditorTimelineShowSamples } } diff --git a/osu.Game/Localisation/EditorStrings.cs b/osu.Game/Localisation/EditorStrings.cs index bcffc18d4d3c..f72a0f4a264b 100644 --- a/osu.Game/Localisation/EditorStrings.cs +++ b/osu.Game/Localisation/EditorStrings.cs @@ -139,6 +139,11 @@ public static class EditorStrings /// public static LocalisableString TimelineShowTicks => new TranslatableString(getKey(@"timeline_show_ticks"), @"Show ticks"); + /// + /// "Show samples" + /// + public static LocalisableString TimelineShowSamples => new TranslatableString(getKey(@"timeline_show_samples"), @"Show samples"); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs index a8cf8723f2d0..3843d56d065a 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs @@ -16,6 +16,7 @@ using osu.Framework.Input.Events; using osu.Framework.Utils; using osu.Game.Audio; +using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; @@ -40,6 +41,8 @@ public partial class SamplePointPiece : HitObjectPointPiece, IHasPopover [Resolved] private Editor? editor { get; set; } + private Bindable samplesVisible = null!; + public SamplePointPiece(HitObject hitObject) { HitObject = hitObject; @@ -53,13 +56,23 @@ public SamplePointPiece(HitObject hitObject) protected virtual double GetTime() => HitObject is IHasRepeats r ? HitObject.StartTime + r.Duration / r.SpanCount() / 2 : HitObject.StartTime; [BackgroundDependencyLoader] - private void load() + private void load(OsuConfigManager config) { HitObject.DefaultsApplied += _ => updateText(); updateText(); if (editor != null) editor.ShowSampleEditPopoverRequested += onShowSampleEditPopoverRequested; + + samplesVisible = config.GetBindable(OsuSetting.EditorTimelineShowSamples); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + samplesVisible.BindValueChanged(visible => this.FadeTo(visible.NewValue ? 1 : 0, 200, Easing.OutQuint)); + this.FadeTo(samplesVisible.Value ? 1 : 0); } protected override void Dispose(bool isDisposing) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 9bb91af80652..b059557b2844 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -215,6 +215,7 @@ protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnl private Bindable editorLimitedDistanceSnap; private Bindable editorTimelineShowTimingChanges; private Bindable editorTimelineShowTicks; + private Bindable editorTimelineShowSamples; /// /// This controls the opacity of components like the timelines, sidebars, etc. @@ -323,6 +324,7 @@ private void load(OsuConfigManager config) editorLimitedDistanceSnap = config.GetBindable(OsuSetting.EditorLimitedDistanceSnap); editorTimelineShowTimingChanges = config.GetBindable(OsuSetting.EditorTimelineShowTimingChanges); editorTimelineShowTicks = config.GetBindable(OsuSetting.EditorTimelineShowTicks); + editorTimelineShowSamples = config.GetBindable(OsuSetting.EditorTimelineShowSamples); AddInternal(new OsuContextMenuContainer { @@ -388,6 +390,10 @@ private void load(OsuConfigManager config) { State = { BindTarget = editorTimelineShowTicks } }, + new ToggleMenuItem(EditorStrings.TimelineShowSamples) + { + State = { BindTarget = editorTimelineShowSamples } + } ] }, new BackgroundDimMenuItem(editorBackgroundDim), From 34087a0f1a303d90bf443fd2d4b71e07049ee450 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Thu, 19 Sep 2024 16:20:26 +0200 Subject: [PATCH 020/120] Reduce sample point pieces to pink dot if zoomed out --- .../Timeline/HitObjectPointPiece.cs | 4 ++- .../Components/Timeline/SamplePointPiece.cs | 30 +++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/HitObjectPointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/HitObjectPointPiece.cs index 4b357d3a6232..76323ac08c1a 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/HitObjectPointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/HitObjectPointPiece.cs @@ -17,6 +17,8 @@ public partial class HitObjectPointPiece : CircularContainer { protected OsuSpriteText Label { get; private set; } + protected Container LabelContainer { get; private set; } + [BackgroundDependencyLoader] private void load(OsuColour colours) { @@ -26,7 +28,7 @@ private void load(OsuColour colours) InternalChildren = new Drawable[] { - new Container + LabelContainer = new Container { AutoSizeAxes = Axes.X, Height = 16, diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs index 3843d56d065a..163870073561 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs @@ -41,6 +41,9 @@ public partial class SamplePointPiece : HitObjectPointPiece, IHasPopover [Resolved] private Editor? editor { get; set; } + [Resolved] + private Timeline? timeline { get; set; } + private Bindable samplesVisible = null!; public SamplePointPiece(HitObject hitObject) @@ -59,6 +62,8 @@ public SamplePointPiece(HitObject hitObject) private void load(OsuConfigManager config) { HitObject.DefaultsApplied += _ => updateText(); + Label.AllowMultiline = false; + LabelContainer.AutoSizeAxes = Axes.None; updateText(); if (editor != null) @@ -75,6 +80,30 @@ protected override void LoadComplete() this.FadeTo(samplesVisible.Value ? 1 : 0); } + private float lastZoom; + private const float zoom_threshold = 40f; + + protected override void Update() + { + base.Update(); + + // Retract visual state if the timeline is zoomed out too far. + if (timeline is null || timeline.Zoom == lastZoom) return; + + lastZoom = timeline.Zoom; + + if (timeline.Zoom < zoom_threshold) + { + Label.FadeOut(200, Easing.OutQuint); + LabelContainer.ResizeWidthTo(16, 200, Easing.OutQuint); + } + else + { + Label.FadeIn(200, Easing.OutQuint); + LabelContainer.ResizeWidthTo(Label.Width, 200, Easing.OutQuint); + } + } + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); @@ -100,6 +129,7 @@ protected override bool OnClick(ClickEvent e) private void updateText() { Label.Text = $"{abbreviateBank(GetBankValue(GetSamples()))} {GetVolumeValue(GetSamples())}"; + LabelContainer.ResizeWidthTo(Label.Width, 200, Easing.OutQuint); } private static string? abbreviateBank(string? bank) From ce41dc46293875b9cb6ac9cd132c0e59c694c69c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 27 Sep 2024 17:13:26 +0900 Subject: [PATCH 021/120] Use bindable flow for zoom handling --- .../Edit/ManiaHitObjectComposer.cs | 2 +- .../Components/Timeline/SamplePointPiece.cs | 39 ++++++++----------- .../Timeline/ZoomableScrollContainer.cs | 15 +++---- 3 files changed, 26 insertions(+), 30 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs index 02a4f3a022f0..4a8f4aa27949 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs @@ -92,7 +92,7 @@ protected override void Update() base.Update(); if (screenWithTimeline?.TimelineArea.Timeline != null) - drawableRuleset.TimelineTimeRange = EditorClock.TrackLength / screenWithTimeline.TimelineArea.Timeline.CurrentZoom / 2; + drawableRuleset.TimelineTimeRange = EditorClock.TrackLength / screenWithTimeline.TimelineArea.Timeline.CurrentZoom.Value / 2; } } } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs index 163870073561..fda907bf242c 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs @@ -72,36 +72,31 @@ private void load(OsuConfigManager config) samplesVisible = config.GetBindable(OsuSetting.EditorTimelineShowSamples); } + private BindableNumber? timelineZoom; + protected override void LoadComplete() { base.LoadComplete(); samplesVisible.BindValueChanged(visible => this.FadeTo(visible.NewValue ? 1 : 0, 200, Easing.OutQuint)); this.FadeTo(samplesVisible.Value ? 1 : 0); - } - - private float lastZoom; - private const float zoom_threshold = 40f; - - protected override void Update() - { - base.Update(); - - // Retract visual state if the timeline is zoomed out too far. - if (timeline is null || timeline.Zoom == lastZoom) return; - lastZoom = timeline.Zoom; - - if (timeline.Zoom < zoom_threshold) - { - Label.FadeOut(200, Easing.OutQuint); - LabelContainer.ResizeWidthTo(16, 200, Easing.OutQuint); - } - else + timelineZoom = timeline?.CurrentZoom.GetBoundCopy(); + timelineZoom?.BindValueChanged(zoom => { - Label.FadeIn(200, Easing.OutQuint); - LabelContainer.ResizeWidthTo(Label.Width, 200, Easing.OutQuint); - } + const float zoom_threshold = 40f; + + if (zoom.NewValue < zoom_threshold) + { + Label.FadeOut(200, Easing.OutQuint); + LabelContainer.ResizeWidthTo(16, 200, Easing.OutQuint); + } + else + { + Label.FadeIn(200, Easing.OutQuint); + LabelContainer.ResizeWidthTo(Label.Width, 200, Easing.OutQuint); + } + }, true); } protected override void Dispose(bool isDisposing) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs index 848c8f9a0ff4..31a0936eb434 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs @@ -3,6 +3,7 @@ using System; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Transforms; @@ -32,10 +33,10 @@ public partial class ZoomableScrollContainer : OsuScrollContainer protected override Container Content => zoomedContent; /// - /// The current zoom level of . + /// The current (final) zoom level of . /// It may differ from during transitions. /// - public float CurrentZoom { get; private set; } = 1; + public BindableFloat CurrentZoom { get; private set; } = new BindableFloat(1); private bool isZoomSetUp; @@ -98,7 +99,7 @@ protected void SetupZoom(float initial, float minimum, float maximum) minZoom = minimum; maxZoom = maximum; - CurrentZoom = zoomTarget = initial; + CurrentZoom.Value = zoomTarget = initial; zoomedContentWidthCache.Invalidate(); isZoomSetUp = true; @@ -124,7 +125,7 @@ private void updateZoom(float value) if (IsLoaded) setZoomTarget(newZoom, ToSpaceOfOtherDrawable(new Vector2(DrawWidth / 2, 0), zoomedContent).X); else - CurrentZoom = zoomTarget = newZoom; + CurrentZoom.Value = zoomTarget = newZoom; } protected override void UpdateAfterChildren() @@ -154,7 +155,7 @@ protected override bool OnScroll(ScrollEvent e) private void updateZoomedContentWidth() { - zoomedContent.Width = DrawWidth * CurrentZoom; + zoomedContent.Width = DrawWidth * CurrentZoom.Value; zoomedContentWidthCache.Validate(); } @@ -238,7 +239,7 @@ protected override void Apply(ZoomableScrollContainer d, double time) float expectedWidth = d.DrawWidth * newZoom; float targetOffset = expectedWidth * (focusPoint / contentSize) - focusOffset; - d.CurrentZoom = newZoom; + d.CurrentZoom.Value = newZoom; d.updateZoomedContentWidth(); // Temporarily here to make sure ScrollTo gets the correct DrawSize for scrollable area. @@ -247,7 +248,7 @@ protected override void Apply(ZoomableScrollContainer d, double time) d.ScrollTo(targetOffset, false); } - protected override void ReadIntoStartValue(ZoomableScrollContainer d) => StartValue = d.CurrentZoom; + protected override void ReadIntoStartValue(ZoomableScrollContainer d) => StartValue = d.CurrentZoom.Value; } } } From 9dcce67b81032cedd9e7d0695d22a70edb70cff0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 27 Sep 2024 17:24:24 +0900 Subject: [PATCH 022/120] Make points slightly smaller when contracted and fix width getting obliterated --- .../Components/Timeline/SamplePointPiece.cs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs index fda907bf242c..1076255b6aa2 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs @@ -74,6 +74,8 @@ private void load(OsuConfigManager config) private BindableNumber? timelineZoom; + private bool contracted; + protected override void LoadComplete() { base.LoadComplete(); @@ -88,13 +90,17 @@ protected override void LoadComplete() if (zoom.NewValue < zoom_threshold) { + contracted = true; Label.FadeOut(200, Easing.OutQuint); - LabelContainer.ResizeWidthTo(16, 200, Easing.OutQuint); + LabelContainer.ResizeTo(new Vector2(12), 200, Easing.OutQuint); + LabelContainer.CornerRadius = 6; } else { + contracted = false; Label.FadeIn(200, Easing.OutQuint); - LabelContainer.ResizeWidthTo(Label.Width, 200, Easing.OutQuint); + LabelContainer.ResizeTo(new Vector2(Label.Width, 16), 200, Easing.OutQuint); + LabelContainer.CornerRadius = 8; } }, true); } @@ -124,7 +130,9 @@ protected override bool OnClick(ClickEvent e) private void updateText() { Label.Text = $"{abbreviateBank(GetBankValue(GetSamples()))} {GetVolumeValue(GetSamples())}"; - LabelContainer.ResizeWidthTo(Label.Width, 200, Easing.OutQuint); + + if (!contracted) + LabelContainer.ResizeWidthTo(Label.Width, 200, Easing.OutQuint); } private static string? abbreviateBank(string? bank) From 48da758c3913f69bcc3cd8cb3096f5fe3a6d5406 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sat, 5 Oct 2024 20:52:45 +0200 Subject: [PATCH 023/120] contract sample point pieces based on smallest gap on the timeline It finds the smallest distance between two sample point pieces on the alive timeline blueprints --- .../Components/Timeline/SamplePointPiece.cs | 20 ++++----- .../Timeline/TimelineBlueprintContainer.cs | 41 +++++++++++++++++++ 2 files changed, 49 insertions(+), 12 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs index 1076255b6aa2..ded7c6605853 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs @@ -42,7 +42,7 @@ public partial class SamplePointPiece : HitObjectPointPiece, IHasPopover private Editor? editor { get; set; } [Resolved] - private Timeline? timeline { get; set; } + private TimelineBlueprintContainer? timelineBlueprintContainer { get; set; } private Bindable samplesVisible = null!; @@ -72,9 +72,7 @@ private void load(OsuConfigManager config) samplesVisible = config.GetBindable(OsuSetting.EditorTimelineShowSamples); } - private BindableNumber? timelineZoom; - - private bool contracted; + private readonly Bindable contracted = new Bindable(); protected override void LoadComplete() { @@ -83,21 +81,19 @@ protected override void LoadComplete() samplesVisible.BindValueChanged(visible => this.FadeTo(visible.NewValue ? 1 : 0, 200, Easing.OutQuint)); this.FadeTo(samplesVisible.Value ? 1 : 0); - timelineZoom = timeline?.CurrentZoom.GetBoundCopy(); - timelineZoom?.BindValueChanged(zoom => - { - const float zoom_threshold = 40f; + if (timelineBlueprintContainer != null) + contracted.BindTo(timelineBlueprintContainer.SamplePointContracted); - if (zoom.NewValue < zoom_threshold) + contracted.BindValueChanged(v => + { + if (v.NewValue) { - contracted = true; Label.FadeOut(200, Easing.OutQuint); LabelContainer.ResizeTo(new Vector2(12), 200, Easing.OutQuint); LabelContainer.CornerRadius = 6; } else { - contracted = false; Label.FadeIn(200, Easing.OutQuint); LabelContainer.ResizeTo(new Vector2(Label.Width, 16), 200, Easing.OutQuint); LabelContainer.CornerRadius = 8; @@ -131,7 +127,7 @@ private void updateText() { Label.Text = $"{abbreviateBank(GetBankValue(GetSamples()))} {GetVolumeValue(GetSamples())}"; - if (!contracted) + if (!contracted.Value) LabelContainer.ResizeWidthTo(Label.Width, 200, Easing.OutQuint); } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs index 740f0b6aac24..b50739c80fe9 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs @@ -15,16 +15,19 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; +using osu.Framework.Layout; using osu.Framework.Utils; using osu.Game.Graphics; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Screens.Edit.Components.Timelines.Summary.Parts; using osuTK; using osuTK.Graphics; namespace osu.Game.Screens.Edit.Compose.Components.Timeline { + [Cached] internal partial class TimelineBlueprintContainer : EditorBlueprintContainer { [Resolved(CanBeNull = true)] @@ -35,6 +38,8 @@ internal partial class TimelineBlueprintContainer : EditorBlueprintContainer private bool hitObjectDragged; + private readonly LayoutValue samplePointContractedStateCache = new LayoutValue(Invalidation.DrawSize); + /// /// Positional input must be received outside the container's bounds, /// in order to handle timeline blueprints which are stacked offscreen. @@ -49,6 +54,8 @@ public TimelineBlueprintContainer(HitObjectComposer composer) Origin = Anchor.Centre; Height = 0.6f; + + AddLayout(samplePointContractedStateCache); } [BackgroundDependencyLoader] @@ -116,11 +123,43 @@ protected override void Update() Composer.Playfield.FutureLifetimeExtension = timeline.VisibleRange / 2; } + updateSamplePointContractedState(); + base.Update(); updateStacking(); } + public Bindable SamplePointContracted = new Bindable(); + + private void updateSamplePointContractedState() + { + if (samplePointContractedStateCache.IsValid) + return; + + const double minimum_gap = 28; + + // Find the smallest time gap between any two sample point pieces + double smallestTimeGap = double.PositiveInfinity; + double lastTime = double.PositiveInfinity; + + // The blueprints are ordered in reverse chronological order + foreach (var selectionBlueprint in SelectionBlueprints) + { + var hitObject = selectionBlueprint.Item; + + if (hitObject is IHasRepeats hasRepeats) + smallestTimeGap = Math.Min(smallestTimeGap, hasRepeats.Duration / hasRepeats.SpanCount() / 2); + + smallestTimeGap = Math.Min(smallestTimeGap, lastTime - hitObject.GetEndTime()); + lastTime = hitObject.StartTime; + } + + double smallestAbsoluteGap = ((TimelineSelectionBlueprintContainer)SelectionBlueprints).ContentRelativeToAbsoluteFactor.X * smallestTimeGap; + SamplePointContracted.Value = smallestAbsoluteGap < minimum_gap; + samplePointContractedStateCache.Validate(); + } + private readonly Stack currentConcurrentObjects = new Stack(); private void updateStacking() @@ -288,6 +327,8 @@ protected partial class TimelineSelectionBlueprintContainer : Container> Content { get; } + public Vector2 ContentRelativeToAbsoluteFactor => Content.RelativeToAbsoluteFactor; + public TimelineSelectionBlueprintContainer() { AddInternal(new TimelinePart>(Content = new HitObjectOrderedSelectionContainer { RelativeSizeAxes = Axes.Both }) { RelativeSizeAxes = Axes.Both }); From 88fa6f6bc61086e9b37c00618bb5025e2c39858a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Fri, 11 Oct 2024 11:33:53 +0200 Subject: [PATCH 024/120] Optimize `PolygonGenerationPopover` --- .../Edit/PolygonGenerationPopover.cs | 47 ++++++++++++------- 1 file changed, 31 insertions(+), 16 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/PolygonGenerationPopover.cs b/osu.Game.Rulesets.Osu/Edit/PolygonGenerationPopover.cs index 6325de5851d5..c46e2650bc2f 100644 --- a/osu.Game.Rulesets.Osu/Edit/PolygonGenerationPopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PolygonGenerationPopover.cs @@ -118,10 +118,10 @@ protected override void LoadComplete() changeHandler?.BeginChange(); began = true; - distanceSnapInput.Current.BindValueChanged(_ => tryCreatePolygon()); - offsetAngleInput.Current.BindValueChanged(_ => tryCreatePolygon()); - repeatCountInput.Current.BindValueChanged(_ => tryCreatePolygon()); - pointInput.Current.BindValueChanged(_ => tryCreatePolygon()); + distanceSnapInput.Current.BindValueChanged(_ => Scheduler.AddOnce(tryCreatePolygon)); + offsetAngleInput.Current.BindValueChanged(_ => Scheduler.AddOnce(tryCreatePolygon)); + repeatCountInput.Current.BindValueChanged(_ => Scheduler.AddOnce(tryCreatePolygon)); + pointInput.Current.BindValueChanged(_ => Scheduler.AddOnce(tryCreatePolygon)); tryCreatePolygon(); } @@ -136,23 +136,34 @@ private void tryCreatePolygon() double length = distanceSnapInput.Current.Value * velocity * timeSpacing; float polygonRadius = (float)(length / (2 * Math.Sin(double.Pi / pointInput.Current.Value))); - editorBeatmap.RemoveRange(insertedCircles); - insertedCircles.Clear(); + int totalPoints = pointInput.Current.Value * repeatCountInput.Current.Value; + + if (insertedCircles.Count > totalPoints) + { + editorBeatmap.RemoveRange(insertedCircles.GetRange(totalPoints + 1, insertedCircles.Count - totalPoints - 1)); + insertedCircles.RemoveRange(totalPoints + 1, insertedCircles.Count - totalPoints - 1); + } var selectionHandler = (EditorSelectionHandler)composer.BlueprintContainer.SelectionHandler; bool first = true; - for (int i = 1; i <= pointInput.Current.Value * repeatCountInput.Current.Value; ++i) + var newlyAdded = new List(); + + for (int i = 0; i < totalPoints; ++i) { - float angle = float.DegreesToRadians(offsetAngleInput.Current.Value) + i * (2 * float.Pi / pointInput.Current.Value); + float angle = float.DegreesToRadians(offsetAngleInput.Current.Value) + (i + 1) * (2 * float.Pi / pointInput.Current.Value); var position = OsuPlayfield.BASE_SIZE / 2 + new Vector2(polygonRadius * float.Cos(angle), polygonRadius * float.Sin(angle)); - var circle = new HitCircle - { - Position = position, - StartTime = startTime, - NewCombo = first && selectionHandler.SelectionNewComboState.Value == TernaryState.True, - }; + bool alreadyAdded = i < insertedCircles.Count; + + var circle = alreadyAdded + ? insertedCircles[i] + : new HitCircle(); + + circle.Position = position; + circle.StartTime = startTime; + circle.NewCombo = first && selectionHandler.SelectionNewComboState.Value == TernaryState.True; + // TODO: probably ensure samples also follow current ternary status (not trivial) circle.Samples.Add(circle.CreateHitSampleInfo()); @@ -162,13 +173,17 @@ private void tryCreatePolygon() return; } - insertedCircles.Add(circle); + if (!alreadyAdded) + newlyAdded.Add(circle); + startTime = beatSnapProvider.SnapTime(startTime + timeSpacing); first = false; } - editorBeatmap.AddRange(insertedCircles); + insertedCircles.AddRange(newlyAdded); + editorBeatmap.AddRange(newlyAdded); + commitButton.Enabled.Value = true; } From 96769897bfbbf7551dbd5ee04fd89e98936819b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Fri, 11 Oct 2024 11:38:32 +0200 Subject: [PATCH 025/120] Extract method for scheduler call --- osu.Game.Rulesets.Osu/Edit/PolygonGenerationPopover.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/PolygonGenerationPopover.cs b/osu.Game.Rulesets.Osu/Edit/PolygonGenerationPopover.cs index c46e2650bc2f..91a8d56fc1cc 100644 --- a/osu.Game.Rulesets.Osu/Edit/PolygonGenerationPopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PolygonGenerationPopover.cs @@ -118,13 +118,15 @@ protected override void LoadComplete() changeHandler?.BeginChange(); began = true; - distanceSnapInput.Current.BindValueChanged(_ => Scheduler.AddOnce(tryCreatePolygon)); - offsetAngleInput.Current.BindValueChanged(_ => Scheduler.AddOnce(tryCreatePolygon)); - repeatCountInput.Current.BindValueChanged(_ => Scheduler.AddOnce(tryCreatePolygon)); - pointInput.Current.BindValueChanged(_ => Scheduler.AddOnce(tryCreatePolygon)); + distanceSnapInput.Current.BindValueChanged(_ => scheduleRefresh()); + offsetAngleInput.Current.BindValueChanged(_ => scheduleRefresh()); + repeatCountInput.Current.BindValueChanged(_ => scheduleRefresh()); + pointInput.Current.BindValueChanged(_ => scheduleRefresh()); tryCreatePolygon(); } + private void scheduleRefresh() => Scheduler.AddOnce(tryCreatePolygon); + private void tryCreatePolygon() { double startTime = beatSnapProvider.SnapTime(editorClock.CurrentTime); From 5c74fceff6ca7e8ea49f14bd2319a56fa8016134 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Fri, 11 Oct 2024 11:46:57 +0200 Subject: [PATCH 026/120] Remove objects when polygon is invalid --- osu.Game.Rulesets.Osu/Edit/PolygonGenerationPopover.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Edit/PolygonGenerationPopover.cs b/osu.Game.Rulesets.Osu/Edit/PolygonGenerationPopover.cs index 91a8d56fc1cc..fb4640db8252 100644 --- a/osu.Game.Rulesets.Osu/Edit/PolygonGenerationPopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PolygonGenerationPopover.cs @@ -172,6 +172,8 @@ private void tryCreatePolygon() if (position.X < 0 || position.Y < 0 || position.X > OsuPlayfield.BASE_SIZE.X || position.Y > OsuPlayfield.BASE_SIZE.Y) { commitButton.Enabled.Value = false; + editorBeatmap.RemoveRange(insertedCircles); + insertedCircles.Clear(); return; } From 7e439be9eca9e626a080defc0cd24da933072710 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Fri, 11 Oct 2024 11:47:23 +0200 Subject: [PATCH 027/120] Fix crash from non-serializable hitobject samples --- osu.Game.Rulesets.Osu/Edit/PolygonGenerationPopover.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/PolygonGenerationPopover.cs b/osu.Game.Rulesets.Osu/Edit/PolygonGenerationPopover.cs index fb4640db8252..9605393f7048 100644 --- a/osu.Game.Rulesets.Osu/Edit/PolygonGenerationPopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PolygonGenerationPopover.cs @@ -166,9 +166,6 @@ private void tryCreatePolygon() circle.StartTime = startTime; circle.NewCombo = first && selectionHandler.SelectionNewComboState.Value == TernaryState.True; - // TODO: probably ensure samples also follow current ternary status (not trivial) - circle.Samples.Add(circle.CreateHitSampleInfo()); - if (position.X < 0 || position.Y < 0 || position.X > OsuPlayfield.BASE_SIZE.X || position.Y > OsuPlayfield.BASE_SIZE.Y) { commitButton.Enabled.Value = false; @@ -178,8 +175,13 @@ private void tryCreatePolygon() } if (!alreadyAdded) + { newlyAdded.Add(circle); + // TODO: probably ensure samples also follow current ternary status (not trivial) + circle.Samples.Add(circle.CreateHitSampleInfo()); + } + startTime = beatSnapProvider.SnapTime(startTime + timeSpacing); first = false; From 8e781c170d034d4395ae6b045f24b1887837bd4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Fri, 11 Oct 2024 19:24:46 +0200 Subject: [PATCH 028/120] Inline scheduler calls --- osu.Game.Rulesets.Osu/Edit/PolygonGenerationPopover.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/PolygonGenerationPopover.cs b/osu.Game.Rulesets.Osu/Edit/PolygonGenerationPopover.cs index 9605393f7048..a84494fb76c6 100644 --- a/osu.Game.Rulesets.Osu/Edit/PolygonGenerationPopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PolygonGenerationPopover.cs @@ -118,15 +118,13 @@ protected override void LoadComplete() changeHandler?.BeginChange(); began = true; - distanceSnapInput.Current.BindValueChanged(_ => scheduleRefresh()); - offsetAngleInput.Current.BindValueChanged(_ => scheduleRefresh()); - repeatCountInput.Current.BindValueChanged(_ => scheduleRefresh()); - pointInput.Current.BindValueChanged(_ => scheduleRefresh()); + distanceSnapInput.Current.BindValueChanged(_ => Scheduler.AddOnce(tryCreatePolygon)); + offsetAngleInput.Current.BindValueChanged(_ => Scheduler.AddOnce(tryCreatePolygon)); + repeatCountInput.Current.BindValueChanged(_ => Scheduler.AddOnce(tryCreatePolygon)); + pointInput.Current.BindValueChanged(_ => Scheduler.AddOnce(tryCreatePolygon)); tryCreatePolygon(); } - private void scheduleRefresh() => Scheduler.AddOnce(tryCreatePolygon); - private void tryCreatePolygon() { double startTime = beatSnapProvider.SnapTime(editorClock.CurrentTime); From 9e97141e33f3f94064790f2db2d668d55243d287 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Fri, 11 Oct 2024 22:03:21 +0200 Subject: [PATCH 029/120] Fix off-by-one error --- osu.Game.Rulesets.Osu/Edit/PolygonGenerationPopover.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/PolygonGenerationPopover.cs b/osu.Game.Rulesets.Osu/Edit/PolygonGenerationPopover.cs index a84494fb76c6..b244e07adfd8 100644 --- a/osu.Game.Rulesets.Osu/Edit/PolygonGenerationPopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PolygonGenerationPopover.cs @@ -140,8 +140,8 @@ private void tryCreatePolygon() if (insertedCircles.Count > totalPoints) { - editorBeatmap.RemoveRange(insertedCircles.GetRange(totalPoints + 1, insertedCircles.Count - totalPoints - 1)); - insertedCircles.RemoveRange(totalPoints + 1, insertedCircles.Count - totalPoints - 1); + editorBeatmap.RemoveRange(insertedCircles.GetRange(totalPoints, insertedCircles.Count - totalPoints)); + insertedCircles.RemoveRange(totalPoints, insertedCircles.Count - totalPoints); } var selectionHandler = (EditorSelectionHandler)composer.BlueprintContainer.SelectionHandler; From f76a2d02f61e522ae69eef5f1c3e8af0836bb42d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Fri, 11 Oct 2024 23:22:15 +0200 Subject: [PATCH 030/120] Make sure to properly update hitobjects --- .../Edit/PolygonGenerationPopover.cs | 13 +++++++++++-- .../Compose/Components/EditorSelectionHandler.cs | 3 ++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/PolygonGenerationPopover.cs b/osu.Game.Rulesets.Osu/Edit/PolygonGenerationPopover.cs index b244e07adfd8..335b0f3ea6b6 100644 --- a/osu.Game.Rulesets.Osu/Edit/PolygonGenerationPopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PolygonGenerationPopover.cs @@ -50,9 +50,14 @@ public partial class PolygonGenerationPopover : OsuPopover [Resolved] private HitObjectComposer composer { get; set; } = null!; + private Bindable newComboState = null!; + [BackgroundDependencyLoader] private void load() { + var selectionHandler = (EditorSelectionHandler)composer.BlueprintContainer.SelectionHandler; + newComboState = selectionHandler.SelectionNewComboState.GetBoundCopy(); + Child = new FillFlowContainer { Width = 220, @@ -122,6 +127,7 @@ protected override void LoadComplete() offsetAngleInput.Current.BindValueChanged(_ => Scheduler.AddOnce(tryCreatePolygon)); repeatCountInput.Current.BindValueChanged(_ => Scheduler.AddOnce(tryCreatePolygon)); pointInput.Current.BindValueChanged(_ => Scheduler.AddOnce(tryCreatePolygon)); + newComboState.BindValueChanged(_ => Scheduler.AddOnce(tryCreatePolygon)); tryCreatePolygon(); } @@ -144,7 +150,6 @@ private void tryCreatePolygon() insertedCircles.RemoveRange(totalPoints, insertedCircles.Count - totalPoints); } - var selectionHandler = (EditorSelectionHandler)composer.BlueprintContainer.SelectionHandler; bool first = true; var newlyAdded = new List(); @@ -162,7 +167,7 @@ private void tryCreatePolygon() circle.Position = position; circle.StartTime = startTime; - circle.NewCombo = first && selectionHandler.SelectionNewComboState.Value == TernaryState.True; + circle.NewCombo = first && newComboState.Value == TernaryState.True; if (position.X < 0 || position.Y < 0 || position.X > OsuPlayfield.BASE_SIZE.X || position.Y > OsuPlayfield.BASE_SIZE.Y) { @@ -179,6 +184,10 @@ private void tryCreatePolygon() // TODO: probably ensure samples also follow current ternary status (not trivial) circle.Samples.Add(circle.CreateHitSampleInfo()); } + else + { + editorBeatmap.Update(circle); + } startTime = beatSnapProvider.SnapTime(startTime + timeSpacing); diff --git a/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs index dbbf767a7d73..dfda93cedd48 100644 --- a/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs @@ -176,7 +176,8 @@ private void createStateBindables() /// protected virtual void UpdateTernaryStates() { - SelectionNewComboState.Value = GetStateFromSelection(SelectedItems.OfType(), h => h.NewCombo); + if (SelectedItems.Any()) + SelectionNewComboState.Value = GetStateFromSelection(SelectedItems.OfType(), h => h.NewCombo); var samplesInSelection = SelectedItems.SelectMany(enumerateAllSamples).ToArray(); From 767ded43267f23b6e2a8a0b9b5221106605112ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Fri, 11 Oct 2024 23:41:57 +0200 Subject: [PATCH 031/120] Restore previous NewCombo state when adding objects --- .../Edit/PolygonGenerationPopover.cs | 50 +++++++++++-------- 1 file changed, 29 insertions(+), 21 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/PolygonGenerationPopover.cs b/osu.Game.Rulesets.Osu/Edit/PolygonGenerationPopover.cs index 335b0f3ea6b6..00abab0bae2b 100644 --- a/osu.Game.Rulesets.Osu/Edit/PolygonGenerationPopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PolygonGenerationPopover.cs @@ -150,53 +150,61 @@ private void tryCreatePolygon() insertedCircles.RemoveRange(totalPoints, insertedCircles.Count - totalPoints); } - bool first = true; - var newlyAdded = new List(); for (int i = 0; i < totalPoints; ++i) { float angle = float.DegreesToRadians(offsetAngleInput.Current.Value) + (i + 1) * (2 * float.Pi / pointInput.Current.Value); var position = OsuPlayfield.BASE_SIZE / 2 + new Vector2(polygonRadius * float.Cos(angle), polygonRadius * float.Sin(angle)); + bool newCombo = i == 0 && newComboState.Value == TernaryState.True; - bool alreadyAdded = i < insertedCircles.Count; + HitCircle circle; - var circle = alreadyAdded - ? insertedCircles[i] - : new HitCircle(); + if (i < insertedCircles.Count) + { + circle = insertedCircles[i]; - circle.Position = position; - circle.StartTime = startTime; - circle.NewCombo = first && newComboState.Value == TernaryState.True; + circle.Position = position; + circle.StartTime = startTime; + circle.NewCombo = newCombo; - if (position.X < 0 || position.Y < 0 || position.X > OsuPlayfield.BASE_SIZE.X || position.Y > OsuPlayfield.BASE_SIZE.Y) - { - commitButton.Enabled.Value = false; - editorBeatmap.RemoveRange(insertedCircles); - insertedCircles.Clear(); - return; + editorBeatmap.Update(circle); } - - if (!alreadyAdded) + else { + circle = new HitCircle + { + Position = position, + StartTime = startTime, + NewCombo = newCombo, + }; + newlyAdded.Add(circle); // TODO: probably ensure samples also follow current ternary status (not trivial) circle.Samples.Add(circle.CreateHitSampleInfo()); } - else + + if (position.X < 0 || position.Y < 0 || position.X > OsuPlayfield.BASE_SIZE.X || position.Y > OsuPlayfield.BASE_SIZE.Y) { - editorBeatmap.Update(circle); + commitButton.Enabled.Value = false; + editorBeatmap.RemoveRange(insertedCircles); + insertedCircles.Clear(); + return; } startTime = beatSnapProvider.SnapTime(startTime + timeSpacing); - - first = false; } + var previousNewComboState = newComboState.Value; + insertedCircles.AddRange(newlyAdded); editorBeatmap.AddRange(newlyAdded); + // When adding new hitObjects, newCombo state will get reset to false when no objects are selected. + // Since this is the case when this popover is showing, we need to restore the previous newCombo state + newComboState.Value = previousNewComboState; + commitButton.Enabled.Value = true; } From 40339b8a1713aaf1b980550b602a932c24f42ba3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Fri, 11 Oct 2024 23:49:52 +0200 Subject: [PATCH 032/120] Fix typo --- osu.Game.Rulesets.Osu/Edit/PolygonGenerationPopover.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/PolygonGenerationPopover.cs b/osu.Game.Rulesets.Osu/Edit/PolygonGenerationPopover.cs index 00abab0bae2b..2f073c2fc418 100644 --- a/osu.Game.Rulesets.Osu/Edit/PolygonGenerationPopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PolygonGenerationPopover.cs @@ -201,7 +201,7 @@ private void tryCreatePolygon() insertedCircles.AddRange(newlyAdded); editorBeatmap.AddRange(newlyAdded); - // When adding new hitObjects, newCombo state will get reset to false when no objects are selected. + // When adding new hitObjects, newCombo state will get reset to false when no objects are selected. // Since this is the case when this popover is showing, we need to restore the previous newCombo state newComboState.Value = previousNewComboState; From 14c6cfc3651173ff95eabef33f241a0a35e983ff Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sun, 13 Oct 2024 11:03:09 -0400 Subject: [PATCH 033/120] Extend test cases to cover direct interaction (i.e. no touch-compose-area-first) --- .../Editor/TestSceneOsuEditor.cs | 83 ++++++++++++++++--- 1 file changed, 73 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditor.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditor.cs index befaf5802925..6ad4330921c8 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditor.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditor.cs @@ -7,6 +7,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Input; using osu.Framework.Testing; +using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.UI; using osu.Game.Screens.Edit.Components.RadioButtons; @@ -23,38 +24,93 @@ public partial class TestSceneOsuEditor : EditorTestScene protected override Ruleset CreateEditorRuleset() => new OsuRuleset(); [Test] - public void TestTouchInputAfterTouchingComposeArea() + public void TestTouchInputPlaceHitCircleDirectly() + { + AddStep("tap circle", () => tap(this.ChildrenOfType().Single(b => b.Button.Label == "HitCircle"))); + + AddStep("tap to place circle", () => tap(this.ChildrenOfType().Single())); + AddAssert("circle placed correctly", () => + { + var circle = (HitCircle)EditorBeatmap.HitObjects.Single(h => h.StartTime == EditorClock.CurrentTimeAccurate); + Assert.Multiple(() => + { + Assert.That(circle.Position.X, Is.EqualTo(256f).Within(0.01f)); + Assert.That(circle.Position.Y, Is.EqualTo(192f).Within(0.01f)); + }); + + return true; + }); + } + + [Test] + public void TestTouchInputPlaceCircleAfterTouchingComposeArea() { AddStep("tap circle", () => tap(this.ChildrenOfType().Single(b => b.Button.Label == "HitCircle"))); - // this input is just for interacting with compose area AddStep("tap playfield", () => tap(this.ChildrenOfType().Single())); + AddAssert("circle placed", () => EditorBeatmap.HitObjects.Single(h => h.StartTime == EditorClock.CurrentTimeAccurate) is HitCircle); - AddStep("move current time", () => InputManager.Key(Key.Right)); + AddStep("move forward", () => InputManager.Key(Key.Right)); - AddStep("tap to place circle", () => tap(this.ChildrenOfType().Single().ToScreenSpace(new Vector2(10, 10)))); + AddStep("tap to place circle", () => tap(this.ChildrenOfType().Single())); AddAssert("circle placed correctly", () => { var circle = (HitCircle)EditorBeatmap.HitObjects.Single(h => h.StartTime == EditorClock.CurrentTimeAccurate); Assert.Multiple(() => { - Assert.That(circle.Position.X, Is.EqualTo(10f).Within(0.01f)); - Assert.That(circle.Position.Y, Is.EqualTo(10f).Within(0.01f)); + Assert.That(circle.Position.X, Is.EqualTo(256f).Within(0.01f)); + Assert.That(circle.Position.Y, Is.EqualTo(192f).Within(0.01f)); + }); + + return true; + }); + } + + [Test] + public void TestTouchInputPlaceSliderDirectly() + { + AddStep("tap slider", () => tap(this.ChildrenOfType().Single(b => b.Button.Label == "Slider"))); + + AddStep("hold to draw slider", () => InputManager.BeginTouch(new Touch(TouchSource.Touch1, this.ChildrenOfType().Single().ToScreenSpace(new Vector2(50, 20))))); + AddStep("drag to draw", () => InputManager.MoveTouchTo(new Touch(TouchSource.Touch1, this.ChildrenOfType().Single().ToScreenSpace(new Vector2(200, 50))))); + AddAssert("selection not initiated", () => this.ChildrenOfType().All(d => d.State == Visibility.Hidden)); + AddAssert("blueprint visible", () => this.ChildrenOfType().Single().Alpha > 0); + AddStep("end", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, InputManager.CurrentState.Touch.GetTouchPosition(TouchSource.Touch1)!.Value))); + AddAssert("slider placed correctly", () => + { + var slider = (Slider)EditorBeatmap.HitObjects.Single(h => h.StartTime == EditorClock.CurrentTimeAccurate); + Assert.Multiple(() => + { + Assert.That(slider.Position.X, Is.EqualTo(50f).Within(0.01f)); + Assert.That(slider.Position.Y, Is.EqualTo(20f).Within(0.01f)); + Assert.That(slider.Path.ControlPoints.Count, Is.EqualTo(2)); + Assert.That(slider.Path.ControlPoints[0].Position, Is.EqualTo(Vector2.Zero)); + + // the final position may be slightly off from the mouse position when drawing, account for that. + Assert.That(slider.Path.ControlPoints[1].Position.X, Is.EqualTo(150).Within(5)); + Assert.That(slider.Path.ControlPoints[1].Position.Y, Is.EqualTo(30).Within(5)); }); return true; }); + } + [Test] + public void TestTouchInputPlaceSliderAfterTouchingComposeArea() + { AddStep("tap slider", () => tap(this.ChildrenOfType().Single(b => b.Button.Label == "Slider"))); - // this input is just for interacting with compose area AddStep("tap playfield", () => tap(this.ChildrenOfType().Single())); + AddStep("tap and hold another spot", () => hold(this.ChildrenOfType().Single(), new Vector2(50, 0))); + AddUntilStep("wait for slider placement", () => EditorBeatmap.HitObjects.SingleOrDefault(h => h.StartTime == EditorClock.CurrentTimeAccurate) is Slider); + AddStep("end", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, InputManager.CurrentState.Touch.GetTouchPosition(TouchSource.Touch1)!.Value))); - AddStep("move current time", () => InputManager.Key(Key.Right)); + AddStep("move forward", () => InputManager.Key(Key.Right)); AddStep("hold to draw slider", () => InputManager.BeginTouch(new Touch(TouchSource.Touch1, this.ChildrenOfType().Single().ToScreenSpace(new Vector2(50, 20))))); AddStep("drag to draw", () => InputManager.MoveTouchTo(new Touch(TouchSource.Touch1, this.ChildrenOfType().Single().ToScreenSpace(new Vector2(200, 50))))); AddAssert("selection not initiated", () => this.ChildrenOfType().All(d => d.State == Visibility.Hidden)); + AddAssert("blueprint visible", () => this.ChildrenOfType().Single().IsPresent); AddStep("end", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, InputManager.CurrentState.Touch.GetTouchPosition(TouchSource.Touch1)!.Value))); AddAssert("slider placed correctly", () => { @@ -75,12 +131,19 @@ public void TestTouchInputAfterTouchingComposeArea() }); } - private void tap(Drawable drawable) => tap(drawable.ScreenSpaceDrawQuad.Centre); + private void tap(Drawable drawable, Vector2 offset = default) => tap(drawable.ToScreenSpace(drawable.LayoutRectangle.Centre + offset)); private void tap(Vector2 position) { - InputManager.BeginTouch(new Touch(TouchSource.Touch1, position)); + hold(position); InputManager.EndTouch(new Touch(TouchSource.Touch1, position)); } + + private void hold(Drawable drawable, Vector2 offset = default) => hold(drawable.ToScreenSpace(drawable.LayoutRectangle.Centre + offset)); + + private void hold(Vector2 position) + { + InputManager.BeginTouch(new Touch(TouchSource.Touch1, position)); + } } } From 0fa1d22210899b2571315911e5827b1a19337002 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sun, 13 Oct 2024 11:04:22 -0400 Subject: [PATCH 034/120] Add tests covering expected behaviour when going on/off compose area --- .../Editing/TestSceneHitObjectComposer.cs | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs index f392841ac739..d7c92a64b13a 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs @@ -19,6 +19,7 @@ using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Edit; using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.UI; using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Components.RadioButtons; using osu.Game.Screens.Edit.Components.TernaryButtons; @@ -82,6 +83,45 @@ public void SetUpSteps() }); } + [Test] + public void TestPlacementOutsideComposeScreen() + { + AddStep("clear all control points and hitobjects", () => + { + editorBeatmap.ControlPointInfo.Clear(); + editorBeatmap.Clear(); + }); + + AddStep("Add timing point", () => editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint())); + AddStep("select circle", () => hitObjectComposer.ChildrenOfType().First(d => d.Button.Label == "HitCircle").TriggerClick()); + AddStep("move mouse to compose", () => InputManager.MoveMouseTo(hitObjectComposer.ChildrenOfType().Single())); + AddStep("click", () => InputManager.Click(MouseButton.Left)); + AddAssert("circle placed", () => editorBeatmap.HitObjects.Count == 1); + + AddStep("move mouse outside compose", () => InputManager.MoveMouseTo(hitObjectComposer.ChildrenOfType().Single().ScreenSpaceDrawQuad.TopLeft - new Vector2(0f, 20f))); + AddStep("click", () => InputManager.Click(MouseButton.Left)); + AddAssert("no circle placed", () => editorBeatmap.HitObjects.Count == 1); + } + + [Test] + public void TestDragSliderOutsideComposeScreen() + { + AddStep("clear all control points and hitobjects", () => + { + editorBeatmap.ControlPointInfo.Clear(); + editorBeatmap.Clear(); + }); + + AddStep("Add timing point", () => editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint())); + AddStep("select slider", () => hitObjectComposer.ChildrenOfType().First(d => d.Button.Label == "Slider").TriggerClick()); + + AddStep("move mouse to compose", () => InputManager.MoveMouseTo(hitObjectComposer.ChildrenOfType().Single())); + AddStep("hold", () => InputManager.PressButton(MouseButton.Left)); + AddStep("move mouse outside compose", () => InputManager.MoveMouseTo(hitObjectComposer.ChildrenOfType().Single().ScreenSpaceDrawQuad.TopLeft - new Vector2(0f, 80f))); + AddStep("release", () => InputManager.ReleaseButton(MouseButton.Left)); + AddAssert("slider placed", () => editorBeatmap.HitObjects.Count == 1); + } + [Test] public void TestPlacementOnlyWorksWithTiming() { From d195a694479d8086a81bed39a33776138b104084 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 17 Oct 2024 19:26:19 +0900 Subject: [PATCH 035/120] Compute maximum performance along with difficulty --- osu.Game/Beatmaps/BeatmapDifficultyCache.cs | 109 +++++++++++++++- osu.Game/Beatmaps/StarDifficulty.cs | 25 ++-- .../PerformanceBreakdownCalculator.cs | 121 ------------------ .../Statistics/PerformanceStatistic.cs | 4 +- .../Statistics/PerformanceBreakdownChart.cs | 34 ++++- 5 files changed, 151 insertions(+), 142 deletions(-) delete mode 100644 osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs diff --git a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs index 871faf5906f1..c4506c9f7cdf 100644 --- a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs +++ b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs @@ -4,12 +4,15 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using osu.Framework.Allocation; +using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Extensions; +using osu.Framework.Graphics.Textures; using osu.Framework.Lists; using osu.Framework.Logging; using osu.Framework.Threading; @@ -18,7 +21,12 @@ using osu.Game.Rulesets; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; +using osu.Game.Scoring; +using osu.Game.Skinning; +using osu.Game.Storyboards; namespace osu.Game.Beatmaps { @@ -237,10 +245,13 @@ private void updateBindable(BindableStarDifficulty bindable, IRulesetInfo? rules var ruleset = rulesetInfo.CreateInstance(); Debug.Assert(ruleset != null); - var calculator = ruleset.CreateDifficultyCalculator(beatmapManager.GetWorkingBeatmap(key.BeatmapInfo)); - var attributes = calculator.Calculate(key.OrderedMods, cancellationToken); + PlayableCachedWorkingBeatmap working = new PlayableCachedWorkingBeatmap(beatmapManager.GetWorkingBeatmap(key.BeatmapInfo)); - return new StarDifficulty(attributes); + var difficulty = ruleset.CreateDifficultyCalculator(working).Calculate(key.OrderedMods, cancellationToken); + cancellationToken.ThrowIfCancellationRequested(); + var performance = computeMaxPerformance(working, key.BeatmapInfo, ruleset, key.OrderedMods, difficulty); + + return new StarDifficulty(difficulty, performance); } catch (OperationCanceledException) { @@ -262,6 +273,60 @@ private void updateBindable(BindableStarDifficulty bindable, IRulesetInfo? rules } } + private static PerformanceAttributes computeMaxPerformance(IWorkingBeatmap working, BeatmapInfo beatmap, Ruleset ruleset, Mod[] mods, DifficultyAttributes attributes) + { + var performanceCalculator = ruleset.CreatePerformanceCalculator(); + if (performanceCalculator == null) + return new PerformanceAttributes(); + + IBeatmap playableBeatmap = working.GetPlayableBeatmap(ruleset.RulesetInfo, mods); + + // create statistics assuming all hit objects have perfect hit result + var statistics = playableBeatmap.HitObjects + .SelectMany(getPerfectHitResults) + .GroupBy(hr => hr, (hr, list) => (hitResult: hr, count: list.Count())) + .ToDictionary(pair => pair.hitResult, pair => pair.count); + + // compute maximum total score + ScoreProcessor scoreProcessor = ruleset.CreateScoreProcessor(); + scoreProcessor.Mods.Value = mods; + scoreProcessor.ApplyBeatmap(playableBeatmap); + long maxScore = scoreProcessor.MaximumTotalScore; + + // todo: Get max combo from difficulty calculator instead when diffcalc properly supports lazer-first scores + int maxCombo = calculateMaxCombo(playableBeatmap); + + // compute maximum rank - default to SS, then adjust the rank with mods + ScoreRank maxRank = ScoreRank.X; + foreach (IApplicableToScoreProcessor mod in mods.OfType()) + maxRank = mod.AdjustRank(maxRank, 1); + + ScoreInfo perfectScore = new ScoreInfo(beatmap, ruleset.RulesetInfo) + { + Accuracy = 1, + Passed = true, + MaxCombo = maxCombo, + Combo = maxCombo, + Mods = mods, + TotalScore = maxScore, + Statistics = statistics, + MaximumStatistics = statistics + }; + + return performanceCalculator.Calculate(perfectScore, attributes); + + static int calculateMaxCombo(IBeatmap beatmap) + => beatmap.HitObjects.SelectMany(getPerfectHitResults).Count(r => r.AffectsCombo()); + + static IEnumerable getPerfectHitResults(HitObject hitObject) + { + foreach (HitObject nested in hitObject.NestedHitObjects) + yield return nested.Judgement.MaxResult; + + yield return hitObject.Judgement.MaxResult; + } + } + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); @@ -276,7 +341,6 @@ protected override void Dispose(bool isDisposing) { public readonly BeatmapInfo BeatmapInfo; public readonly RulesetInfo Ruleset; - public readonly Mod[] OrderedMods; public DifficultyCacheLookup(BeatmapInfo beatmapInfo, RulesetInfo? ruleset, IEnumerable? mods) @@ -317,5 +381,42 @@ public BindableStarDifficulty(IBeatmapInfo beatmapInfo, CancellationToken cancel CancellationToken = cancellationToken; } } + + /// + /// A working beatmap that caches its playable representation. + /// This is intended as single-use for when it is guaranteed that the playable beatmap can be reused. + /// + private class PlayableCachedWorkingBeatmap : IWorkingBeatmap + { + private readonly IWorkingBeatmap working; + private IBeatmap? playable; + + public PlayableCachedWorkingBeatmap(IWorkingBeatmap working) + { + this.working = working; + } + + public IBeatmap GetPlayableBeatmap(IRulesetInfo ruleset, IReadOnlyList mods) + => playable ??= working.GetPlayableBeatmap(ruleset, mods); + + public IBeatmap GetPlayableBeatmap(IRulesetInfo ruleset, IReadOnlyList mods, CancellationToken cancellationToken) + => playable ??= working.GetPlayableBeatmap(ruleset, mods, cancellationToken); + + IBeatmapInfo IWorkingBeatmap.BeatmapInfo => working.BeatmapInfo; + bool IWorkingBeatmap.BeatmapLoaded => working.BeatmapLoaded; + bool IWorkingBeatmap.TrackLoaded => working.TrackLoaded; + IBeatmap IWorkingBeatmap.Beatmap => working.Beatmap; + Texture IWorkingBeatmap.GetBackground() => working.GetBackground(); + Texture IWorkingBeatmap.GetPanelBackground() => working.GetPanelBackground(); + Waveform IWorkingBeatmap.Waveform => working.Waveform; + Storyboard IWorkingBeatmap.Storyboard => working.Storyboard; + ISkin IWorkingBeatmap.Skin => working.Skin; + Track IWorkingBeatmap.Track => working.Track; + Track IWorkingBeatmap.LoadTrack() => working.LoadTrack(); + Stream IWorkingBeatmap.GetStream(string storagePath) => working.GetStream(storagePath); + void IWorkingBeatmap.BeginAsyncLoad() => working.BeginAsyncLoad(); + void IWorkingBeatmap.CancelAsyncLoad() => working.CancelAsyncLoad(); + void IWorkingBeatmap.PrepareTrackForPreview(bool looping, double offsetFromPreviewPoint) => working.PrepareTrackForPreview(looping, offsetFromPreviewPoint); + } } } diff --git a/osu.Game/Beatmaps/StarDifficulty.cs b/osu.Game/Beatmaps/StarDifficulty.cs index 6aac275a6a13..51358fcd7f11 100644 --- a/osu.Game/Beatmaps/StarDifficulty.cs +++ b/osu.Game/Beatmaps/StarDifficulty.cs @@ -26,29 +26,36 @@ public readonly struct StarDifficulty /// Might not be available if the star difficulty is associated with a beatmap that's not locally available. /// [CanBeNull] - public readonly DifficultyAttributes Attributes; + public readonly DifficultyAttributes DifficultyAttributes; /// - /// Creates a structure based on computed - /// by a . + /// The performance attributes computed for a perfect score on the given beatmap. + /// Might not be available if the star difficulty is associated with a beatmap that's not locally available. + /// + [CanBeNull] + public readonly PerformanceAttributes PerformanceAttributes; + + /// + /// Creates a structure. /// - public StarDifficulty([NotNull] DifficultyAttributes attributes) + public StarDifficulty([NotNull] DifficultyAttributes difficulty, [NotNull] PerformanceAttributes performance) { - Stars = double.IsFinite(attributes.StarRating) ? attributes.StarRating : 0; - MaxCombo = attributes.MaxCombo; - Attributes = attributes; + Stars = double.IsFinite(difficulty.StarRating) ? difficulty.StarRating : 0; + MaxCombo = difficulty.MaxCombo; + DifficultyAttributes = difficulty; + PerformanceAttributes = performance; // Todo: Add more members (BeatmapInfo.DifficultyRating? Attributes? Etc...) } /// /// Creates a structure with a pre-populated star difficulty and max combo - /// in scenarios where computing is not feasible (i.e. when working with online sources). + /// in scenarios where computing is not feasible (i.e. when working with online sources). /// public StarDifficulty(double starDifficulty, int maxCombo) { Stars = double.IsFinite(starDifficulty) ? starDifficulty : 0; MaxCombo = maxCombo; - Attributes = null; + DifficultyAttributes = null; } public DifficultyRating DifficultyRating => GetDifficultyRating(Stars); diff --git a/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs b/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs deleted file mode 100644 index 946d83b14b41..000000000000 --- a/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs +++ /dev/null @@ -1,121 +0,0 @@ -// 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.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using JetBrains.Annotations; -using osu.Framework.Extensions.ObjectExtensions; -using osu.Game.Beatmaps; -using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Objects; -using osu.Game.Rulesets.Scoring; -using osu.Game.Scoring; - -namespace osu.Game.Rulesets.Difficulty -{ - public class PerformanceBreakdownCalculator - { - private readonly IBeatmap playableBeatmap; - private readonly BeatmapDifficultyCache difficultyCache; - - public PerformanceBreakdownCalculator(IBeatmap playableBeatmap, BeatmapDifficultyCache difficultyCache) - { - this.playableBeatmap = playableBeatmap; - this.difficultyCache = difficultyCache; - } - - [ItemCanBeNull] - public async Task CalculateAsync(ScoreInfo score, CancellationToken cancellationToken = default) - { - var attributes = await difficultyCache.GetDifficultyAsync(score.BeatmapInfo!, score.Ruleset, score.Mods, cancellationToken).ConfigureAwait(false); - - var performanceCalculator = score.Ruleset.CreateInstance().CreatePerformanceCalculator(); - - // Performance calculation requires the beatmap and ruleset to be locally available. If not, return a default value. - if (attributes?.Attributes == null || performanceCalculator == null) - return null; - - cancellationToken.ThrowIfCancellationRequested(); - - PerformanceAttributes[] performanceArray = await Task.WhenAll( - // compute actual performance - performanceCalculator.CalculateAsync(score, attributes.Value.Attributes, cancellationToken), - // compute performance for perfect play - getPerfectPerformance(score, cancellationToken) - ).ConfigureAwait(false); - - return new PerformanceBreakdown(performanceArray[0] ?? new PerformanceAttributes(), performanceArray[1] ?? new PerformanceAttributes()); - } - - [ItemCanBeNull] - private Task getPerfectPerformance(ScoreInfo score, CancellationToken cancellationToken = default) - { - return Task.Run(async () => - { - Ruleset ruleset = score.Ruleset.CreateInstance(); - ScoreInfo perfectPlay = score.DeepClone(); - perfectPlay.Accuracy = 1; - perfectPlay.Passed = true; - - // calculate max combo - // todo: Get max combo from difficulty calculator instead when diffcalc properly supports lazer-first scores - perfectPlay.MaxCombo = calculateMaxCombo(playableBeatmap); - - // create statistics assuming all hit objects have perfect hit result - var statistics = playableBeatmap.HitObjects - .SelectMany(getPerfectHitResults) - .GroupBy(hr => hr, (hr, list) => (hitResult: hr, count: list.Count())) - .ToDictionary(pair => pair.hitResult, pair => pair.count); - perfectPlay.Statistics = statistics; - perfectPlay.MaximumStatistics = statistics; - - // calculate total score - ScoreProcessor scoreProcessor = ruleset.CreateScoreProcessor(); - scoreProcessor.Mods.Value = perfectPlay.Mods; - scoreProcessor.ApplyBeatmap(playableBeatmap); - perfectPlay.TotalScore = scoreProcessor.MaximumTotalScore; - - // compute rank achieved - // default to SS, then adjust the rank with mods - perfectPlay.Rank = ScoreRank.X; - - foreach (IApplicableToScoreProcessor mod in perfectPlay.Mods.OfType()) - { - perfectPlay.Rank = mod.AdjustRank(perfectPlay.Rank, 1); - } - - // calculate performance for this perfect score - var difficulty = await difficultyCache.GetDifficultyAsync( - playableBeatmap.BeatmapInfo, - score.Ruleset, - score.Mods, - cancellationToken - ).ConfigureAwait(false); - - var performanceCalculator = ruleset.CreatePerformanceCalculator(); - - if (performanceCalculator == null || difficulty == null) - return null; - - return await performanceCalculator.CalculateAsync(perfectPlay, difficulty.Value.Attributes.AsNonNull(), cancellationToken).ConfigureAwait(false); - }, cancellationToken); - } - - private int calculateMaxCombo(IBeatmap beatmap) - { - return beatmap.HitObjects.SelectMany(getPerfectHitResults).Count(r => r.AffectsCombo()); - } - - private IEnumerable getPerfectHitResults(HitObject hitObject) - { - foreach (HitObject nested in hitObject.NestedHitObjects) - yield return nested.Judgement.MaxResult; - - yield return hitObject.Judgement.MaxResult; - } - } -} diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs index 7ea3cbe91714..7d155e32b06b 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs @@ -53,10 +53,10 @@ private void load(BeatmapDifficultyCache difficultyCache, CancellationToken? can var performanceCalculator = score.Ruleset.CreateInstance().CreatePerformanceCalculator(); // Performance calculation requires the beatmap and ruleset to be locally available. If not, return a default value. - if (attributes?.Attributes == null || performanceCalculator == null) + if (attributes?.DifficultyAttributes == null || performanceCalculator == null) return; - var result = await performanceCalculator.CalculateAsync(score, attributes.Value.Attributes, cancellationToken ?? default).ConfigureAwait(false); + var result = await performanceCalculator.CalculateAsync(score, attributes.Value.DifficultyAttributes, cancellationToken ?? default).ConfigureAwait(false); Schedule(() => setPerformanceValue(score, result.Total)); }, cancellationToken ?? default); diff --git a/osu.Game/Screens/Ranking/Statistics/PerformanceBreakdownChart.cs b/osu.Game/Screens/Ranking/Statistics/PerformanceBreakdownChart.cs index b5eed2d12a41..f9c8c93deca2 100644 --- a/osu.Game/Screens/Ranking/Statistics/PerformanceBreakdownChart.cs +++ b/osu.Game/Screens/Ranking/Statistics/PerformanceBreakdownChart.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading; +using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Extensions; using osu.Framework.Extensions.Color4Extensions; @@ -26,7 +27,6 @@ namespace osu.Game.Screens.Ranking.Statistics public partial class PerformanceBreakdownChart : Container { private readonly ScoreInfo score; - private readonly IBeatmap playableBeatmap; private Drawable spinner = null!; private Drawable content = null!; @@ -42,7 +42,6 @@ public partial class PerformanceBreakdownChart : Container public PerformanceBreakdownChart(ScoreInfo score, IBeatmap playableBeatmap) { this.score = score; - this.playableBeatmap = playableBeatmap; } [BackgroundDependencyLoader] @@ -142,12 +141,33 @@ private void load() spinner.Show(); - new PerformanceBreakdownCalculator(playableBeatmap, difficultyCache) - .CalculateAsync(score, cancellationTokenSource.Token) - .ContinueWith(t => Schedule(() => setPerformanceValue(t.GetResultSafely()!))); + computePerformance(cancellationTokenSource.Token) + .ContinueWith(t => Schedule(() => + { + if (t.GetResultSafely() is PerformanceBreakdown breakdown) + setPerformance(breakdown); + }), TaskContinuationOptions.OnlyOnRanToCompletion); } - private void setPerformanceValue(PerformanceBreakdown breakdown) + private async Task computePerformance(CancellationToken token) + { + var performanceCalculator = score.Ruleset.CreateInstance().CreatePerformanceCalculator(); + if (performanceCalculator == null) + return null; + + var starsTask = difficultyCache.GetDifficultyAsync(score.BeatmapInfo!, score.Ruleset, score.Mods, token).ConfigureAwait(false); + if (await starsTask is not StarDifficulty stars) + return null; + + if (stars.DifficultyAttributes == null || stars.PerformanceAttributes == null) + return null; + + return new PerformanceBreakdown( + await performanceCalculator.CalculateAsync(score, stars.DifficultyAttributes, token).ConfigureAwait(false), + stars.PerformanceAttributes); + } + + private void setPerformance(PerformanceBreakdown breakdown) { spinner.Hide(); content.FadeIn(200); @@ -236,6 +256,8 @@ private void setTotalValues(PerformanceDisplayAttribute attribute, PerformanceDi protected override void Dispose(bool isDisposing) { cancellationTokenSource.Cancel(); + cancellationTokenSource.Dispose(); + base.Dispose(isDisposing); } } From bfcf6693cac611de1deb8909a76a15ef7f8517f2 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 17 Oct 2024 19:39:03 +0900 Subject: [PATCH 036/120] Simplify implementation --- osu.Game/Beatmaps/BeatmapDifficultyCache.cs | 85 +++++++-------------- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 6 ++ 2 files changed, 33 insertions(+), 58 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs index c4506c9f7cdf..fc4175415c31 100644 --- a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs +++ b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs @@ -21,7 +21,6 @@ using osu.Game.Rulesets; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; using osu.Game.Scoring; @@ -245,11 +244,35 @@ private void updateBindable(BindableStarDifficulty bindable, IRulesetInfo? rules var ruleset = rulesetInfo.CreateInstance(); Debug.Assert(ruleset != null); - PlayableCachedWorkingBeatmap working = new PlayableCachedWorkingBeatmap(beatmapManager.GetWorkingBeatmap(key.BeatmapInfo)); + PlayableCachedWorkingBeatmap workingBeatmap = new PlayableCachedWorkingBeatmap(beatmapManager.GetWorkingBeatmap(key.BeatmapInfo)); + IBeatmap playableBeatmap = workingBeatmap.GetPlayableBeatmap(ruleset.RulesetInfo, key.OrderedMods, cancellationToken); - var difficulty = ruleset.CreateDifficultyCalculator(working).Calculate(key.OrderedMods, cancellationToken); + var difficulty = ruleset.CreateDifficultyCalculator(workingBeatmap).Calculate(key.OrderedMods, cancellationToken); + cancellationToken.ThrowIfCancellationRequested(); + + var performanceCalculator = ruleset.CreatePerformanceCalculator(); + if (performanceCalculator == null) + return new StarDifficulty(difficulty, new PerformanceAttributes()); + + ScoreProcessor scoreProcessor = ruleset.CreateScoreProcessor(); + scoreProcessor.Mods.Value = key.OrderedMods; + scoreProcessor.ApplyBeatmap(playableBeatmap); + cancellationToken.ThrowIfCancellationRequested(); + + ScoreInfo perfectScore = new ScoreInfo(key.BeatmapInfo, ruleset.RulesetInfo) + { + Passed = true, + Accuracy = 1, + Mods = key.OrderedMods, + MaxCombo = scoreProcessor.MaximumCombo, + Combo = scoreProcessor.MaximumCombo, + TotalScore = scoreProcessor.MaximumTotalScore, + Statistics = scoreProcessor.MaximumStatistics, + MaximumStatistics = scoreProcessor.MaximumStatistics + }; + + var performance = performanceCalculator.Calculate(perfectScore, difficulty); cancellationToken.ThrowIfCancellationRequested(); - var performance = computeMaxPerformance(working, key.BeatmapInfo, ruleset, key.OrderedMods, difficulty); return new StarDifficulty(difficulty, performance); } @@ -273,60 +296,6 @@ private void updateBindable(BindableStarDifficulty bindable, IRulesetInfo? rules } } - private static PerformanceAttributes computeMaxPerformance(IWorkingBeatmap working, BeatmapInfo beatmap, Ruleset ruleset, Mod[] mods, DifficultyAttributes attributes) - { - var performanceCalculator = ruleset.CreatePerformanceCalculator(); - if (performanceCalculator == null) - return new PerformanceAttributes(); - - IBeatmap playableBeatmap = working.GetPlayableBeatmap(ruleset.RulesetInfo, mods); - - // create statistics assuming all hit objects have perfect hit result - var statistics = playableBeatmap.HitObjects - .SelectMany(getPerfectHitResults) - .GroupBy(hr => hr, (hr, list) => (hitResult: hr, count: list.Count())) - .ToDictionary(pair => pair.hitResult, pair => pair.count); - - // compute maximum total score - ScoreProcessor scoreProcessor = ruleset.CreateScoreProcessor(); - scoreProcessor.Mods.Value = mods; - scoreProcessor.ApplyBeatmap(playableBeatmap); - long maxScore = scoreProcessor.MaximumTotalScore; - - // todo: Get max combo from difficulty calculator instead when diffcalc properly supports lazer-first scores - int maxCombo = calculateMaxCombo(playableBeatmap); - - // compute maximum rank - default to SS, then adjust the rank with mods - ScoreRank maxRank = ScoreRank.X; - foreach (IApplicableToScoreProcessor mod in mods.OfType()) - maxRank = mod.AdjustRank(maxRank, 1); - - ScoreInfo perfectScore = new ScoreInfo(beatmap, ruleset.RulesetInfo) - { - Accuracy = 1, - Passed = true, - MaxCombo = maxCombo, - Combo = maxCombo, - Mods = mods, - TotalScore = maxScore, - Statistics = statistics, - MaximumStatistics = statistics - }; - - return performanceCalculator.Calculate(perfectScore, attributes); - - static int calculateMaxCombo(IBeatmap beatmap) - => beatmap.HitObjects.SelectMany(getPerfectHitResults).Count(r => r.AffectsCombo()); - - static IEnumerable getPerfectHitResults(HitObject hitObject) - { - foreach (HitObject nested in hitObject.NestedHitObjects) - yield return nested.Judgement.MaxResult; - - yield return hitObject.Judgement.MaxResult; - } - } - protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 9752918dfbea..7b5af9bedaef 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -119,6 +119,11 @@ public partial class ScoreProcessor : JudgementProcessor /// public long MaximumTotalScore { get; private set; } + /// + /// The maximum achievable combo. + /// + public int MaximumCombo { get; private set; } + /// /// The maximum sum of accuracy-affecting judgements at the current point in time. /// @@ -423,6 +428,7 @@ protected override void Reset(bool storeResults) MaximumResultCounts.AddRange(ScoreResultCounts); MaximumTotalScore = TotalScore.Value; + MaximumCombo = HighestCombo.Value; } ScoreResultCounts.Clear(); From 6749768b9e1a1b4d095581ade5359e7927f1e34a Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 17 Oct 2024 19:43:34 +0900 Subject: [PATCH 037/120] Add max performance beatmap attribute text --- .../SkinComponents/BeatmapAttributeTextStrings.cs | 13 +++++++++---- .../Skinning/Components/BeatmapAttributeText.cs | 10 ++++++++++ 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/osu.Game/Localisation/SkinComponents/BeatmapAttributeTextStrings.cs b/osu.Game/Localisation/SkinComponents/BeatmapAttributeTextStrings.cs index b2e2285fafa0..390a6f9ca400 100644 --- a/osu.Game/Localisation/SkinComponents/BeatmapAttributeTextStrings.cs +++ b/osu.Game/Localisation/SkinComponents/BeatmapAttributeTextStrings.cs @@ -12,23 +12,28 @@ public static class BeatmapAttributeTextStrings /// /// "Attribute" /// - public static LocalisableString Attribute => new TranslatableString(getKey(@"attribute"), "Attribute"); + public static LocalisableString Attribute => new TranslatableString(getKey(@"attribute"), @"Attribute"); /// /// "The attribute to be displayed." /// - public static LocalisableString AttributeDescription => new TranslatableString(getKey(@"attribute_description"), "The attribute to be displayed."); + public static LocalisableString AttributeDescription => new TranslatableString(getKey(@"attribute_description"), @"The attribute to be displayed."); /// /// "Template" /// - public static LocalisableString Template => new TranslatableString(getKey(@"template"), "Template"); + public static LocalisableString Template => new TranslatableString(getKey(@"template"), @"Template"); /// /// "Supports {{Label}} and {{Value}}, but also including arbitrary attributes like {{StarRating}} (see attribute list for supported values)." /// public static LocalisableString TemplateDescription => new TranslatableString(getKey(@"template_description"), @"Supports {{Label}} and {{Value}}, but also including arbitrary attributes like {{StarRating}} (see attribute list for supported values)."); - private static string getKey(string key) => $"{prefix}:{key}"; + /// + /// "Max PP" + /// + public static LocalisableString MaxPP => new TranslatableString(getKey(@"max_pp"), @"Max PP"); + + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Skinning/Components/BeatmapAttributeText.cs b/osu.Game/Skinning/Components/BeatmapAttributeText.cs index ccee90410e85..28352b533fc6 100644 --- a/osu.Game/Skinning/Components/BeatmapAttributeText.cs +++ b/osu.Game/Skinning/Components/BeatmapAttributeText.cs @@ -21,6 +21,7 @@ using osu.Game.Localisation.SkinComponents; using osu.Game.Resources.Localisation.Web; using osu.Game.Rulesets; +using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Mods; using osu.Game.Utils; @@ -167,6 +168,9 @@ private LocalisableString getLabelString(BeatmapAttribute attribute) case BeatmapAttribute.BPM: return BeatmapsetsStrings.ShowStatsBpm; + case BeatmapAttribute.MaxPP: + return BeatmapAttributeTextStrings.MaxPP; + default: throw new ArgumentOutOfRangeException(); } @@ -217,6 +221,11 @@ private LocalisableString getValueString(BeatmapAttribute attribute) ? starDifficulty.Stars.ToLocalisableString(@"F2") : @"..."; + case BeatmapAttribute.MaxPP: + return difficultyBindable?.Value?.PerformanceAttributes is PerformanceAttributes attributes + ? attributes.Total.ToLocalisableString(@"F2") + : @"..."; + default: throw new ArgumentOutOfRangeException(); } @@ -269,5 +278,6 @@ public enum BeatmapAttribute RankedStatus, BPM, Source, + MaxPP } } From 0473f0b60a58a0b0b733f9125ed121022c1fc6d1 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 17 Oct 2024 20:39:24 +0900 Subject: [PATCH 038/120] Use stored difficulty to reduce flickering --- osu.Game/Skinning/Components/BeatmapAttributeText.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/osu.Game/Skinning/Components/BeatmapAttributeText.cs b/osu.Game/Skinning/Components/BeatmapAttributeText.cs index bfdb7655fc14..1b4f853d72c4 100644 --- a/osu.Game/Skinning/Components/BeatmapAttributeText.cs +++ b/osu.Game/Skinning/Components/BeatmapAttributeText.cs @@ -21,7 +21,6 @@ using osu.Game.Localisation.SkinComponents; using osu.Game.Resources.Localisation.Web; using osu.Game.Rulesets; -using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Mods; using osu.Game.Utils; @@ -225,9 +224,7 @@ private LocalisableString getValueString(BeatmapAttribute attribute) return (starDifficulty?.Stars ?? 0).ToLocalisableString(@"F2"); case BeatmapAttribute.MaxPP: - return difficultyBindable?.Value?.PerformanceAttributes is PerformanceAttributes attributes - ? attributes.Total.ToLocalisableString(@"F2") - : @"..."; + return (starDifficulty?.PerformanceAttributes?.Total ?? 0).ToLocalisableString(@"F2"); default: throw new ArgumentOutOfRangeException(); From 03094533b481cc2365b59d2b9c5e9b51a77f08f4 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 17 Oct 2024 22:03:53 +0900 Subject: [PATCH 039/120] Add test --- .../TestSceneBeatmapAttributeText.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapAttributeText.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapAttributeText.cs index 01659a265421..5c929a4d6216 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapAttributeText.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapAttributeText.cs @@ -159,6 +159,23 @@ public void TestStarRating() AddUntilStep("check star rating is 2", getText, () => Is.EqualTo("Star Rating: 2.00")); } + [Test] + public void TestMaxPp() + { + AddStep("set test ruleset", () => Ruleset.Value = new TestRuleset().RulesetInfo); + AddStep("set max pp attribute", () => text.Attribute.Value = BeatmapAttribute.MaxPP); + AddAssert("check max pp is 0", getText, () => Is.EqualTo("Max PP: 0.00")); + + // Adding mod + TestMod mod = null!; + AddStep("add mod with pp 1", () => SelectedMods.Value = new[] { mod = new TestMod { Performance = { Value = 1 } } }); + AddUntilStep("check max pp is 1", getText, () => Is.EqualTo("Max PP: 1.00")); + + // Changing mod setting + AddStep("change mod pp to 2", () => mod.Performance.Value = 2); + AddUntilStep("check max pp is 2", getText, () => Is.EqualTo("Max PP: 2.00")); + } + private string getText() => text.ChildrenOfType().Single().Text.ToString(); private class TestRuleset : Ruleset From c03017438e3c78fb0e13cdf28fb2fb426b13f363 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sat, 19 Oct 2024 23:57:05 +0200 Subject: [PATCH 040/120] Use FinishTransforms to init visual state --- .../Edit/Compose/Components/Timeline/SamplePointPiece.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs index ded7c6605853..48178b2e191f 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs @@ -78,9 +78,6 @@ protected override void LoadComplete() { base.LoadComplete(); - samplesVisible.BindValueChanged(visible => this.FadeTo(visible.NewValue ? 1 : 0, 200, Easing.OutQuint)); - this.FadeTo(samplesVisible.Value ? 1 : 0); - if (timelineBlueprintContainer != null) contracted.BindTo(timelineBlueprintContainer.SamplePointContracted); @@ -99,6 +96,9 @@ protected override void LoadComplete() LabelContainer.CornerRadius = 8; } }, true); + + samplesVisible.BindValueChanged(visible => this.FadeTo(visible.NewValue ? 1 : 0, 200, Easing.OutQuint), true); + FinishTransforms(); } protected override void Dispose(bool isDisposing) From f137fed04e16b6a1829073b673571e6d6e475376 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sat, 19 Oct 2024 23:57:41 +0200 Subject: [PATCH 041/120] Update sample point contracted state each frame --- .../Timeline/TimelineBlueprintContainer.cs | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs index b50739c80fe9..fec15517a47a 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs @@ -15,7 +15,6 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; -using osu.Framework.Layout; using osu.Framework.Utils; using osu.Game.Graphics; using osu.Game.Rulesets.Edit; @@ -38,8 +37,6 @@ internal partial class TimelineBlueprintContainer : EditorBlueprintContainer private bool hitObjectDragged; - private readonly LayoutValue samplePointContractedStateCache = new LayoutValue(Invalidation.DrawSize); - /// /// Positional input must be received outside the container's bounds, /// in order to handle timeline blueprints which are stacked offscreen. @@ -54,8 +51,6 @@ public TimelineBlueprintContainer(HitObjectComposer composer) Origin = Anchor.Centre; Height = 0.6f; - - AddLayout(samplePointContractedStateCache); } [BackgroundDependencyLoader] @@ -123,10 +118,9 @@ protected override void Update() Composer.Playfield.FutureLifetimeExtension = timeline.VisibleRange / 2; } - updateSamplePointContractedState(); - base.Update(); + updateSamplePointContractedState(); updateStacking(); } @@ -134,8 +128,7 @@ protected override void Update() private void updateSamplePointContractedState() { - if (samplePointContractedStateCache.IsValid) - return; + // because only blueprints of objects which are alive (via pooling) are displayed in the timeline, it's feasible to do this every-update. const double minimum_gap = 28; @@ -157,7 +150,6 @@ private void updateSamplePointContractedState() double smallestAbsoluteGap = ((TimelineSelectionBlueprintContainer)SelectionBlueprints).ContentRelativeToAbsoluteFactor.X * smallestTimeGap; SamplePointContracted.Value = smallestAbsoluteGap < minimum_gap; - samplePointContractedStateCache.Validate(); } private readonly Stack currentConcurrentObjects = new Stack(); From 22aef90cb7b1edaff97c86ec640ce49b750c6b1f Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sun, 20 Oct 2024 00:07:40 +0200 Subject: [PATCH 042/120] dont contract samples for stacked objects --- .../Components/Timeline/TimelineBlueprintContainer.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs index fec15517a47a..c8448b745e46 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs @@ -144,7 +144,13 @@ private void updateSamplePointContractedState() if (hitObject is IHasRepeats hasRepeats) smallestTimeGap = Math.Min(smallestTimeGap, hasRepeats.Duration / hasRepeats.SpanCount() / 2); - smallestTimeGap = Math.Min(smallestTimeGap, lastTime - hitObject.GetEndTime()); + double gap = lastTime - hitObject.GetEndTime(); + + // If the gap is less than 1ms, we can assume that the objects are stacked on top of each other + // Contracting doesn't make sense in this case + if (gap > 1 && gap < smallestTimeGap) + smallestTimeGap = gap; + lastTime = hitObject.StartTime; } From 3a4d5af83efc812f41ba6a95d6890fe7b768225b Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sun, 13 Oct 2024 11:07:49 -0400 Subject: [PATCH 043/120] Fix placement blueprints sometimes not behaving correctly with touch input More specifically, this fixes placement blueprints not beginning placement when using touch input while the cursor was previously outside compose area, due to the placement blueprint not existing (removed from the scene by `ComposeBlueprintContainer`). --- .../Edit/Blueprints/GridPlacementBlueprint.cs | 25 ++++++++++--- .../Sliders/SliderPlacementBlueprint.cs | 7 ++++ osu.Game/Rulesets/Edit/HitObjectComposer.cs | 17 +++++---- .../Edit/HitObjectPlacementBlueprint.cs | 27 +++++++++++++- osu.Game/Rulesets/Edit/PlacementBlueprint.cs | 24 +++++++++--- .../Components/ComposeBlueprintContainer.cs | 37 +++++++++---------- .../Screens/Edit/Compose/IPlacementHandler.cs | 16 +++++--- .../Visual/PlacementBlueprintTestScene.cs | 16 +++++--- 8 files changed, 116 insertions(+), 53 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/GridPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/GridPlacementBlueprint.cs index b13663cb446b..163b42bcfd30 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/GridPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/GridPlacementBlueprint.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; using osu.Game.Rulesets.Edit; using osuTK; @@ -31,12 +32,7 @@ private void load(OsuGridToolboxGroup gridToolboxGroup) public override void EndPlacement(bool commit) { if (!commit && PlacementActive != PlacementState.Finished) - { - gridToolboxGroup.StartPosition.Value = originalOrigin; - gridToolboxGroup.Spacing.Value = originalSpacing; - if (!gridToolboxGroup.GridLinesRotation.Disabled) - gridToolboxGroup.GridLinesRotation.Value = originalRotation; - } + resetGridState(); base.EndPlacement(commit); @@ -103,6 +99,9 @@ protected override void OnDragEnd(DragEndEvent e) public override void UpdateTimeAndPosition(SnapResult result) { + if (State.Value == Visibility.Hidden) + return; + var pos = ToLocalSpace(result.ScreenSpacePosition); if (PlacementActive != PlacementState.Active) @@ -122,5 +121,19 @@ public override void UpdateTimeAndPosition(SnapResult result) } } } + + protected override void PopOut() + { + base.PopOut(); + resetGridState(); + } + + private void resetGridState() + { + gridToolboxGroup.StartPosition.Value = originalOrigin; + gridToolboxGroup.Spacing.Value = originalSpacing; + if (!gridToolboxGroup.GridLinesRotation.Disabled) + gridToolboxGroup.GridLinesRotation.Value = originalRotation; + } } } diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index cb57c8e6e0c9..4f2f6516a890 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -156,6 +156,13 @@ protected override bool OnMouseDown(MouseDownEvent e) return true; } + // this allows sliders to be drawn outside compose area (after starting from a point within the compose area). + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => base.ReceivePositionalInputAt(screenSpacePos) || PlacementActive == PlacementState.Active; + + // ReceivePositionalInputAtSubTree generally always returns true when masking is disabled, but we don't want that, + // otherwise a slider path tooltip will be displayed anywhere in the editor (outside compose area). + protected override bool ReceivePositionalInputAtSubTree(Vector2 screenSpacePos) => ReceivePositionalInputAt(screenSpacePos); + private void beginNewSegment(PathControlPoint lastPoint) { segmentStart = lastPoint; diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index 0499e10607ad..a33d258602ee 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -484,22 +484,23 @@ private void toolSelected(CompositionTool tool) #region IPlacementHandler - public void BeginPlacement(HitObject hitObject) + public void ShowPlacement(HitObject hitObject) { EditorBeatmap.PlacementObject.Value = hitObject; } - public void EndPlacement(HitObject hitObject, bool commit) + public void HidePlacement() { EditorBeatmap.PlacementObject.Value = null; + } - if (commit) - { - EditorBeatmap.Add(hitObject); + public void CommitPlacement(HitObject hitObject) + { + EditorBeatmap.PlacementObject.Value = null; + EditorBeatmap.Add(hitObject); - if (autoSeekOnPlacement.Value && EditorClock.CurrentTime < hitObject.StartTime) - EditorClock.SeekSmoothlyTo(hitObject.StartTime); - } + if (autoSeekOnPlacement.Value && EditorClock.CurrentTime < hitObject.StartTime) + EditorClock.SeekSmoothlyTo(hitObject.StartTime); } public void Delete(HitObject hitObject) => EditorBeatmap.Remove(hitObject); diff --git a/osu.Game/Rulesets/Edit/HitObjectPlacementBlueprint.cs b/osu.Game/Rulesets/Edit/HitObjectPlacementBlueprint.cs index 74025b426089..f5b5648d5eda 100644 --- a/osu.Game/Rulesets/Edit/HitObjectPlacementBlueprint.cs +++ b/osu.Game/Rulesets/Edit/HitObjectPlacementBlueprint.cs @@ -5,6 +5,7 @@ using System.Threading; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Graphics.Containers; using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; @@ -58,18 +59,26 @@ private void load() startTimeBindable.BindValueChanged(_ => ApplyDefaultsToHitObject(), true); } + private bool placementBegun; + protected override void BeginPlacement(bool commitStart = false) { base.BeginPlacement(commitStart); - placementHandler.BeginPlacement(HitObject); + if (State.Value == Visibility.Visible) + placementHandler.ShowPlacement(HitObject); + + placementBegun = true; } public override void EndPlacement(bool commit) { base.EndPlacement(commit); - placementHandler.EndPlacement(HitObject, IsValidForPlacement && commit); + if (IsValidForPlacement && commit) + placementHandler.CommitPlacement(HitObject); + else + placementHandler.HidePlacement(); } /// @@ -122,5 +131,19 @@ public override void UpdateTimeAndPosition(SnapResult result) /// refreshing and parameters for the . /// protected void ApplyDefaultsToHitObject() => HitObject.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty); + + protected override void PopIn() + { + base.PopIn(); + + if (placementBegun) + placementHandler.ShowPlacement(HitObject); + } + + protected override void PopOut() + { + base.PopOut(); + placementHandler.HidePlacement(); + } } } diff --git a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs index a36de0243395..a4a35fcd5cac 100644 --- a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs +++ b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs @@ -7,7 +7,6 @@ using osu.Framework.Input.Events; using osu.Game.Input.Bindings; using osu.Game.Rulesets.Objects; -using osuTK; using osuTK.Input; namespace osu.Game.Rulesets.Edit @@ -15,7 +14,7 @@ namespace osu.Game.Rulesets.Edit /// /// A blueprint which governs the placement of something. /// - public abstract partial class PlacementBlueprint : CompositeDrawable, IKeyBindingHandler + public abstract partial class PlacementBlueprint : VisibilityContainer, IKeyBindingHandler { /// /// Whether the is currently mid-placement, but has not necessarily finished being placed. @@ -31,12 +30,17 @@ public abstract partial class PlacementBlueprint : CompositeDrawable, IKeyBindin /// protected virtual bool IsValidForPlacement => true; + // the blueprint should still be considered for input even if it is hidden, + // especially when such input is the reason for making the blueprint become visible. + public override bool PropagatePositionalInputSubTree => true; + public override bool PropagateNonPositionalInputSubTree => true; + protected PlacementBlueprint() { RelativeSizeAxes = Axes.Both; - // This is required to allow the blueprint's position to be updated via OnMouseMove/Handle - // on the same frame it is made visible via a PlacementState change. + // the blueprint should still be considered for input even if it is hidden, + // especially when such input is the reason for making the blueprint become visible. AlwaysPresent = true; } @@ -104,8 +108,6 @@ public void OnReleased(KeyBindingReleaseEvent e) { } - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; - protected override bool Handle(UIEvent e) { base.Handle(e); @@ -127,6 +129,16 @@ protected override bool Handle(UIEvent e) } } + protected override void PopIn() + { + this.FadeIn(200, Easing.OutQuint); + } + + protected override void PopOut() + { + this.FadeOut(200, Easing.OutQuint); + } + public enum PlacementState { Waiting, diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index aa7072007d71..fde4783661ce 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -289,9 +289,12 @@ public static Drawable GetIconForSample(string sampleName) /// /// Refreshes the current placement tool. /// - private void refreshTool() + private void refreshPlacement() { - removePlacement(); + CurrentPlacement?.EndPlacement(false); + CurrentPlacement?.Expire(); + CurrentPlacement = null; + ensurePlacementCreated(); } @@ -317,21 +320,24 @@ protected override void Update() { case PlacementBlueprint.PlacementState.Waiting: if (!Composer.CursorInPlacementArea) - removePlacement(); + CurrentPlacement.Hide(); + else if (Composer.CursorInPlacementArea) + CurrentPlacement.Show(); + + break; + + case PlacementBlueprint.PlacementState.Active: + CurrentPlacement.Show(); break; case PlacementBlueprint.PlacementState.Finished: - removePlacement(); + refreshPlacement(); break; } - } - - if (Composer.CursorInPlacementArea) - ensurePlacementCreated(); - // updates the placement with the latest editor clock time. - if (CurrentPlacement != null) + // updates the placement with the latest editor clock time. updatePlacementTimeAndPosition(); + } } protected override bool OnMouseMove(MouseMoveEvent e) @@ -359,7 +365,7 @@ protected sealed override SelectionBlueprint CreateBlueprintFor(HitOb private void hitObjectAdded(HitObject obj) { // refresh the tool to handle the case of placement completing. - refreshTool(); + refreshPlacement(); // on successful placement, the new combo button should be reset as this is the most common user interaction. if (Beatmap.SelectedHitObjects.Count == 0) @@ -388,14 +394,7 @@ private void ensurePlacementCreated() public void CommitIfPlacementActive() { CurrentPlacement?.EndPlacement(CurrentPlacement.PlacementActive == PlacementBlueprint.PlacementState.Active); - removePlacement(); - } - - private void removePlacement() - { - CurrentPlacement?.EndPlacement(false); - CurrentPlacement?.Expire(); - CurrentPlacement = null; + refreshPlacement(); } private CompositionTool currentTool; diff --git a/osu.Game/Screens/Edit/Compose/IPlacementHandler.cs b/osu.Game/Screens/Edit/Compose/IPlacementHandler.cs index 57960a76a156..e2046cd532c6 100644 --- a/osu.Game/Screens/Edit/Compose/IPlacementHandler.cs +++ b/osu.Game/Screens/Edit/Compose/IPlacementHandler.cs @@ -10,17 +10,21 @@ namespace osu.Game.Screens.Edit.Compose public interface IPlacementHandler { /// - /// Notifies that a placement has begun. + /// Notifies that a placement blueprint became visible on the screen. /// - /// The being placed. - void BeginPlacement(HitObject hitObject); + /// The representing the placement. + void ShowPlacement(HitObject hitObject); /// - /// Notifies that a placement has finished. + /// Notifies that a visible placement blueprint has been hidden. + /// + void HidePlacement(); + + /// + /// Notifies that a placement has been committed. /// /// The that has been placed. - /// Whether the object should be committed. - void EndPlacement(HitObject hitObject, bool commit); + void CommitPlacement(HitObject hitObject); /// /// Deletes a . diff --git a/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs b/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs index a52325dea2d4..aa8aff3adcf5 100644 --- a/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs +++ b/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs @@ -59,20 +59,20 @@ protected virtual IBeatmap GetPlayableBeatmap() protected override void LoadComplete() { base.LoadComplete(); - ResetPlacement(); } - public void BeginPlacement(HitObject hitObject) + public void ShowPlacement(HitObject hitObject) { } - public void EndPlacement(HitObject hitObject, bool commit) + public void HidePlacement() { - if (commit) - AddHitObject(CreateHitObject(hitObject)); + } - ResetPlacement(); + public void CommitPlacement(HitObject hitObject) + { + AddHitObject(CreateHitObject(hitObject)); } protected void ResetPlacement() @@ -89,6 +89,10 @@ public void Delete(HitObject hitObject) protected override void Update() { base.Update(); + + if (CurrentBlueprint.PlacementActive == PlacementBlueprint.PlacementState.Finished) + ResetPlacement(); + updatePlacementTimeAndPosition(); } From eb61da423299e50de211f10cd1e093f1ac444207 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Thu, 24 Oct 2024 21:22:09 +0200 Subject: [PATCH 044/120] only consider hit objects which are visible in timeline for contracted state --- .../Timeline/TimelineBlueprintContainer.cs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs index c8448b745e46..d4b910174772 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs @@ -32,6 +32,9 @@ internal partial class TimelineBlueprintContainer : EditorBlueprintContainer [Resolved(CanBeNull = true)] private Timeline timeline { get; set; } + [Resolved(CanBeNull = true)] + private EditorClock editorClock { get; set; } + private Bindable placement; private SelectionBlueprint placementBlueprint; @@ -128,10 +131,11 @@ protected override void Update() private void updateSamplePointContractedState() { - // because only blueprints of objects which are alive (via pooling) are displayed in the timeline, it's feasible to do this every-update. - const double minimum_gap = 28; + if (timeline == null || editorClock == null) + return; + // Find the smallest time gap between any two sample point pieces double smallestTimeGap = double.PositiveInfinity; double lastTime = double.PositiveInfinity; @@ -141,6 +145,14 @@ private void updateSamplePointContractedState() { var hitObject = selectionBlueprint.Item; + // Only check the hit objects which are visible in the timeline + // SelectionBlueprints can contain hit objects which are not visible in the timeline due to selection keeping them alive + if (hitObject.StartTime > editorClock.CurrentTime + timeline.VisibleRange / 2) + continue; + + if (hitObject.GetEndTime() < editorClock.CurrentTime - timeline.VisibleRange / 2) + break; + if (hitObject is IHasRepeats hasRepeats) smallestTimeGap = Math.Min(smallestTimeGap, hasRepeats.Duration / hasRepeats.SpanCount() / 2); From 757416e6476ca8168b95c129e3d5ed15b49581a2 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 24 Oct 2024 16:19:00 -0400 Subject: [PATCH 045/120] Move added test coverage to `TestSceneSliderDrawing` --- .../Editor/TestSceneSliderDrawing.cs | 83 ++++++++++++++++--- 1 file changed, 73 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderDrawing.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderDrawing.cs index 3c8358b4cd93..0e36c1dc4582 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderDrawing.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderDrawing.cs @@ -8,6 +8,7 @@ using osu.Framework.Input; using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.UI; using osu.Game.Screens.Edit.Components.RadioButtons; @@ -24,38 +25,93 @@ public partial class TestSceneSliderDrawing : TestSceneOsuEditor protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false); [Test] - public void TestTouchInputAfterTouchingComposeArea() + public void TestTouchInputPlaceHitCircleDirectly() + { + AddStep("tap circle", () => tap(this.ChildrenOfType().Single(b => b.Button.Label == "HitCircle"))); + + AddStep("tap to place circle", () => tap(this.ChildrenOfType().Single())); + AddAssert("circle placed correctly", () => + { + var circle = (HitCircle)EditorBeatmap.HitObjects.Single(h => h.StartTime == EditorClock.CurrentTimeAccurate); + Assert.Multiple(() => + { + Assert.That(circle.Position.X, Is.EqualTo(256f).Within(0.01f)); + Assert.That(circle.Position.Y, Is.EqualTo(192f).Within(0.01f)); + }); + + return true; + }); + } + + [Test] + public void TestTouchInputPlaceCircleAfterTouchingComposeArea() { AddStep("tap circle", () => tap(this.ChildrenOfType().Single(b => b.Button.Label == "HitCircle"))); - // this input is just for interacting with compose area AddStep("tap playfield", () => tap(this.ChildrenOfType().Single())); + AddAssert("circle placed", () => EditorBeatmap.HitObjects.Single(h => h.StartTime == EditorClock.CurrentTimeAccurate) is HitCircle); - AddStep("move current time", () => InputManager.Key(Key.Right)); + AddStep("move forward", () => InputManager.Key(Key.Right)); - AddStep("tap to place circle", () => tap(this.ChildrenOfType().Single().ToScreenSpace(new Vector2(10, 10)))); + AddStep("tap to place circle", () => tap(this.ChildrenOfType().Single())); AddAssert("circle placed correctly", () => { var circle = (HitCircle)EditorBeatmap.HitObjects.Single(h => h.StartTime == EditorClock.CurrentTimeAccurate); Assert.Multiple(() => { - Assert.That(circle.Position.X, Is.EqualTo(10f).Within(0.01f)); - Assert.That(circle.Position.Y, Is.EqualTo(10f).Within(0.01f)); + Assert.That(circle.Position.X, Is.EqualTo(256f).Within(0.01f)); + Assert.That(circle.Position.Y, Is.EqualTo(192f).Within(0.01f)); + }); + + return true; + }); + } + + [Test] + public void TestTouchInputPlaceSliderDirectly() + { + AddStep("tap slider", () => tap(this.ChildrenOfType().Single(b => b.Button.Label == "Slider"))); + + AddStep("hold to draw slider", () => InputManager.BeginTouch(new Touch(TouchSource.Touch1, this.ChildrenOfType().Single().ToScreenSpace(new Vector2(50, 20))))); + AddStep("drag to draw", () => InputManager.MoveTouchTo(new Touch(TouchSource.Touch1, this.ChildrenOfType().Single().ToScreenSpace(new Vector2(200, 50))))); + AddAssert("selection not initiated", () => this.ChildrenOfType().All(d => d.State == Visibility.Hidden)); + AddAssert("blueprint visible", () => this.ChildrenOfType().Single().Alpha > 0); + AddStep("end", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, InputManager.CurrentState.Touch.GetTouchPosition(TouchSource.Touch1)!.Value))); + AddAssert("slider placed correctly", () => + { + var slider = (Slider)EditorBeatmap.HitObjects.Single(h => h.StartTime == EditorClock.CurrentTimeAccurate); + Assert.Multiple(() => + { + Assert.That(slider.Position.X, Is.EqualTo(50f).Within(0.01f)); + Assert.That(slider.Position.Y, Is.EqualTo(20f).Within(0.01f)); + Assert.That(slider.Path.ControlPoints.Count, Is.EqualTo(2)); + Assert.That(slider.Path.ControlPoints[0].Position, Is.EqualTo(Vector2.Zero)); + + // the final position may be slightly off from the mouse position when drawing, account for that. + Assert.That(slider.Path.ControlPoints[1].Position.X, Is.EqualTo(150).Within(5)); + Assert.That(slider.Path.ControlPoints[1].Position.Y, Is.EqualTo(30).Within(5)); }); return true; }); + } + [Test] + public void TestTouchInputPlaceSliderAfterTouchingComposeArea() + { AddStep("tap slider", () => tap(this.ChildrenOfType().Single(b => b.Button.Label == "Slider"))); - // this input is just for interacting with compose area AddStep("tap playfield", () => tap(this.ChildrenOfType().Single())); + AddStep("tap and hold another spot", () => hold(this.ChildrenOfType().Single(), new Vector2(50, 0))); + AddUntilStep("wait for slider placement", () => EditorBeatmap.HitObjects.SingleOrDefault(h => h.StartTime == EditorClock.CurrentTimeAccurate) is Slider); + AddStep("end", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, InputManager.CurrentState.Touch.GetTouchPosition(TouchSource.Touch1)!.Value))); - AddStep("move current time", () => InputManager.Key(Key.Right)); + AddStep("move forward", () => InputManager.Key(Key.Right)); AddStep("hold to draw slider", () => InputManager.BeginTouch(new Touch(TouchSource.Touch1, this.ChildrenOfType().Single().ToScreenSpace(new Vector2(50, 20))))); AddStep("drag to draw", () => InputManager.MoveTouchTo(new Touch(TouchSource.Touch1, this.ChildrenOfType().Single().ToScreenSpace(new Vector2(200, 50))))); AddAssert("selection not initiated", () => this.ChildrenOfType().All(d => d.State == Visibility.Hidden)); + AddAssert("blueprint visible", () => this.ChildrenOfType().Single().IsPresent); AddStep("end", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, InputManager.CurrentState.Touch.GetTouchPosition(TouchSource.Touch1)!.Value))); AddAssert("slider placed correctly", () => { @@ -76,12 +132,19 @@ public void TestTouchInputAfterTouchingComposeArea() }); } - private void tap(Drawable drawable) => tap(drawable.ScreenSpaceDrawQuad.Centre); + private void tap(Drawable drawable, Vector2 offset = default) => tap(drawable.ToScreenSpace(drawable.LayoutRectangle.Centre + offset)); private void tap(Vector2 position) { - InputManager.BeginTouch(new Touch(TouchSource.Touch1, position)); + hold(position); InputManager.EndTouch(new Touch(TouchSource.Touch1, position)); } + + private void hold(Drawable drawable, Vector2 offset = default) => hold(drawable.ToScreenSpace(drawable.LayoutRectangle.Centre + offset)); + + private void hold(Vector2 position) + { + InputManager.BeginTouch(new Touch(TouchSource.Touch1, position)); + } } } From 1d559b2cade00341422b7efbf27f67dda0040489 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 24 Oct 2024 16:20:49 -0400 Subject: [PATCH 046/120] Remove fade transitions --- osu.Game/Rulesets/Edit/PlacementBlueprint.cs | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs index a4a35fcd5cac..52b8a5c796a3 100644 --- a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs +++ b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs @@ -129,15 +129,8 @@ protected override bool Handle(UIEvent e) } } - protected override void PopIn() - { - this.FadeIn(200, Easing.OutQuint); - } - - protected override void PopOut() - { - this.FadeOut(200, Easing.OutQuint); - } + protected override void PopIn() => this.FadeIn(); + protected override void PopOut() => this.FadeOut(); public enum PlacementState { From 36bcc5896ced539dae14a8e78093ba5f0ab81248 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 25 Oct 2024 14:40:37 +0200 Subject: [PATCH 047/120] Add failing test case --- .../TestSceneMultiplayerMatchSongSelect.cs | 43 ++++++++++++++++--- 1 file changed, 38 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs index 88cc7eb9b32a..bd635b166965 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Linq; +using JetBrains.Annotations; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; @@ -15,6 +16,7 @@ using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Configuration; using osu.Game.Database; using osu.Game.Online.Rooms; using osu.Game.Overlays.Mods; @@ -42,6 +44,9 @@ public partial class TestSceneMultiplayerMatchSongSelect : MultiplayerTestScene private Live importedBeatmapSet; + [Resolved] + private OsuConfigManager configManager { get; set; } + [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { @@ -57,10 +62,8 @@ private void load(GameHost host, AudioManager audio) Add(detachedBeatmapStore); } - public override void SetUpSteps() + private void setUp() { - base.SetUpSteps(); - AddStep("reset", () => { Ruleset.Value = new OsuRuleset().RulesetInfo; @@ -75,6 +78,8 @@ public override void SetUpSteps() [Test] public void TestSelectFreeMods() { + setUp(); + AddStep("set some freemods", () => songSelect.FreeMods.Value = new OsuRuleset().GetModsFor(ModType.Fun).ToArray()); AddStep("set all freemods", () => songSelect.FreeMods.Value = new OsuRuleset().CreateAllMods().ToArray()); AddStep("set no freemods", () => songSelect.FreeMods.Value = Array.Empty()); @@ -85,6 +90,8 @@ public void TestBeatmapConfirmed() { BeatmapInfo selectedBeatmap = null; + setUp(); + AddStep("change ruleset", () => Ruleset.Value = new TaikoRuleset().RulesetInfo); AddStep("select beatmap", () => songSelect.Carousel.SelectBeatmap(selectedBeatmap = beatmaps.First(beatmap => beatmap.Ruleset.OnlineID == new TaikoRuleset().LegacyID))); @@ -107,6 +114,8 @@ public void TestBeatmapConfirmed() [TestCase(typeof(OsuModHidden), typeof(OsuModTraceable))] // Incompatible. public void TestAllowedModDeselectedWhenRequired(Type allowedMod, Type requiredMod) { + setUp(); + AddStep("change ruleset", () => Ruleset.Value = new OsuRuleset().RulesetInfo); AddStep($"select {allowedMod.ReadableName()} as allowed", () => songSelect.FreeMods.Value = new[] { (Mod)Activator.CreateInstance(allowedMod) }); AddStep($"select {requiredMod.ReadableName()} as required", () => songSelect.Mods.Value = new[] { (Mod)Activator.CreateInstance(requiredMod) }); @@ -120,6 +129,30 @@ public void TestAllowedModDeselectedWhenRequired(Type allowedMod, Type requiredM assertFreeModNotShown(requiredMod); } + [Test] + public void TestChangeRulesetImmediatelyAfterLoadComplete() + { + AddStep("reset", () => + { + configManager.SetValue(OsuSetting.ShowConvertedBeatmaps, false); + Beatmap.SetDefault(); + SelectedMods.SetDefault(); + }); + + AddStep("create song select", () => + { + SelectedRoom.Value.Playlist.Single().RulesetID = 2; + songSelect = new TestMultiplayerMatchSongSelect(SelectedRoom.Value, SelectedRoom.Value.Playlist.Single()); + songSelect.OnLoadComplete += _ => Ruleset.Value = new TaikoRuleset().RulesetInfo; + LoadScreen(songSelect); + }); + AddUntilStep("wait for present", () => songSelect.IsCurrentScreen() && songSelect.BeatmapSetsLoaded); + + AddStep("confirm selection", () => songSelect.FinaliseSelection()); + AddAssert("beatmap is taiko", () => Beatmap.Value.BeatmapInfo.Ruleset.OnlineID, () => Is.EqualTo(1)); + AddAssert("ruleset is taiko", () => Ruleset.Value.OnlineID, () => Is.EqualTo(1)); + } + private void assertFreeModNotShown(Type type) { AddAssert($"{type.ReadableName()} not displayed in freemod overlay", @@ -138,8 +171,8 @@ private partial class TestMultiplayerMatchSongSelect : MultiplayerMatchSongSelec public new BeatmapCarousel Carousel => base.Carousel; - public TestMultiplayerMatchSongSelect(Room room) - : base(room) + public TestMultiplayerMatchSongSelect(Room room, [CanBeNull] PlaylistItem itemToEdit = null) + : base(room, itemToEdit) { } } From b78e7d5d9ae987c383c5c65689e9a62efb6c4b01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 25 Oct 2024 15:51:26 +0200 Subject: [PATCH 048/120] Fix multiplayer song select not correctly applying filter sometimes Fixes the root client-side failure causing https://github.com/ppy/osu/issues/30415. Thread of breakage is as follows: 1. `SongSelect` loads the carousel. At this point, the ruleset is what the ambient ruleset would have been at the time of pushing song select, so most likely it will match the current ruleset. Notably, the carousel is loaded with `AllowSelection == false`. 2. `OnlinePlaySongSelect` sets the ruleset to the one taken from the relevant playlist item in `LoadComplete()`. 3. At any point between the previous and the next step, the user changes the ruleset manually. 4. `SongSelect.carouselBeatmapsLoaded()` is ran, which calls `transferRulesetValue()`, which calls `FilterControl.FilterChanged`. But at this stage `Carousel.AllowSelection` is still false, so the filter is not executed, but `pendingFilterApplication` is set instead. Unfortunately, the pending filter never gets applied after that. The only place that checks that flag is `OnEntering()`, which at this point has already ran. To fix, move the `pendingFilterApplication` check to `Update()`, which seems like the most obvious and safe solution. --- osu.Game/Screens/Select/SongSelect.cs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index ea5048ca49dd..9f7a2c02ff1a 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -711,12 +711,6 @@ public override void OnResuming(ScreenTransitionEvent e) Carousel.AllowSelection = true; - if (pendingFilterApplication) - { - Carousel.Filter(FilterControl.CreateCriteria()); - pendingFilterApplication = false; - } - BeatmapDetails.Refresh(); beginLooping(); @@ -749,6 +743,17 @@ public override void OnResuming(ScreenTransitionEvent e) FilterControl.Activate(); } + protected override void Update() + { + base.Update(); + + if (Carousel.AllowSelection && pendingFilterApplication) + { + Carousel.Filter(FilterControl.CreateCriteria()); + pendingFilterApplication = false; + } + } + public override void OnSuspending(ScreenTransitionEvent e) { // Handle the case where FinaliseSelection is never called (ie. when a screen is pushed externally). From 0e65655feffcbe38f6e9b9573e16227c7225bbc3 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Fri, 25 Oct 2024 23:15:16 -0400 Subject: [PATCH 049/120] Remove redundant condition in else branch Yep. --- .../Edit/Compose/Components/ComposeBlueprintContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index d74152e78e5b..ef4f134766fa 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -325,7 +325,7 @@ protected override void Update() case PlacementBlueprint.PlacementState.Waiting: if (!Composer.CursorInPlacementArea) CurrentPlacement.Hide(); - else if (Composer.CursorInPlacementArea) + else CurrentPlacement.Show(); break; From 86989aa5ce8f835f03f3c8bb923346bf2456a6b6 Mon Sep 17 00:00:00 2001 From: StanR Date: Sun, 27 Oct 2024 13:40:29 +0500 Subject: [PATCH 050/120] Fix `UserRankPanel` rank overflowing on 6+ digits --- .../Visual/Online/TestSceneUserPanel.cs | 20 +++++++++++++------ osu.Game/Users/UserRankPanel.cs | 2 +- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs index 91942c391ae6..ab3d5907efb8 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs @@ -55,21 +55,21 @@ public void SetUp() => Schedule(() => { Username = @"flyte", Id = 3103765, - CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg" + CoverUrl = @"https://assets.ppy.sh/user-cover-presets/1/df28696b58541a9e67f6755918951d542d93bdf1da41720fcca2fd2c1ea8cf51.jpeg", }), new UserBrickPanel(new APIUser { Username = @"peppy", Id = 2, Colour = "99EB47", - CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg", }), new UserGridPanel(new APIUser { Username = @"flyte", Id = 3103765, CountryCode = CountryCode.JP, - CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg", + CoverUrl = @"https://assets.ppy.sh/user-cover-presets/1/df28696b58541a9e67f6755918951d542d93bdf1da41720fcca2fd2c1ea8cf51.jpeg", Status = { Value = UserStatus.Online } }) { Width = 300 }, boundPanel1 = new UserGridPanel(new APIUser @@ -77,7 +77,7 @@ public void SetUp() => Schedule(() => Username = @"peppy", Id = 2, CountryCode = CountryCode.AU, - CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg", IsSupporter = true, SupportLevel = 3, }) { Width = 300 }, @@ -95,7 +95,7 @@ public void SetUp() => Schedule(() => Username = @"flyte", Id = 3103765, CountryCode = CountryCode.JP, - CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg", + CoverUrl = @"https://assets.ppy.sh/user-cover-presets/1/df28696b58541a9e67f6755918951d542d93bdf1da41720fcca2fd2c1ea8cf51.jpeg", Statistics = new UserStatistics { GlobalRank = 12345, CountryRank = 1234 } }) { Width = 300 }, new UserRankPanel(new APIUser @@ -104,7 +104,7 @@ public void SetUp() => Schedule(() => Id = 2, Colour = "99EB47", CountryCode = CountryCode.AU, - CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg", Statistics = new UserStatistics { GlobalRank = null, CountryRank = null } }) { Width = 300 } } @@ -168,6 +168,14 @@ public void TestUserStatisticsChange() CountryRank = RNG.Next(100000) }); }); + AddStep("set statistics to something big", () => + { + API.UpdateStatistics(new UserStatistics + { + GlobalRank = RNG.Next(1_000_000, 100_000_000), + CountryRank = RNG.Next(1_000_000, 100_000_000) + }); + }); AddStep("set statistics to empty", () => { API.UpdateStatistics(new UserStatistics()); diff --git a/osu.Game/Users/UserRankPanel.cs b/osu.Game/Users/UserRankPanel.cs index 0d57b7bb7d48..6d8d7dd143cd 100644 --- a/osu.Game/Users/UserRankPanel.cs +++ b/osu.Game/Users/UserRankPanel.cs @@ -152,7 +152,7 @@ protected override Drawable CreateLayout() Margin = new MarginPadding { Top = main_content_height }, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Left = 80, Vertical = padding }, + Padding = new MarginPadding(padding), ColumnDimensions = new[] { new Dimension(), From ef22b6b1a86afa8998ec6cb7c482ba28bbf097d0 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 1 Nov 2024 22:40:00 +0900 Subject: [PATCH 051/120] Round to integral units The rounding matches the implementation of `PerformancePointsCounter`. --- osu.Game/Skinning/Components/BeatmapAttributeText.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Skinning/Components/BeatmapAttributeText.cs b/osu.Game/Skinning/Components/BeatmapAttributeText.cs index c858e750328e..79a1ed4d7c38 100644 --- a/osu.Game/Skinning/Components/BeatmapAttributeText.cs +++ b/osu.Game/Skinning/Components/BeatmapAttributeText.cs @@ -229,7 +229,7 @@ private LocalisableString getValueString(BeatmapAttribute attribute) return (starDifficulty?.Stars ?? 0).ToLocalisableString(@"F2"); case BeatmapAttribute.MaxPP: - return (starDifficulty?.PerformanceAttributes?.Total ?? 0).ToLocalisableString(@"F2"); + return Math.Round(starDifficulty?.PerformanceAttributes?.Total ?? 0, MidpointRounding.AwayFromZero).ToLocalisableString(); default: return string.Empty; From 03fc744e927e2cab99f2d43f9e289c75a70dba61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 1 Nov 2024 18:26:49 +0100 Subject: [PATCH 052/120] Fix test --- .../Visual/UserInterface/TestSceneBeatmapAttributeText.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapAttributeText.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapAttributeText.cs index df128ed89e2e..e3a6fca3198f 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapAttributeText.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapAttributeText.cs @@ -164,16 +164,16 @@ public void TestMaxPp() { AddStep("set test ruleset", () => Ruleset.Value = new TestRuleset().RulesetInfo); AddStep("set max pp attribute", () => text.Attribute.Value = BeatmapAttribute.MaxPP); - AddAssert("check max pp is 0", getText, () => Is.EqualTo("Max PP: 0.00")); + AddAssert("check max pp is 0", getText, () => Is.EqualTo("Max PP: 0")); // Adding mod TestMod mod = null!; AddStep("add mod with pp 1", () => SelectedMods.Value = new[] { mod = new TestMod { Performance = { Value = 1 } } }); - AddUntilStep("check max pp is 1", getText, () => Is.EqualTo("Max PP: 1.00")); + AddUntilStep("check max pp is 1", getText, () => Is.EqualTo("Max PP: 1")); // Changing mod setting AddStep("change mod pp to 2", () => mod.Performance.Value = 2); - AddUntilStep("check max pp is 2", getText, () => Is.EqualTo("Max PP: 2.00")); + AddUntilStep("check max pp is 2", getText, () => Is.EqualTo("Max PP: 2")); } private string getText() => text.ChildrenOfType().Single().Text.ToString(); From 8f48682d0a8a8385d2ade6db7ce0f03eac1668b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 1 Nov 2024 18:36:01 +0100 Subject: [PATCH 053/120] Remove hide sample toggle In line with feedback from https://github.com/ppy/osu/pull/29896#issuecomment-2378676851. --- osu.Game/Configuration/OsuConfigManager.cs | 2 -- osu.Game/Localisation/EditorStrings.cs | 5 ----- .../Edit/Compose/Components/Timeline/SamplePointPiece.cs | 5 ----- osu.Game/Screens/Edit/Editor.cs | 6 ------ 4 files changed, 18 deletions(-) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index ae2025fb50c2..8705253b7db4 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -205,7 +205,6 @@ protected override void InitialiseDefaults() SetDefault(OsuSetting.EditorTimelineShowTimingChanges, true); SetDefault(OsuSetting.EditorTimelineShowTicks, true); - SetDefault(OsuSetting.EditorTimelineShowSamples, true); SetDefault(OsuSetting.AlwaysShowHoldForMenuButton, false); } @@ -433,6 +432,5 @@ public enum OsuSetting EditorTimelineShowTimingChanges, EditorTimelineShowTicks, AlwaysShowHoldForMenuButton, - EditorTimelineShowSamples } } diff --git a/osu.Game/Localisation/EditorStrings.cs b/osu.Game/Localisation/EditorStrings.cs index f72a0f4a264b..bcffc18d4d3c 100644 --- a/osu.Game/Localisation/EditorStrings.cs +++ b/osu.Game/Localisation/EditorStrings.cs @@ -139,11 +139,6 @@ public static class EditorStrings /// public static LocalisableString TimelineShowTicks => new TranslatableString(getKey(@"timeline_show_ticks"), @"Show ticks"); - /// - /// "Show samples" - /// - public static LocalisableString TimelineShowSamples => new TranslatableString(getKey(@"timeline_show_samples"), @"Show samples"); - private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs index 48178b2e191f..0cfaeb8c38ac 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs @@ -44,8 +44,6 @@ public partial class SamplePointPiece : HitObjectPointPiece, IHasPopover [Resolved] private TimelineBlueprintContainer? timelineBlueprintContainer { get; set; } - private Bindable samplesVisible = null!; - public SamplePointPiece(HitObject hitObject) { HitObject = hitObject; @@ -68,8 +66,6 @@ private void load(OsuConfigManager config) if (editor != null) editor.ShowSampleEditPopoverRequested += onShowSampleEditPopoverRequested; - - samplesVisible = config.GetBindable(OsuSetting.EditorTimelineShowSamples); } private readonly Bindable contracted = new Bindable(); @@ -97,7 +93,6 @@ protected override void LoadComplete() } }, true); - samplesVisible.BindValueChanged(visible => this.FadeTo(visible.NewValue ? 1 : 0, 200, Easing.OutQuint), true); FinishTransforms(); } diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index b059557b2844..9bb91af80652 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -215,7 +215,6 @@ protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnl private Bindable editorLimitedDistanceSnap; private Bindable editorTimelineShowTimingChanges; private Bindable editorTimelineShowTicks; - private Bindable editorTimelineShowSamples; /// /// This controls the opacity of components like the timelines, sidebars, etc. @@ -324,7 +323,6 @@ private void load(OsuConfigManager config) editorLimitedDistanceSnap = config.GetBindable(OsuSetting.EditorLimitedDistanceSnap); editorTimelineShowTimingChanges = config.GetBindable(OsuSetting.EditorTimelineShowTimingChanges); editorTimelineShowTicks = config.GetBindable(OsuSetting.EditorTimelineShowTicks); - editorTimelineShowSamples = config.GetBindable(OsuSetting.EditorTimelineShowSamples); AddInternal(new OsuContextMenuContainer { @@ -390,10 +388,6 @@ private void load(OsuConfigManager config) { State = { BindTarget = editorTimelineShowTicks } }, - new ToggleMenuItem(EditorStrings.TimelineShowSamples) - { - State = { BindTarget = editorTimelineShowSamples } - } ] }, new BackgroundDimMenuItem(editorBackgroundDim), From 32190fdb580821dde55310fcd759a1b696657603 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 5 Nov 2024 16:39:13 +0900 Subject: [PATCH 054/120] Hide cursor trail from draw visualiser --- osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs index a4bccb0affc4..5132dc2859b3 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs @@ -14,6 +14,7 @@ using osu.Framework.Graphics.Shaders; using osu.Framework.Graphics.Shaders.Types; using osu.Framework.Graphics.Textures; +using osu.Framework.Graphics.Visualisation; using osu.Framework.Input; using osu.Framework.Input.Events; using osu.Framework.Timing; @@ -23,6 +24,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor { + [DrawVisualiserHidden] public partial class CursorTrail : Drawable, IRequireHighFrequencyMousePosition { private const int max_sprites = 2048; From 5f1b6963877cff5f2c496056701b5bccf69d793d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 4 Nov 2024 15:21:29 +0100 Subject: [PATCH 055/120] Fix strong context menu operation not being written properly --- osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs b/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs index b706e96bdb8b..be2a5ac144ab 100644 --- a/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs +++ b/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs @@ -54,17 +54,17 @@ private void load() public void SetStrongState(bool state) { - if (SelectedItems.OfType().All(h => h.IsStrong == state)) + if (SelectedItems.OfType().All(h => h.IsStrong == state)) return; EditorBeatmap.PerformOnSelection(h => { - if (!(h is Hit taikoHit)) return; + if (h is not TaikoStrongableHitObject strongable) return; - if (taikoHit.IsStrong != state) + if (strongable.IsStrong != state) { - taikoHit.IsStrong = state; - EditorBeatmap.Update(taikoHit); + strongable.IsStrong = state; + EditorBeatmap.Update(strongable); } }); } From 387fbc2214a37f70affd934c4e0f71761de78460 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 5 Nov 2024 10:59:22 +0100 Subject: [PATCH 056/120] Fix drum rolls losing width on strong state toggle in editor Fixes https://github.com/ppy/osu/issues/30480. --- .../Objects/Drawables/DrawableDrumRoll.cs | 1 + .../Objects/Drawables/DrawableDrumRollTick.cs | 7 +++++++ .../Objects/Drawables/DrawableHit.cs | 2 ++ .../Objects/Drawables/DrawableSwell.cs | 11 ++++++++++- .../Objects/Drawables/DrawableTaikoHitObject.cs | 3 --- .../Drawables/DrawableTaikoStrongableHitObject.cs | 8 -------- 6 files changed, 20 insertions(+), 12 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs index 1af4719b02c0..35c8a02af52a 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs @@ -78,6 +78,7 @@ protected override void RecreatePieces() { base.RecreatePieces(); updateColour(); + Height = HitObject.IsStrong ? TaikoStrongableHitObject.DEFAULT_STRONG_SIZE : TaikoHitObject.DEFAULT_SIZE; } protected override void OnFree() diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs index 0333fd71a93a..64d2020edc12 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs @@ -12,6 +12,7 @@ using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Taiko.Skinning.Default; using osu.Game.Skinning; +using osuTK; namespace osu.Game.Rulesets.Taiko.Objects.Drawables { @@ -44,6 +45,12 @@ protected override void OnApply() IsFirstTick.Value = HitObject.FirstTick; } + protected override void RecreatePieces() + { + base.RecreatePieces(); + Size = new Vector2(HitObject.IsStrong ? TaikoStrongableHitObject.DEFAULT_STRONG_SIZE : TaikoHitObject.DEFAULT_SIZE); + } + protected override void CheckForResult(bool userTriggered, double timeOffset) { if (!userTriggered) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs index a5e63c373fd1..28831a6d2c7b 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs @@ -14,6 +14,7 @@ using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Skinning.Default; using osu.Game.Skinning; +using osuTK; namespace osu.Game.Rulesets.Taiko.Objects.Drawables { @@ -63,6 +64,7 @@ protected override void RecreatePieces() { updateActionsFromType(); base.RecreatePieces(); + Size = new Vector2(HitObject.IsStrong ? TaikoStrongableHitObject.DEFAULT_STRONG_SIZE : TaikoHitObject.DEFAULT_SIZE); } protected override void OnFree() diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs index f2fcd185dd82..28617b35f68c 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs @@ -19,6 +19,7 @@ using osu.Game.Rulesets.Taiko.Skinning.Default; using osu.Game.Screens.Play; using osu.Game.Skinning; +using osuTK; namespace osu.Game.Rulesets.Taiko.Objects.Drawables { @@ -34,6 +35,8 @@ public partial class DrawableSwell : DrawableTaikoHitObject /// private const double ring_appear_offset = 100; + private Vector2 baseSize; + private readonly Container ticks; private readonly Container bodyContainer; private readonly CircularContainer targetRing; @@ -141,6 +144,12 @@ private void load(OsuColour colours) Origin = Anchor.Centre, }); + protected override void RecreatePieces() + { + base.RecreatePieces(); + Size = baseSize = new Vector2(TaikoHitObject.DEFAULT_SIZE); + } + protected override void OnFree() { base.OnFree(); @@ -269,7 +278,7 @@ protected override void Update() { base.Update(); - Size = BaseSize * Parent!.RelativeChildSize; + Size = baseSize * Parent!.RelativeChildSize; // Make the swell stop at the hit target X = Math.Max(0, X); diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs index 3f4694d71db3..0cf9651965db 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs @@ -130,7 +130,6 @@ public abstract partial class DrawableTaikoHitObject : DrawableTaikoHit public new TObject HitObject => (TObject)base.HitObject; - protected Vector2 BaseSize; protected SkinnableDrawable MainPiece; protected DrawableTaikoHitObject([CanBeNull] TObject hitObject) @@ -152,8 +151,6 @@ protected override void OnApply() protected virtual void RecreatePieces() { - Size = BaseSize = new Vector2(TaikoHitObject.DEFAULT_SIZE); - if (MainPiece != null) Content.Remove(MainPiece, true); diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoStrongableHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoStrongableHitObject.cs index 4d7cdf32430d..7c3ff4f27ec9 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoStrongableHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoStrongableHitObject.cs @@ -8,7 +8,6 @@ using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; -using osuTK; namespace osu.Game.Rulesets.Taiko.Objects.Drawables { @@ -44,13 +43,6 @@ protected override void OnFree() isStrong.UnbindEvents(); } - protected override void RecreatePieces() - { - base.RecreatePieces(); - if (HitObject.IsStrong) - Size = BaseSize = new Vector2(TaikoStrongableHitObject.DEFAULT_STRONG_SIZE); - } - protected override void AddNestedHitObject(DrawableHitObject hitObject) { base.AddNestedHitObject(hitObject); From a5e6da76cb090a72932d7c610d037390266c5036 Mon Sep 17 00:00:00 2001 From: Jay Lawton Date: Thu, 7 Nov 2024 19:53:53 +1000 Subject: [PATCH 057/120] introduce difficult strains globally --- .../Rulesets/Difficulty/Skills/StrainSkill.cs | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs b/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs index b07e8399c024..c28635ef2290 100644 --- a/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs +++ b/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs @@ -26,10 +26,10 @@ public abstract class StrainSkill : Skill protected virtual int SectionLength => 400; private double currentSectionPeak; // We also keep track of the peak strain level in the current section. - private double currentSectionEnd; private readonly List strainPeaks = new List(); + protected List ObjectStrains = new List(); // Store individual strains protected StrainSkill(Mod[] mods) : base(mods) @@ -57,7 +57,25 @@ public sealed override void Process(DifficultyHitObject current) currentSectionEnd += SectionLength; } - currentSectionPeak = Math.Max(StrainValueAt(current), currentSectionPeak); + double strain = StrainValueAt(current); + currentSectionPeak = Math.Max(strain, currentSectionPeak); + + // Store the strain value for the object + ObjectStrains.Add(strain); + } + + /// + /// Calculates the number of strains weighted against the top strain. + /// The result is scaled by clock rate as it affects the total number of strains. + /// + public double CountDifficultStrains() + { + if (ObjectStrains.Count == 0) + return 0.0; + + double consistentTopStrain = DifficultyValue() / 10; // What would the top strain be if all strain values were identical + // Use a weighted sum of all strains. Constants are arbitrary and give nice values + return ObjectStrains.Sum(s => 1.1 / (1 + Math.Exp(-10 * (s / consistentTopStrain - 0.88)))); } /// From 177781aca547b82ec9cc3bd0c2fbde81c7d8c1a7 Mon Sep 17 00:00:00 2001 From: Jay Lawton Date: Thu, 7 Nov 2024 19:57:37 +1000 Subject: [PATCH 058/120] remove localised instance of difficultstrains --- .../Difficulty/Skills/OsuStrainSkill.cs | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs index 96180c0aa120..c36534dcef95 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs @@ -60,20 +60,6 @@ public override double DifficultyValue() return Difficulty; } - /// - /// Returns the number of strains weighted against the top strain. - /// The result is scaled by clock rate as it affects the total number of strains. - /// - public double CountDifficultStrains() - { - if (Difficulty == 0) - return 0.0; - - double consistentTopStrain = Difficulty / 10; // What would the top strain be if all strain values were identical - // Use a weighted sum of all strains. Constants are arbitrary and give nice values - return ObjectStrains.Sum(s => 1.1 / (1 + Math.Exp(-10 * (s / consistentTopStrain - 0.88)))); - } - public static double DifficultyToPerformance(double difficulty) => Math.Pow(5.0 * Math.Max(1.0, difficulty / 0.0675) - 4.0, 3.0) / 100000.0; } } From 748055ab29d04c53542df0814689b05970110f53 Mon Sep 17 00:00:00 2001 From: Jay Lawton Date: Thu, 7 Nov 2024 20:15:59 +1000 Subject: [PATCH 059/120] remove double instance of array --- osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs | 1 - osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs | 1 - osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs | 1 - 3 files changed, 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs index b3d530e3affa..faf91e46520b 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs @@ -34,7 +34,6 @@ protected override double StrainValueAt(DifficultyHitObject current) { currentStrain *= strainDecay(current.DeltaTime); currentStrain += AimEvaluator.EvaluateDifficultyOf(current, withSliders) * skillMultiplier; - ObjectStrains.Add(currentStrain); return currentStrain; } diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs index c36534dcef95..559a871df1f0 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs @@ -23,7 +23,6 @@ public abstract class OsuStrainSkill : StrainSkill /// protected virtual double ReducedStrainBaseline => 0.75; - protected List ObjectStrains = new List(); protected double Difficulty; protected OsuStrainSkill(Mod[] mods) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs index e5aa25c1ebc2..d2c4bbb6180e 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs @@ -40,7 +40,6 @@ protected override double StrainValueAt(DifficultyHitObject current) currentRhythm = RhythmEvaluator.EvaluateDifficultyOf(current); double totalStrain = currentStrain * currentRhythm; - ObjectStrains.Add(totalStrain); return totalStrain; } From 2b6a47316430bd4c8f939314ec8c4b086d63dc99 Mon Sep 17 00:00:00 2001 From: Jay Lawton Date: Thu, 7 Nov 2024 21:12:04 +1000 Subject: [PATCH 060/120] update methods --- osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs b/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs index c28635ef2290..d597a0f6b2a5 100644 --- a/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs +++ b/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs @@ -29,7 +29,7 @@ public abstract class StrainSkill : Skill private double currentSectionEnd; private readonly List strainPeaks = new List(); - protected List ObjectStrains = new List(); // Store individual strains + protected readonly List ObjectStrains = new List(); // Store individual strains protected StrainSkill(Mod[] mods) : base(mods) @@ -68,7 +68,7 @@ public sealed override void Process(DifficultyHitObject current) /// Calculates the number of strains weighted against the top strain. /// The result is scaled by clock rate as it affects the total number of strains. /// - public double CountDifficultStrains() + public virtual double CountDifficultStrains() { if (ObjectStrains.Count == 0) return 0.0; From 7c3a3c4d1093b3ffcd238da5bf30fb6e8ed0d0a6 Mon Sep 17 00:00:00 2001 From: Jay Lawton Date: Thu, 7 Nov 2024 21:56:42 +1000 Subject: [PATCH 061/120] rename DifficultStrains for clarity --- osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs | 4 ++-- osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index acf01b2a838c..575e03051c75 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -48,8 +48,8 @@ protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beat double sliderFactor = aimRating > 0 ? aimRatingNoSliders / aimRating : 1; - double aimDifficultyStrainCount = ((OsuStrainSkill)skills[0]).CountDifficultStrains(); - double speedDifficultyStrainCount = ((OsuStrainSkill)skills[2]).CountDifficultStrains(); + double aimDifficultyStrainCount = ((OsuStrainSkill)skills[0]).CountTopWeightedStrains(); + double speedDifficultyStrainCount = ((OsuStrainSkill)skills[2]).CountTopWeightedStrains(); if (mods.Any(m => m is OsuModTouchDevice)) { diff --git a/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs b/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs index d597a0f6b2a5..1cb6b69f91e8 100644 --- a/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs +++ b/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs @@ -68,7 +68,7 @@ public sealed override void Process(DifficultyHitObject current) /// Calculates the number of strains weighted against the top strain. /// The result is scaled by clock rate as it affects the total number of strains. /// - public virtual double CountDifficultStrains() + public virtual double CountTopWeightedStrains() { if (ObjectStrains.Count == 0) return 0.0; From 78c97d2cd7af2d7b05dfc2f5a6ba45fefa4dc72d Mon Sep 17 00:00:00 2001 From: StanR Date: Thu, 7 Nov 2024 20:36:00 +0500 Subject: [PATCH 062/120] Add `DifficultyCalculationUtils` --- .../Difficulty/Skills/Strain.cs | 3 +- .../Difficulty/Evaluators/AimEvaluator.cs | 12 +++-- .../Difficulty/Evaluators/RhythmEvaluator.cs | 5 +- .../Difficulty/Evaluators/SpeedEvaluator.cs | 9 ++-- .../Preprocessing/OsuDifficultyHitObject.cs | 1 + .../Difficulty/Evaluators/ColourEvaluator.cs | 21 ++------ .../Utils/DifficultyCalculationUtils.cs | 50 +++++++++++++++++++ 7 files changed, 72 insertions(+), 29 deletions(-) create mode 100644 osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs diff --git a/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs b/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs index a24fcaad8d44..bb4261ea1342 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs @@ -5,6 +5,7 @@ using osu.Framework.Utils; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; +using osu.Game.Rulesets.Difficulty.Utils; using osu.Game.Rulesets.Mania.Difficulty.Preprocessing; using osu.Game.Rulesets.Mods; @@ -73,7 +74,7 @@ protected override double StrainValueOf(DifficultyHitObject current) // 0.0 +--------+-+---------------> Release Difference / ms // release_threshold if (isOverlapping) - holdAddition = 1 / (1 + Math.Exp(0.27 * (release_threshold - closestEndTime))); + holdAddition = DifficultyCalculationUtils.Logistic(x: closestEndTime, multiplier: 0.27, midpointOffset: release_threshold); // Decay and increase individualStrains in own column individualStrains[column] = applyDecay(individualStrains[column], startTime - startTimes[column], individual_decay_base); diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs index 3d1939acac5f..9816f6d0a412 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs @@ -3,6 +3,7 @@ using System; using osu.Game.Rulesets.Difficulty.Preprocessing; +using osu.Game.Rulesets.Difficulty.Utils; using osu.Game.Rulesets.Osu.Difficulty.Preprocessing; using osu.Game.Rulesets.Osu.Objects; @@ -33,6 +34,9 @@ public static double EvaluateDifficultyOf(DifficultyHitObject current, bool with var osuLastObj = (OsuDifficultyHitObject)current.Previous(0); var osuLastLastObj = (OsuDifficultyHitObject)current.Previous(1); + const int radius = OsuDifficultyHitObject.NORMALISED_RADIUS; + const int diameter = OsuDifficultyHitObject.NORMALISED_DIAMETER; + // Calculate the velocity to the current hitobject, which starts with a base distance / time assuming the last object is a hitcircle. double currVelocity = osuCurrObj.LazyJumpDistance / osuCurrObj.StrainTime; @@ -77,14 +81,14 @@ public static double EvaluateDifficultyOf(DifficultyHitObject current, bool with wideAngleBonus = calcWideAngleBonus(currAngle); acuteAngleBonus = calcAcuteAngleBonus(currAngle); - if (osuCurrObj.StrainTime > 100) // Only buff deltaTime exceeding 300 bpm 1/2. + if (DifficultyCalculationUtils.MillisecondsToBPM(osuCurrObj.StrainTime, 2) < 300) // Only buff deltaTime exceeding 300 bpm 1/2. acuteAngleBonus = 0; else { acuteAngleBonus *= calcAcuteAngleBonus(lastAngle) // Multiply by previous angle, we don't want to buff unless this is a wiggle type pattern. - * Math.Min(angleBonus, 125 / osuCurrObj.StrainTime) // The maximum velocity we buff is equal to 125 / strainTime + * Math.Min(angleBonus, diameter * 1.25 / osuCurrObj.StrainTime) // The maximum velocity we buff is equal to 125 / strainTime * Math.Pow(Math.Sin(Math.PI / 2 * Math.Min(1, (100 - osuCurrObj.StrainTime) / 25)), 2) // scale buff from 150 bpm 1/4 to 200 bpm 1/4 - * Math.Pow(Math.Sin(Math.PI / 2 * (Math.Clamp(osuCurrObj.LazyJumpDistance, 50, 100) - 50) / 50), 2); // Buff distance exceeding 50 (radius) up to 100 (diameter). + * Math.Pow(Math.Sin(Math.PI / 2 * (Math.Clamp(osuCurrObj.LazyJumpDistance, radius, diameter) - radius) / radius), 2); // Buff distance exceeding radius up to diameter. } // Penalize wide angles if they're repeated, reducing the penalty as the lastAngle gets more acute. @@ -104,7 +108,7 @@ public static double EvaluateDifficultyOf(DifficultyHitObject current, bool with double distRatio = Math.Pow(Math.Sin(Math.PI / 2 * Math.Abs(prevVelocity - currVelocity) / Math.Max(prevVelocity, currVelocity)), 2); // Reward for % distance up to 125 / strainTime for overlaps where velocity is still changing. - double overlapVelocityBuff = Math.Min(125 / Math.Min(osuCurrObj.StrainTime, osuLastObj.StrainTime), Math.Abs(prevVelocity - currVelocity)); + double overlapVelocityBuff = Math.Min(diameter * 1.25 / Math.Min(osuCurrObj.StrainTime, osuLastObj.StrainTime), Math.Abs(prevVelocity - currVelocity)); velocityChangeBonus = overlapVelocityBuff * distRatio; diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs index d10d2c5c05a4..d503dd2bcc0a 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using osu.Game.Rulesets.Difficulty.Preprocessing; +using osu.Game.Rulesets.Difficulty.Utils; using osu.Game.Rulesets.Osu.Difficulty.Preprocessing; using osu.Game.Rulesets.Osu.Objects; @@ -120,7 +121,7 @@ public static double EvaluateDifficultyOf(DifficultyHitObject current) islandCount.Count++; // repeated island (ex: triplet -> triplet) - double power = logistic(island.Delta, 2.75, 0.24, 14); + double power = DifficultyCalculationUtils.Logistic(island.Delta, maxValue: 2.75, multiplier: 0.24, midpointOffset: 58.33); effectiveRatio *= Math.Min(3.0 / islandCount.Count, Math.Pow(1.0 / islandCount.Count, power)); islandCounts[countIndex] = (islandCount.Island, islandCount.Count); @@ -172,8 +173,6 @@ public static double EvaluateDifficultyOf(DifficultyHitObject current) return Math.Sqrt(4 + rhythmComplexitySum * rhythm_overall_multiplier) / 2.0; // produces multiplier that can be applied to strain. range [1, infinity) (not really though) } - private static double logistic(double x, double maxValue, double multiplier, double offset) => (maxValue / (1 + Math.Pow(Math.E, offset - (multiplier * x)))); - private class Island : IEquatable { private readonly double deltaDifferenceEpsilon; diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs index c220352ee098..a5f6468f1779 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs @@ -3,6 +3,7 @@ using System; using osu.Game.Rulesets.Difficulty.Preprocessing; +using osu.Game.Rulesets.Difficulty.Utils; using osu.Game.Rulesets.Osu.Difficulty.Preprocessing; using osu.Game.Rulesets.Osu.Objects; @@ -10,8 +11,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators { public static class SpeedEvaluator { - private const double single_spacing_threshold = 125; // 1.25 circles distance between centers - private const double min_speed_bonus = 75; // ~200BPM + private const double single_spacing_threshold = OsuDifficultyHitObject.NORMALISED_DIAMETER * 1.25; // 1.25 circles distance between centers + private const double min_speed_bonus = 200; // 200 BPM 1/4th private const double speed_balancing_factor = 40; private const double distance_multiplier = 0.94; @@ -43,8 +44,8 @@ public static double EvaluateDifficultyOf(DifficultyHitObject current) double speedBonus = 0.0; // Add additional scaling bonus for streams/bursts higher than 200bpm - if (strainTime < min_speed_bonus) - speedBonus = 0.75 * Math.Pow((min_speed_bonus - strainTime) / speed_balancing_factor, 2); + if (DifficultyCalculationUtils.MillisecondsToBPM(strainTime) > min_speed_bonus) + speedBonus = 0.75 * Math.Pow((DifficultyCalculationUtils.BPMToMilliseconds(min_speed_bonus) - strainTime) / speed_balancing_factor, 2); double travelDistance = osuPrevObj?.TravelDistance ?? 0; double distance = travelDistance + osuCurrObj.MinimumJumpDistance; diff --git a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs index 3eaf500ad7ab..46d8c63751fe 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs @@ -19,6 +19,7 @@ public class OsuDifficultyHitObject : DifficultyHitObject /// A distance by which all distances should be scaled in order to assume a uniform circle size. /// public const int NORMALISED_RADIUS = 50; // Change radius to 50 to make 100 the diameter. Easier for mental maths. + public const int NORMALISED_DIAMETER = NORMALISED_RADIUS * 2; public const int MIN_DELTA_TIME = 25; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs index 9f63e8486764..25428c8b2fd3 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs @@ -3,6 +3,7 @@ using System; using osu.Game.Rulesets.Difficulty.Preprocessing; +using osu.Game.Rulesets.Difficulty.Utils; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour.Data; @@ -11,26 +12,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators { public class ColourEvaluator { - /// - /// A sigmoid function. It gives a value between (middle - height/2) and (middle + height/2). - /// - /// The input value. - /// The center of the sigmoid, where the largest gradient occurs and value is equal to middle. - /// The radius of the sigmoid, outside of which values are near the minimum/maximum. - /// The middle of the sigmoid output. - /// The height of the sigmoid output. This will be equal to max value - min value. - private static double sigmoid(double val, double center, double width, double middle, double height) - { - double sigmoid = Math.Tanh(Math.E * -(val - center) / width); - return sigmoid * (height / 2) + middle; - } - /// /// Evaluate the difficulty of the first note of a . /// public static double EvaluateDifficultyOf(MonoStreak monoStreak) { - return sigmoid(monoStreak.Index, 2, 2, 0.5, 1) * EvaluateDifficultyOf(monoStreak.Parent) * 0.5; + return DifficultyCalculationUtils.Logistic(exponent: Math.E * monoStreak.Index - 2 * Math.E) * EvaluateDifficultyOf(monoStreak.Parent) * 0.5; } /// @@ -38,7 +25,7 @@ public static double EvaluateDifficultyOf(MonoStreak monoStreak) /// public static double EvaluateDifficultyOf(AlternatingMonoPattern alternatingMonoPattern) { - return sigmoid(alternatingMonoPattern.Index, 2, 2, 0.5, 1) * EvaluateDifficultyOf(alternatingMonoPattern.Parent); + return DifficultyCalculationUtils.Logistic(exponent: Math.E * alternatingMonoPattern.Index - 2 * Math.E) * EvaluateDifficultyOf(alternatingMonoPattern.Parent); } /// @@ -46,7 +33,7 @@ public static double EvaluateDifficultyOf(AlternatingMonoPattern alternatingMono /// public static double EvaluateDifficultyOf(RepeatingHitPatterns repeatingHitPattern) { - return 2 * (1 - sigmoid(repeatingHitPattern.RepetitionInterval, 2, 2, 0.5, 1)); + return 2 * (1 - DifficultyCalculationUtils.Logistic(exponent: Math.E * repeatingHitPattern.RepetitionInterval - 2 * Math.E)); } public static double EvaluateDifficultyOf(DifficultyHitObject hitObject) diff --git a/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs b/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs new file mode 100644 index 000000000000..ac7bf7a04e0b --- /dev/null +++ b/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs @@ -0,0 +1,50 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; + +namespace osu.Game.Rulesets.Difficulty.Utils +{ + public static class DifficultyCalculationUtils + { + /// + /// Converts BPM value into milliseconds + /// + /// Beats per minute + /// Which rhythm delimiter to use, default is 1/4 + /// BPM conveted to milliseconds + public static double BPMToMilliseconds(double bpm, int delimiter = 4) + { + return 60000.0 / delimiter / bpm; + } + + /// + /// Converts milliseconds value into a BPM value + /// + /// Milliseconds + /// Which rhythm delimiter to use, default is 1/4 + /// Milliseconds conveted to beats per minute + public static double MillisecondsToBPM(double ms, int delimiter = 4) + { + return 60000.0 / (ms * delimiter); + } + + /// + /// Calculates a S-shaped logistic function (https://en.wikipedia.org/wiki/Logistic_function) + /// + /// Value to calculate the function for + /// Maximum value returnable by the function + /// Growth rate of the function + /// How much the function midpoint is offset from zero + /// The output of logistic function of + public static double Logistic(double x, double maxValue = 1, double multiplier = 1, double midpointOffset = 0) => maxValue / (1 + Math.Exp(multiplier * (midpointOffset - x))); + + /// + /// Calculates a S-shaped logistic function (https://en.wikipedia.org/wiki/Logistic_function) + /// + /// Maximum value returnable by the function + /// Exponent + /// The output of logistic function + public static double Logistic(double exponent, double maxValue = 1) => maxValue / (1 + Math.Exp(exponent)); + } +} From c9d3b6557d37d6991971925ee7dc4358a5734495 Mon Sep 17 00:00:00 2001 From: StanR Date: Thu, 7 Nov 2024 21:23:26 +0500 Subject: [PATCH 063/120] Fix code issues --- .../Difficulty/Preprocessing/OsuDifficultyHitObject.cs | 1 + .../Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs index 46d8c63751fe..5e4c5c1ee9fa 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs @@ -19,6 +19,7 @@ public class OsuDifficultyHitObject : DifficultyHitObject /// A distance by which all distances should be scaled in order to assume a uniform circle size. /// public const int NORMALISED_RADIUS = 50; // Change radius to 50 to make 100 the diameter. Easier for mental maths. + public const int NORMALISED_DIAMETER = NORMALISED_RADIUS * 2; public const int MIN_DELTA_TIME = 25; diff --git a/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs b/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs index ac7bf7a04e0b..b9efcd683d1c 100644 --- a/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs +++ b/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs @@ -37,7 +37,7 @@ public static double MillisecondsToBPM(double ms, int delimiter = 4) /// Growth rate of the function /// How much the function midpoint is offset from zero /// The output of logistic function of - public static double Logistic(double x, double maxValue = 1, double multiplier = 1, double midpointOffset = 0) => maxValue / (1 + Math.Exp(multiplier * (midpointOffset - x))); + public static double Logistic(double x, double midpointOffset, double multiplier, double maxValue = 1) => maxValue / (1 + Math.Exp(multiplier * (midpointOffset - x))); /// /// Calculates a S-shaped logistic function (https://en.wikipedia.org/wiki/Logistic_function) From 233560b8679ce0c69e1b32b41e631ccb7afc359a Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 7 Nov 2024 18:15:54 +0900 Subject: [PATCH 064/120] Fix CI test report workflow --- .github/workflows/report-nunit.yml | 37 ++++++++++++++++++------------ 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/.github/workflows/report-nunit.yml b/.github/workflows/report-nunit.yml index c44f46d70a60..14f0208fc8b1 100644 --- a/.github/workflows/report-nunit.yml +++ b/.github/workflows/report-nunit.yml @@ -5,33 +5,40 @@ name: Annotate CI run with test results on: workflow_run: - workflows: ["Continuous Integration"] + workflows: [ "Continuous Integration" ] types: - completed -permissions: {} + +permissions: + contents: read + actions: read + checks: write + jobs: annotate: - permissions: - checks: write # to create checks (dorny/test-reporter) - name: Annotate CI run with test results runs-on: ubuntu-latest if: ${{ github.event.workflow_run.conclusion != 'cancelled' }} - strategy: - fail-fast: false - matrix: - os: - - { prettyname: Windows } - - { prettyname: macOS } - - { prettyname: Linux } - threadingMode: ['SingleThread', 'MultiThreaded'] timeout-minutes: 5 steps: + - name: Checkout + uses: actions/checkout@v4 + with: + repository: ${{ github.event.workflow_run.repository.full_name }} + ref: ${{ github.event.workflow_run.head_sha }} + + - name: Download results + uses: actions/download-artifact@v4 + with: + pattern: osu-test-results-* + merge-multiple: true + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ github.token }} + - name: Annotate CI run with test results uses: dorny/test-reporter@v1.8.0 with: - artifact: osu-test-results-${{matrix.os.prettyname}}-${{matrix.threadingMode}} - name: Test Results (${{matrix.os.prettyname}}, ${{matrix.threadingMode}}) + name: Results path: "*.trx" reporter: dotnet-trx list-suites: 'failed' From 70be82b0488924f0a1d73c15138c95b1639882aa Mon Sep 17 00:00:00 2001 From: finadoggie <75299710+Finadoggie@users.noreply.github.com> Date: Thu, 7 Nov 2024 22:42:41 -0800 Subject: [PATCH 065/120] Clamp estimateImproperlyFollowedDifficultSliders for lazer scores --- osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index ddabf866ff3f..9687944817a5 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -178,7 +178,7 @@ private double computeAimValue(ScoreInfo score, OsuDifficultyAttributes attribut { // We add tick misses here since they too mean that the player didn't follow the slider properly // We however aren't adding misses here because missing slider heads has a harsh penalty by itself and doesn't mean that the rest of the slider wasn't followed properly - estimateImproperlyFollowedDifficultSliders = Math.Min(countSliderEndsDropped + countSliderTickMiss, estimateDifficultSliders); + estimateImproperlyFollowedDifficultSliders = Math.Clamp(countSliderEndsDropped + countSliderTickMiss, 0, estimateDifficultSliders); } double sliderNerfFactor = (1 - attributes.SliderFactor) * Math.Pow(1 - estimateImproperlyFollowedDifficultSliders / estimateDifficultSliders, 3) + attributes.SliderFactor; From 091d02b3a8a41a11dea2feff5f384d613dc39f92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 8 Nov 2024 09:22:20 +0100 Subject: [PATCH 066/120] Fix retry button on storage unavailable dialog not reopening realm if retry succeeds Related: https://github.com/ppy/osu/issues/30539 When starting up the game with a data location that points to an unavailable external device, a new realm file is created in the default location. Eventually a popup is shown that informs the user that the external storage is unavailable, and the user has an option to try the storage again. The button that invokes said option would check said storage correctly, but would not do anything about realm, which means the previously opened empty realm that is placed in the default location would remain open, which means the retry essentially doesn't work because the user's stuff isn't there after the retry. To fix this, take out a `BlockAllOperations()`, which will flush all open realms, and re-open the realm on the external location if the custom storage restore succeeds. --- osu.Game/Screens/Menu/StorageErrorDialog.cs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Menu/StorageErrorDialog.cs b/osu.Game/Screens/Menu/StorageErrorDialog.cs index b48046d1900d..677a3b02780b 100644 --- a/osu.Game/Screens/Menu/StorageErrorDialog.cs +++ b/osu.Game/Screens/Menu/StorageErrorDialog.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Graphics.Sprites; +using osu.Game.Database; using osu.Game.IO; using osu.Game.Localisation; using osu.Game.Overlays; @@ -16,6 +17,9 @@ public partial class StorageErrorDialog : PopupDialog [Resolved] private IDialogOverlay dialogOverlay { get; set; } = null!; + [Resolved] + private RealmAccess realmAccess { get; set; } = null!; + public StorageErrorDialog(OsuStorage storage, OsuStorageError error) { HeaderText = StorageErrorDialogStrings.StorageError; @@ -35,7 +39,15 @@ public StorageErrorDialog(OsuStorage storage, OsuStorageError error) Text = StorageErrorDialogStrings.TryAgain, Action = () => { - if (!storage.TryChangeToCustomStorage(out var nextError)) + bool success; + OsuStorageError nextError; + + // blocking all operations has a side effect of closing & reopening the realm db, + // which is desirable here since the restoration of the old storage - if it succeeds - means the realm db has moved. + using (realmAccess.BlockAllOperations(@"restoration of previously unavailable storage")) + success = storage.TryChangeToCustomStorage(out nextError); + + if (!success) dialogOverlay.Push(new StorageErrorDialog(storage, nextError)); } }, From d8fc7b180359ce16b90029fe94efdc207fc47519 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 8 Nov 2024 10:00:21 +0100 Subject: [PATCH 067/120] Add extended logging to NVAPI operations To help diagnose https://github.com/ppy/osu/issues/30546. --- osu.Desktop/NVAPI.cs | 37 +++++++++++++++++++++---------------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/osu.Desktop/NVAPI.cs b/osu.Desktop/NVAPI.cs index 0b09613ba0c3..1fc04aff59dd 100644 --- a/osu.Desktop/NVAPI.cs +++ b/osu.Desktop/NVAPI.cs @@ -141,12 +141,12 @@ public static bool IsLaptop // Make sure that this is a laptop. IntPtr[] gpus = new IntPtr[64]; - if (checkError(EnumPhysicalGPUs(gpus, out int gpuCount))) + if (checkError(EnumPhysicalGPUs(gpus, out int gpuCount), nameof(EnumPhysicalGPUs))) return false; for (int i = 0; i < gpuCount; i++) { - if (checkError(GetSystemType(gpus[i], out var type))) + if (checkError(GetSystemType(gpus[i], out var type), nameof(GetSystemType))) return false; if (type == NvSystemType.LAPTOP) @@ -182,7 +182,7 @@ public static NvThreadControlSetting ThreadedOptimisations bool success = setSetting(NvSettingID.OGL_THREAD_CONTROL_ID, (uint)value); - Logger.Log(success ? $"Threaded optimizations set to \"{value}\"!" : "Threaded optimizations set failed!"); + Logger.Log(success ? $"[NVAPI] Threaded optimizations set to \"{value}\"!" : "[NVAPI] Threaded optimizations set failed!"); } } @@ -205,7 +205,7 @@ private static bool containsApplication(IntPtr profileHandle, NvProfile profile, uint numApps = profile.NumOfApps; - if (checkError(EnumApplications(sessionHandle, profileHandle, 0, ref numApps, applications))) + if (checkError(EnumApplications(sessionHandle, profileHandle, 0, ref numApps, applications), nameof(EnumApplications))) return false; for (uint i = 0; i < numApps; i++) @@ -236,10 +236,10 @@ private static bool getProfile(out IntPtr profileHandle, out NvApplication appli isApplicationSpecific = true; - if (checkError(FindApplicationByName(sessionHandle, osu_filename, out profileHandle, ref application))) + if (checkError(FindApplicationByName(sessionHandle, osu_filename, out profileHandle, ref application), nameof(FindApplicationByName))) { isApplicationSpecific = false; - if (checkError(GetCurrentGlobalProfile(sessionHandle, out profileHandle))) + if (checkError(GetCurrentGlobalProfile(sessionHandle, out profileHandle), nameof(GetCurrentGlobalProfile))) return false; } @@ -263,7 +263,7 @@ private static bool createProfile(out IntPtr profileHandle) newProfile.GPUSupport[0] = 1; - if (checkError(CreateProfile(sessionHandle, ref newProfile, out profileHandle))) + if (checkError(CreateProfile(sessionHandle, ref newProfile, out profileHandle), nameof(CreateProfile))) return false; return true; @@ -284,7 +284,7 @@ private static bool getSetting(NvSettingID settingId, IntPtr profileHandle, out SettingID = settingId }; - if (checkError(GetSetting(sessionHandle, profileHandle, settingId, ref setting))) + if (checkError(GetSetting(sessionHandle, profileHandle, settingId, ref setting), nameof(GetSetting))) return false; return true; @@ -313,7 +313,7 @@ private static bool setSetting(NvSettingID settingId, uint settingValue) }; // Set the thread state - if (checkError(SetSetting(sessionHandle, profileHandle, ref newSetting))) + if (checkError(SetSetting(sessionHandle, profileHandle, ref newSetting), nameof(SetSetting))) return false; // Get the profile (needed to check app count) @@ -321,7 +321,7 @@ private static bool setSetting(NvSettingID settingId, uint settingValue) { Version = NvProfile.Stride }; - if (checkError(GetProfileInfo(sessionHandle, profileHandle, ref profile))) + if (checkError(GetProfileInfo(sessionHandle, profileHandle, ref profile), nameof(GetProfileInfo))) return false; if (!containsApplication(profileHandle, profile, out application)) @@ -332,12 +332,12 @@ private static bool setSetting(NvSettingID settingId, uint settingValue) application.AppName = osu_filename; application.UserFriendlyName = APPLICATION_NAME; - if (checkError(CreateApplication(sessionHandle, profileHandle, ref application))) + if (checkError(CreateApplication(sessionHandle, profileHandle, ref application), nameof(CreateApplication))) return false; } // Save! - return !checkError(SaveSettings(sessionHandle)); + return !checkError(SaveSettings(sessionHandle), nameof(SaveSettings)); } /// @@ -346,20 +346,25 @@ private static bool setSetting(NvSettingID settingId, uint settingValue) /// If the operation succeeded. private static bool createSession() { - if (checkError(CreateSession(out sessionHandle))) + if (checkError(CreateSession(out sessionHandle), nameof(CreateSession))) return false; // Load settings into session - if (checkError(LoadSettings(sessionHandle))) + if (checkError(LoadSettings(sessionHandle), nameof(LoadSettings))) return false; return true; } - private static bool checkError(NvStatus status) + private static bool checkError(NvStatus status, string caller) { Status = status; - return status != NvStatus.OK; + + bool hasError = status != NvStatus.OK; + if (hasError) + Logger.Log($"[NVAPI] {caller} call failed with status code {status}"); + + return hasError; } static NVAPI() From 8d7b19d6cf75de724ace35f29c6a2b8406cbb3b8 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sat, 9 Nov 2024 15:09:20 +0900 Subject: [PATCH 068/120] Fix incorrectly translated NVAPI ABI --- osu.Desktop/NVAPI.cs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/osu.Desktop/NVAPI.cs b/osu.Desktop/NVAPI.cs index 1fc04aff59dd..fd372cddc582 100644 --- a/osu.Desktop/NVAPI.cs +++ b/osu.Desktop/NVAPI.cs @@ -258,11 +258,9 @@ private static bool createProfile(out IntPtr profileHandle) Version = NvProfile.Stride, IsPredefined = 0, ProfileName = PROFILE_NAME, - GPUSupport = new uint[32] + GpuSupport = NvDrsGpuSupport.Geforce }; - newProfile.GPUSupport[0] = 1; - if (checkError(CreateProfile(sessionHandle, ref newProfile, out profileHandle), nameof(CreateProfile))) return false; @@ -463,9 +461,7 @@ internal struct NvProfile [MarshalAs(UnmanagedType.ByValTStr, SizeConst = NVAPI.UNICODE_STRING_MAX)] public string ProfileName; - [MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)] - public uint[] GPUSupport; - + public NvDrsGpuSupport GpuSupport; public uint IsPredefined; public uint NumOfApps; public uint NumOfSettings; @@ -611,6 +607,7 @@ internal enum NvStatus SYNC_NOT_ACTIVE = -194, // The requested action cannot be performed without Sync being enabled. SYNC_MASTER_NOT_FOUND = -195, // The requested action cannot be performed without Sync Master being enabled. INVALID_SYNC_TOPOLOGY = -196, // Invalid displays passed in the NV_GSYNC_DISPLAY pointer. + ECID_SIGN_ALGO_UNSUPPORTED = -197, // The specified signing algorithm is not supported. Either an incorrect value was entered or the current installed driver/hardware does not support the input value. ECID_KEY_VERIFICATION_FAILED = -198, // The encrypted public key verification has failed. FIRMWARE_OUT_OF_DATE = -199, // The device's firmware is out of date. @@ -749,4 +746,12 @@ internal enum NvThreadControlSetting : uint OGL_THREAD_CONTROL_NUM_VALUES = 2, OGL_THREAD_CONTROL_DEFAULT = 0 } + + [Flags] + internal enum NvDrsGpuSupport : uint + { + Geforce = 1 << 0, + Quadro = 1 << 1, + Nvs = 1 << 2 + } } From 6183daa95f90decd479a0fee0310ca8c55f87e8c Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sat, 9 Nov 2024 14:54:19 +0900 Subject: [PATCH 069/120] Split diffcalc workflow to add concurrency group --- .github/workflows/_diffcalc_processor.yml | 250 ++++++++++++++++++++ .github/workflows/diffcalc.yml | 271 ++-------------------- 2 files changed, 275 insertions(+), 246 deletions(-) create mode 100644 .github/workflows/_diffcalc_processor.yml diff --git a/.github/workflows/_diffcalc_processor.yml b/.github/workflows/_diffcalc_processor.yml new file mode 100644 index 000000000000..74ce226240ec --- /dev/null +++ b/.github/workflows/_diffcalc_processor.yml @@ -0,0 +1,250 @@ +name: "🔒Run diffcalc runner" + +on: + workflow_call: + inputs: + id: + type: string + head-sha: + type: string + pr-url: + type: string + pr-text: + type: string + dispatch-inputs: + type: string + outputs: + target: + description: The comparison target. + value: ${{ jobs.generator.outputs.target }} + sheet: + description: The comparison spreadsheet. + value: ${{ jobs.generator.outputs.sheet }} + secrets: + DIFFCALC_GOOGLE_CREDENTIALS: + required: true + +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: false + +env: + GENERATOR_DIR: ${{ github.workspace }}/${{ inputs.id }} + GENERATOR_ENV: ${{ github.workspace }}/${{ inputs.id }}/.env + +jobs: + environment: + name: Setup environment + runs-on: self-hosted + steps: + - name: Checkout diffcalc-sheet-generator + uses: actions/checkout@v4 + with: + path: ${{ inputs.id }} + repository: 'smoogipoo/diffcalc-sheet-generator' + + - name: Add base environment + env: + GOOGLE_CREDS_FILE: ${{ github.workspace }}/${{ inputs.id }}/google-credentials.json + VARS_JSON: ${{ (vars != null && toJSON(vars)) || '' }} + run: | + # Required by diffcalc-sheet-generator + cp '${{ env.GENERATOR_DIR }}/.env.sample' "${{ env.GENERATOR_ENV }}" + + # Add Google credentials + echo '${{ secrets.DIFFCALC_GOOGLE_CREDENTIALS }}' | base64 -d > "${{ env.GOOGLE_CREDS_FILE }}" + + # Add repository variables + echo "${VARS_JSON}" | jq -c '. | to_entries | .[]' | while read -r line; do + opt=$(jq -r '.key' <<< ${line}) + val=$(jq -r '.value' <<< ${line}) + + if [[ "${opt}" =~ ^DIFFCALC_ ]]; then + optNoPrefix=$(echo "${opt}" | cut -d '_' -f2-) + sed -i "s;^${optNoPrefix}=.*$;${optNoPrefix}=${val};" "${{ env.GENERATOR_ENV }}" + fi + done + + - name: Add HEAD environment + run: | + sed -i "s;^OSU_A=.*$;OSU_A=${{ inputs.head-sha }};" "${{ env.GENERATOR_ENV }}" + + - name: Add pull-request environment + if: ${{ inputs.pr-url != '' }} + run: | + sed -i "s;^OSU_B=.*$;OSU_B=${{ inputs.pr-url }};" "${{ env.GENERATOR_ENV }}" + + - name: Add comment environment + if: ${{ inputs.pr-text != '' }} + env: + PR_TEXT: ${{ inputs.pr-text }} + run: | + # Add comment environment + echo "${PR_TEXT}" | sed -r 's/\r$//' | grep -E '^\w+=' | while read -r line; do + opt=$(echo "${line}" | cut -d '=' -f1) + sed -i "s;^${opt}=.*$;${line};" "${{ env.GENERATOR_ENV }}" + done + + - name: Add dispatch environment + if: ${{ inputs.dispatch-inputs != '' }} + env: + DISPATCH_INPUTS_JSON: ${{ inputs.dispatch-inputs }} + run: | + function get_input() { + echo "${DISPATCH_INPUTS_JSON}" | jq -r ".\"$1\"" + } + + osu_a=$(get_input 'osu-a') + osu_b=$(get_input 'osu-b') + ruleset=$(get_input 'ruleset') + generators=$(get_input 'generators') + difficulty_calculator_a=$(get_input 'difficulty-calculator-a') + difficulty_calculator_b=$(get_input 'difficulty-calculator-b') + score_processor_a=$(get_input 'score-processor-a') + score_processor_b=$(get_input 'score-processor-b') + converts=$(get_input 'converts') + ranked_only=$(get_input 'ranked-only') + + sed -i "s;^OSU_B=.*$;OSU_B=${osu_b};" "${{ env.GENERATOR_ENV }}" + sed -i "s/^RULESET=.*$/RULESET=${ruleset}/" "${{ env.GENERATOR_ENV }}" + sed -i "s/^GENERATORS=.*$/GENERATORS=${generators}/" "${{ env.GENERATOR_ENV }}" + + if [[ "${osu_a}" != 'latest' ]]; then + sed -i "s;^OSU_A=.*$;OSU_A=${osu_a};" "${{ env.GENERATOR_ENV }}" + fi + + if [[ "${difficulty_calculator_a}" != 'latest' ]]; then + sed -i "s;^DIFFICULTY_CALCULATOR_A=.*$;DIFFICULTY_CALCULATOR_A=${difficulty_calculator_a};" "${{ env.GENERATOR_ENV }}" + fi + + if [[ "${difficulty_calculator_b}" != 'latest' ]]; then + sed -i "s;^DIFFICULTY_CALCULATOR_B=.*$;DIFFICULTY_CALCULATOR_B=${difficulty_calculator_b};" "${{ env.GENERATOR_ENV }}" + fi + + if [[ "${score_processor_a}" != 'latest' ]]; then + sed -i "s;^SCORE_PROCESSOR_A=.*$;SCORE_PROCESSOR_A=${score_processor_a};" "${{ env.GENERATOR_ENV }}" + fi + + if [[ "${score_processor_b}" != 'latest' ]]; then + sed -i "s;^SCORE_PROCESSOR_B=.*$;SCORE_PROCESSOR_B=${score_processor_b};" "${{ env.GENERATOR_ENV }}" + fi + + if [[ "${converts}" == 'true' ]]; then + sed -i 's/^NO_CONVERTS=.*$/NO_CONVERTS=0/' "${{ env.GENERATOR_ENV }}" + else + sed -i 's/^NO_CONVERTS=.*$/NO_CONVERTS=1/' "${{ env.GENERATOR_ENV }}" + fi + + if [[ "${ranked_only}" == 'true' ]]; then + sed -i 's/^RANKED_ONLY=.*$/RANKED_ONLY=1/' "${{ env.GENERATOR_ENV }}" + else + sed -i 's/^RANKED_ONLY=.*$/RANKED_ONLY=0/' "${{ env.GENERATOR_ENV }}" + fi + + scores: + name: Setup scores + needs: environment + runs-on: self-hosted + steps: + - name: Query latest data + id: query + run: | + ruleset=$(cat ${{ env.GENERATOR_ENV }} | grep -E '^RULESET=' | cut -d '=' -f2-) + performance_data_name=$(curl -s "https://data.ppy.sh/" | grep "performance_${ruleset}_top_1000\b" | tail -1 | awk -F "'" '{print $2}' | sed 's/\.tar\.bz2//g') + + echo "TARGET_DIR=${{ env.GENERATOR_DIR }}/sql/${ruleset}" >> "${GITHUB_OUTPUT}" + echo "DATA_NAME=${performance_data_name}" >> "${GITHUB_OUTPUT}" + echo "DATA_PKG=${performance_data_name}.tar.bz2" >> "${GITHUB_OUTPUT}" + + - name: Restore cache + id: restore-cache + uses: maxnowack/local-cache@720e69c948191660a90aa1cf6a42fc4d2dacdf30 # v2 + with: + path: ${{ steps.query.outputs.DATA_PKG }} + key: ${{ steps.query.outputs.DATA_NAME }} + + - name: Download + if: steps.restore-cache.outputs.cache-hit != 'true' + run: | + wget -q -O "${{ steps.query.outputs.DATA_PKG }}" "https://data.ppy.sh/${{ steps.query.outputs.DATA_PKG }}" + + - name: Extract + run: | + tar -I lbzip2 -xf "${{ steps.query.outputs.DATA_PKG }}" + rm -r "${{ steps.query.outputs.TARGET_DIR }}" + mv "${{ steps.query.outputs.DATA_NAME }}" "${{ steps.query.outputs.TARGET_DIR }}" + + beatmaps: + name: Setup beatmaps + needs: environment + runs-on: self-hosted + steps: + - name: Query latest data + id: query + run: | + beatmaps_data_name=$(curl -s "https://data.ppy.sh/" | grep "osu_files" | tail -1 | awk -F "'" '{print $2}' | sed 's/\.tar\.bz2//g') + + echo "TARGET_DIR=${{ env.GENERATOR_DIR }}/beatmaps" >> "${GITHUB_OUTPUT}" + echo "DATA_NAME=${beatmaps_data_name}" >> "${GITHUB_OUTPUT}" + echo "DATA_PKG=${beatmaps_data_name}.tar.bz2" >> "${GITHUB_OUTPUT}" + + - name: Restore cache + id: restore-cache + uses: maxnowack/local-cache@720e69c948191660a90aa1cf6a42fc4d2dacdf30 # v2 + with: + path: ${{ steps.query.outputs.DATA_PKG }} + key: ${{ steps.query.outputs.DATA_NAME }} + + - name: Download + if: steps.restore-cache.outputs.cache-hit != 'true' + run: | + wget -q -O "${{ steps.query.outputs.DATA_PKG }}" "https://data.ppy.sh/${{ steps.query.outputs.DATA_PKG }}" + + - name: Extract + run: | + tar -I lbzip2 -xf "${{ steps.query.outputs.DATA_PKG }}" + rm -r "${{ steps.query.outputs.TARGET_DIR }}" + mv "${{ steps.query.outputs.DATA_NAME }}" "${{ steps.query.outputs.TARGET_DIR }}" + + generator: + name: Run generator + needs: [ environment, scores, beatmaps ] + runs-on: self-hosted + timeout-minutes: 720 + outputs: + target: ${{ steps.run.outputs.target }} + sheet: ${{ steps.run.outputs.sheet }} + steps: + - name: Run + id: run + run: | + # Add the GitHub token. This needs to be done here because it's unique per-job. + sed -i 's/^GH_TOKEN=.*$/GH_TOKEN=${{ github.token }}/' "${{ env.GENERATOR_ENV }}" + + cd "${{ env.GENERATOR_DIR }}" + + docker compose up --build --detach + docker compose logs --follow & + docker compose wait generator + + link=$(docker compose logs --tail 10 generator | grep 'http' | sed -E 's/^.*(http.*)$/\1/') + target=$(cat "${{ env.GENERATOR_ENV }}" | grep -E '^OSU_B=' | cut -d '=' -f2-) + + echo "target=${target}" >> "${GITHUB_OUTPUT}" + echo "sheet=${link}" >> "${GITHUB_OUTPUT}" + + - name: Shutdown + if: ${{ always() }} + run: | + cd "${{ env.GENERATOR_DIR }}" + docker compose down --volumes + + cleanup: + name: Cleanup + needs: [ environment, scores, beatmaps, generator ] + runs-on: self-hosted + if: ${{ always() }} + steps: + - name: Cleanup + run: | + rm -rf "${{ needs.directory.outputs.GENERATOR_DIR }}" diff --git a/.github/workflows/diffcalc.yml b/.github/workflows/diffcalc.yml index 15fbcf38e4e7..d494bbcb48ad 100644 --- a/.github/workflows/diffcalc.yml +++ b/.github/workflows/diffcalc.yml @@ -104,26 +104,6 @@ env: EXECUTION_ID: execution-${{ github.run_id }}-${{ github.run_number }}-${{ github.run_attempt }} jobs: - master-environment: - name: Save master environment - runs-on: ubuntu-latest - if: ${{ github.event_name == 'workflow_dispatch' || contains(github.event.comment.body, '!diffcalc') }} - outputs: - HEAD: ${{ steps.get-head.outputs.HEAD }} - steps: - - name: Checkout osu - uses: actions/checkout@v4 - with: - ref: master - sparse-checkout: | - README.md - - - name: Get HEAD ref - id: get-head - run: | - ref=$(git log -1 --format='%H') - echo "HEAD=https://github.com/${{ github.repository }}/commit/${ref}" >> "${GITHUB_OUTPUT}" - check-permissions: name: Check permissions runs-on: ubuntu-latest @@ -139,9 +119,23 @@ jobs: done exit 1 + run-diffcalc: + name: Run spreadsheet generator + needs: check-permissions + uses: ./.github/workflows/_diffcalc_processor.yml + with: + # Can't reference env... Why GitHub, WHY? + id: execution-${{ github.run_id }}-${{ github.run_number }}-${{ github.run_attempt }} + head-sha: https://github.com/${{ github.repository }}/commit/${{ github.event.pull_request.head.sha || github.sha }} + pr-url: ${{ github.event.issue.pull_request.html_url || '' }} + pr-text: ${{ github.event.comment.body || '' }} + dispatch-inputs: ${{ toJSON(inputs) }} + secrets: + DIFFCALC_GOOGLE_CREDENTIALS: ${{ secrets.DIFFCALC_GOOGLE_CREDENTIALS }} + create-comment: name: Create PR comment - needs: [ master-environment, check-permissions ] + needs: check-permissions runs-on: ubuntu-latest if: ${{ github.event_name == 'issue_comment' && github.event.issue.pull_request }} steps: @@ -154,249 +148,34 @@ jobs: *This comment will update on completion* - directory: - name: Prepare directory - needs: check-permissions - runs-on: self-hosted - outputs: - GENERATOR_DIR: ${{ steps.set-outputs.outputs.GENERATOR_DIR }} - GENERATOR_ENV: ${{ steps.set-outputs.outputs.GENERATOR_ENV }} - GOOGLE_CREDS_FILE: ${{ steps.set-outputs.outputs.GOOGLE_CREDS_FILE }} - steps: - - name: Checkout diffcalc-sheet-generator - uses: actions/checkout@v4 - with: - path: ${{ env.EXECUTION_ID }} - repository: 'smoogipoo/diffcalc-sheet-generator' - - - name: Set outputs - id: set-outputs - run: | - echo "GENERATOR_DIR=${{ github.workspace }}/${{ env.EXECUTION_ID }}" >> "${GITHUB_OUTPUT}" - echo "GENERATOR_ENV=${{ github.workspace }}/${{ env.EXECUTION_ID }}/.env" >> "${GITHUB_OUTPUT}" - echo "GOOGLE_CREDS_FILE=${{ github.workspace }}/${{ env.EXECUTION_ID }}/google-credentials.json" >> "${GITHUB_OUTPUT}" - - environment: - name: Setup environment - needs: [ master-environment, directory ] - runs-on: self-hosted - env: - VARS_JSON: ${{ toJSON(vars) }} - steps: - - name: Add base environment - run: | - # Required by diffcalc-sheet-generator - cp '${{ needs.directory.outputs.GENERATOR_DIR }}/.env.sample' "${{ needs.directory.outputs.GENERATOR_ENV }}" - - # Add Google credentials - echo '${{ secrets.DIFFCALC_GOOGLE_CREDENTIALS }}' | base64 -d > "${{ needs.directory.outputs.GOOGLE_CREDS_FILE }}" - - # Add repository variables - echo "${VARS_JSON}" | jq -c '. | to_entries | .[]' | while read -r line; do - opt=$(jq -r '.key' <<< ${line}) - val=$(jq -r '.value' <<< ${line}) - - if [[ "${opt}" =~ ^DIFFCALC_ ]]; then - optNoPrefix=$(echo "${opt}" | cut -d '_' -f2-) - sed -i "s;^${optNoPrefix}=.*$;${optNoPrefix}=${val};" "${{ needs.directory.outputs.GENERATOR_ENV }}" - fi - done - - - name: Add master environment - run: | - sed -i "s;^OSU_A=.*$;OSU_A=${{ needs.master-environment.outputs.HEAD }};" "${{ needs.directory.outputs.GENERATOR_ENV }}" - - - name: Add pull-request environment - if: ${{ github.event_name == 'issue_comment' && github.event.issue.pull_request }} - run: | - sed -i "s;^OSU_B=.*$;OSU_B=${{ github.event.issue.pull_request.html_url }};" "${{ needs.directory.outputs.GENERATOR_ENV }}" - - - name: Add comment environment - if: ${{ github.event_name == 'issue_comment' }} - env: - COMMENT_BODY: ${{ github.event.comment.body }} - run: | - # Add comment environment - echo "$COMMENT_BODY" | sed -r 's/\r$//' | grep -E '^\w+=' | while read -r line; do - opt=$(echo "${line}" | cut -d '=' -f1) - sed -i "s;^${opt}=.*$;${line};" "${{ needs.directory.outputs.GENERATOR_ENV }}" - done - - - name: Add dispatch environment - if: ${{ github.event_name == 'workflow_dispatch' }} - run: | - sed -i 's;^OSU_B=.*$;OSU_B=${{ inputs.osu-b }};' "${{ needs.directory.outputs.GENERATOR_ENV }}" - sed -i 's/^RULESET=.*$/RULESET=${{ inputs.ruleset }}/' "${{ needs.directory.outputs.GENERATOR_ENV }}" - sed -i 's/^GENERATORS=.*$/GENERATORS=${{ inputs.generators }}/' "${{ needs.directory.outputs.GENERATOR_ENV }}" - - if [[ '${{ inputs.osu-a }}' != 'latest' ]]; then - sed -i 's;^OSU_A=.*$;OSU_A=${{ inputs.osu-a }};' "${{ needs.directory.outputs.GENERATOR_ENV }}" - fi - - if [[ '${{ inputs.difficulty-calculator-a }}' != 'latest' ]]; then - sed -i 's;^DIFFICULTY_CALCULATOR_A=.*$;DIFFICULTY_CALCULATOR_A=${{ inputs.difficulty-calculator-a }};' "${{ needs.directory.outputs.GENERATOR_ENV }}" - fi - - if [[ '${{ inputs.difficulty-calculator-b }}' != 'latest' ]]; then - sed -i 's;^DIFFICULTY_CALCULATOR_B=.*$;DIFFICULTY_CALCULATOR_B=${{ inputs.difficulty-calculator-b }};' "${{ needs.directory.outputs.GENERATOR_ENV }}" - fi - - if [[ '${{ inputs.score-processor-a }}' != 'latest' ]]; then - sed -i 's;^SCORE_PROCESSOR_A=.*$;SCORE_PROCESSOR_A=${{ inputs.score-processor-a }};' "${{ needs.directory.outputs.GENERATOR_ENV }}" - fi - - if [[ '${{ inputs.score-processor-b }}' != 'latest' ]]; then - sed -i 's;^SCORE_PROCESSOR_B=.*$;SCORE_PROCESSOR_B=${{ inputs.score-processor-b }};' "${{ needs.directory.outputs.GENERATOR_ENV }}" - fi - - if [[ '${{ inputs.converts }}' == 'true' ]]; then - sed -i 's/^NO_CONVERTS=.*$/NO_CONVERTS=0/' "${{ needs.directory.outputs.GENERATOR_ENV }}" - else - sed -i 's/^NO_CONVERTS=.*$/NO_CONVERTS=1/' "${{ needs.directory.outputs.GENERATOR_ENV }}" - fi - - if [[ '${{ inputs.ranked-only }}' == 'true' ]]; then - sed -i 's/^RANKED_ONLY=.*$/RANKED_ONLY=1/' "${{ needs.directory.outputs.GENERATOR_ENV }}" - else - sed -i 's/^RANKED_ONLY=.*$/RANKED_ONLY=0/' "${{ needs.directory.outputs.GENERATOR_ENV }}" - fi - - scores: - name: Setup scores - needs: [ directory, environment ] - runs-on: self-hosted - steps: - - name: Query latest data - id: query - run: | - ruleset=$(cat ${{ needs.directory.outputs.GENERATOR_ENV }} | grep -E '^RULESET=' | cut -d '=' -f2-) - performance_data_name=$(curl -s "https://data.ppy.sh/" | grep "performance_${ruleset}_top_1000\b" | tail -1 | awk -F "'" '{print $2}' | sed 's/\.tar\.bz2//g') - - echo "TARGET_DIR=${{ needs.directory.outputs.GENERATOR_DIR }}/sql/${ruleset}" >> "${GITHUB_OUTPUT}" - echo "DATA_NAME=${performance_data_name}" >> "${GITHUB_OUTPUT}" - echo "DATA_PKG=${performance_data_name}.tar.bz2" >> "${GITHUB_OUTPUT}" - - - name: Restore cache - id: restore-cache - uses: maxnowack/local-cache@720e69c948191660a90aa1cf6a42fc4d2dacdf30 # v2 - with: - path: ${{ steps.query.outputs.DATA_PKG }} - key: ${{ steps.query.outputs.DATA_NAME }} - - - name: Download - if: steps.restore-cache.outputs.cache-hit != 'true' - run: | - wget -q -O "${{ steps.query.outputs.DATA_PKG }}" "https://data.ppy.sh/${{ steps.query.outputs.DATA_PKG }}" - - - name: Extract - run: | - tar -I lbzip2 -xf "${{ steps.query.outputs.DATA_PKG }}" - rm -r "${{ steps.query.outputs.TARGET_DIR }}" - mv "${{ steps.query.outputs.DATA_NAME }}" "${{ steps.query.outputs.TARGET_DIR }}" - - beatmaps: - name: Setup beatmaps - needs: directory - runs-on: self-hosted - steps: - - name: Query latest data - id: query - run: | - beatmaps_data_name=$(curl -s "https://data.ppy.sh/" | grep "osu_files" | tail -1 | awk -F "'" '{print $2}' | sed 's/\.tar\.bz2//g') - - echo "TARGET_DIR=${{ needs.directory.outputs.GENERATOR_DIR }}/beatmaps" >> "${GITHUB_OUTPUT}" - echo "DATA_NAME=${beatmaps_data_name}" >> "${GITHUB_OUTPUT}" - echo "DATA_PKG=${beatmaps_data_name}.tar.bz2" >> "${GITHUB_OUTPUT}" - - - name: Restore cache - id: restore-cache - uses: maxnowack/local-cache@720e69c948191660a90aa1cf6a42fc4d2dacdf30 # v2 - with: - path: ${{ steps.query.outputs.DATA_PKG }} - key: ${{ steps.query.outputs.DATA_NAME }} - - - name: Download - if: steps.restore-cache.outputs.cache-hit != 'true' - run: | - wget -q -O "${{ steps.query.outputs.DATA_PKG }}" "https://data.ppy.sh/${{ steps.query.outputs.DATA_PKG }}" - - - name: Extract - run: | - tar -I lbzip2 -xf "${{ steps.query.outputs.DATA_PKG }}" - rm -r "${{ steps.query.outputs.TARGET_DIR }}" - mv "${{ steps.query.outputs.DATA_NAME }}" "${{ steps.query.outputs.TARGET_DIR }}" - - generator: - name: Run generator - needs: [ directory, environment, scores, beatmaps ] - runs-on: self-hosted - timeout-minutes: 720 - outputs: - TARGET: ${{ steps.run.outputs.TARGET }} - SPREADSHEET_LINK: ${{ steps.run.outputs.SPREADSHEET_LINK }} - steps: - - name: Run - id: run - run: | - # Add the GitHub token. This needs to be done here because it's unique per-job. - sed -i 's/^GH_TOKEN=.*$/GH_TOKEN=${{ github.token }}/' "${{ needs.directory.outputs.GENERATOR_ENV }}" - - cd "${{ needs.directory.outputs.GENERATOR_DIR }}" - - docker compose up --build --detach - docker compose logs --follow & - docker compose wait generator - - link=$(docker compose logs --tail 10 generator | grep 'http' | sed -E 's/^.*(http.*)$/\1/') - target=$(cat "${{ needs.directory.outputs.GENERATOR_ENV }}" | grep -E '^OSU_B=' | cut -d '=' -f2-) - - echo "TARGET=${target}" >> "${GITHUB_OUTPUT}" - echo "SPREADSHEET_LINK=${link}" >> "${GITHUB_OUTPUT}" - - - name: Shutdown - if: ${{ always() }} - run: | - cd "${{ needs.directory.outputs.GENERATOR_DIR }}" - docker compose down --volumes - output-cli: - name: Output info - needs: generator + name: Info + needs: run-diffcalc runs-on: ubuntu-latest steps: - name: Output info run: | - echo "Target: ${{ needs.generator.outputs.TARGET }}" - echo "Spreadsheet: ${{ needs.generator.outputs.SPREADSHEET_LINK }}" - - cleanup: - name: Cleanup - needs: [ directory, generator ] - if: ${{ always() && needs.directory.result == 'success' }} - runs-on: self-hosted - steps: - - name: Cleanup - run: | - rm -rf "${{ needs.directory.outputs.GENERATOR_DIR }}" + echo "Target: ${{ needs.run-diffcalc.outputs.target }}" + echo "Spreadsheet: ${{ needs.run-diffcalc.outputs.sheet }}" update-comment: name: Update PR comment - needs: [ create-comment, generator ] + needs: [ create-comment, run-diffcalc ] runs-on: ubuntu-latest if: ${{ always() && needs.create-comment.result == 'success' }} steps: - name: Update comment on success - if: ${{ needs.generator.result == 'success' }} + if: ${{ needs.run-diffcalc.result == 'success' }} uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6 # v2.5.0 with: comment_tag: ${{ env.EXECUTION_ID }} mode: recreate message: | - Target: ${{ needs.generator.outputs.TARGET }} - Spreadsheet: ${{ needs.generator.outputs.SPREADSHEET_LINK }} + Target: ${{ needs.run-diffcalc.outputs.target }} + Spreadsheet: ${{ needs.run-diffcalc.outputs.sheet }} - name: Update comment on failure - if: ${{ needs.generator.result == 'failure' }} + if: ${{ needs.run-diffcalc.result == 'failure' }} uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6 # v2.5.0 with: comment_tag: ${{ env.EXECUTION_ID }} @@ -405,7 +184,7 @@ jobs: Difficulty calculation failed: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} - name: Update comment on cancellation - if: ${{ needs.generator.result == 'cancelled' }} + if: ${{ needs.run-diffcalc.result == 'cancelled' }} uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6 # v2.5.0 with: comment_tag: ${{ env.EXECUTION_ID }} From 5e8df623d444244d6ab2603a8a66ccf234169411 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sat, 9 Nov 2024 23:49:00 +0900 Subject: [PATCH 070/120] Rename workflow --- .github/workflows/_diffcalc_processor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/_diffcalc_processor.yml b/.github/workflows/_diffcalc_processor.yml index 74ce226240ec..38ee0c45eaa5 100644 --- a/.github/workflows/_diffcalc_processor.yml +++ b/.github/workflows/_diffcalc_processor.yml @@ -1,4 +1,4 @@ -name: "🔒Run diffcalc runner" +name: "🔒diffcalc (do not use)" on: workflow_call: From 9acfb3c9008513fe094ed7b9ecdd2e06f7fcf1e6 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Sat, 9 Nov 2024 16:45:18 -0800 Subject: [PATCH 071/120] Fix break overlay grades not using localised string --- osu.Game/Screens/Play/Break/BreakInfoLine.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/Break/BreakInfoLine.cs b/osu.Game/Screens/Play/Break/BreakInfoLine.cs index df71767f82f4..79b417732a4d 100644 --- a/osu.Game/Screens/Play/Break/BreakInfoLine.cs +++ b/osu.Game/Screens/Play/Break/BreakInfoLine.cs @@ -63,7 +63,7 @@ private void load(OsuColour colours) protected virtual LocalisableString Format(T count) { if (count is Enum countEnum) - return countEnum.GetDescription(); + return countEnum.GetLocalisableDescription(); return count.ToString() ?? string.Empty; } From 27670542867a5f5a8157934f4c786d5630cee906 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 11 Nov 2024 12:47:55 +0900 Subject: [PATCH 072/120] Set `-euo pipefail` in diffcalc workflows --- .github/workflows/_diffcalc_processor.yml | 4 ++++ .github/workflows/diffcalc.yml | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/.github/workflows/_diffcalc_processor.yml b/.github/workflows/_diffcalc_processor.yml index 38ee0c45eaa5..e68449ea682e 100644 --- a/.github/workflows/_diffcalc_processor.yml +++ b/.github/workflows/_diffcalc_processor.yml @@ -32,6 +32,10 @@ env: GENERATOR_DIR: ${{ github.workspace }}/${{ inputs.id }} GENERATOR_ENV: ${{ github.workspace }}/${{ inputs.id }}/.env +defaults: + run: + shell: bash -euo pipefail {0} + jobs: environment: name: Setup environment diff --git a/.github/workflows/diffcalc.yml b/.github/workflows/diffcalc.yml index d494bbcb48ad..8b86650c710f 100644 --- a/.github/workflows/diffcalc.yml +++ b/.github/workflows/diffcalc.yml @@ -103,6 +103,10 @@ permissions: env: EXECUTION_ID: execution-${{ github.run_id }}-${{ github.run_number }}-${{ github.run_attempt }} +defaults: + run: + shell: bash -euo pipefail {0} + jobs: check-permissions: name: Check permissions From a3b8c4d127abea98c7040dbe9af01f07b49d7a26 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 11 Nov 2024 12:48:39 +0900 Subject: [PATCH 073/120] Fix wrong cleaned up directory --- .github/workflows/_diffcalc_processor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/_diffcalc_processor.yml b/.github/workflows/_diffcalc_processor.yml index e68449ea682e..b193d823175b 100644 --- a/.github/workflows/_diffcalc_processor.yml +++ b/.github/workflows/_diffcalc_processor.yml @@ -251,4 +251,4 @@ jobs: steps: - name: Cleanup run: | - rm -rf "${{ needs.directory.outputs.GENERATOR_DIR }}" + rm -rf "${{ env.GENERATOR_DIR }}" From c1686fb68718988e28ec89006132c96c5a0f83d2 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 11 Nov 2024 13:02:26 +0900 Subject: [PATCH 074/120] Don't fail grep if no matches --- .github/workflows/_diffcalc_processor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/_diffcalc_processor.yml b/.github/workflows/_diffcalc_processor.yml index b193d823175b..e08534fbff81 100644 --- a/.github/workflows/_diffcalc_processor.yml +++ b/.github/workflows/_diffcalc_processor.yml @@ -84,7 +84,7 @@ jobs: PR_TEXT: ${{ inputs.pr-text }} run: | # Add comment environment - echo "${PR_TEXT}" | sed -r 's/\r$//' | grep -E '^\w+=' | while read -r line; do + echo "${PR_TEXT}" | sed -r 's/\r$//' | { grep -E '^\w+=' || true; } | while read -r line; do opt=$(echo "${line}" | cut -d '=' -f1) sed -i "s;^${opt}=.*$;${line};" "${{ env.GENERATOR_ENV }}" done From 394ff88a629ee6b1e63da54032c8b514c7e4d5a3 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 11 Nov 2024 13:11:21 +0900 Subject: [PATCH 075/120] Fix empty JSON sent on non-`workflow_dispatch` events --- .github/workflows/diffcalc.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/diffcalc.yml b/.github/workflows/diffcalc.yml index 8b86650c710f..4297a88e89a0 100644 --- a/.github/workflows/diffcalc.yml +++ b/.github/workflows/diffcalc.yml @@ -133,7 +133,7 @@ jobs: head-sha: https://github.com/${{ github.repository }}/commit/${{ github.event.pull_request.head.sha || github.sha }} pr-url: ${{ github.event.issue.pull_request.html_url || '' }} pr-text: ${{ github.event.comment.body || '' }} - dispatch-inputs: ${{ toJSON(inputs) }} + dispatch-inputs: ${{ (github.event.type == 'workflow_dispatch' && toJSON(inputs)) || '' }} secrets: DIFFCALC_GOOGLE_CREDENTIALS: ${{ secrets.DIFFCALC_GOOGLE_CREDENTIALS }} From 0b570c4e151f047ac66e5ee73a5e5a48247baf29 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 11 Nov 2024 13:40:17 +0900 Subject: [PATCH 076/120] Enforce concurrency by using single job I've yet again re-confirmed by doubts about using concurrency groups. It's just not flexible enough. In this case, it cancels any _future_ jobs. --- .github/workflows/_diffcalc_processor.yml | 92 ++++++++--------------- 1 file changed, 33 insertions(+), 59 deletions(-) diff --git a/.github/workflows/_diffcalc_processor.yml b/.github/workflows/_diffcalc_processor.yml index e08534fbff81..ac2c1b7e3f6b 100644 --- a/.github/workflows/_diffcalc_processor.yml +++ b/.github/workflows/_diffcalc_processor.yml @@ -24,10 +24,6 @@ on: DIFFCALC_GOOGLE_CREDENTIALS: required: true -concurrency: - group: ${{ github.workflow }} - cancel-in-progress: false - env: GENERATOR_DIR: ${{ github.workspace }}/${{ inputs.id }} GENERATOR_ENV: ${{ github.workspace }}/${{ inputs.id }}/.env @@ -37,9 +33,15 @@ defaults: shell: bash -euo pipefail {0} jobs: - environment: + generator: name: Setup environment runs-on: self-hosted + timeout-minutes: 720 + + outputs: + target: ${{ steps.run.outputs.target }} + sheet: ${{ steps.run.outputs.sheet }} + steps: - name: Checkout diffcalc-sheet-generator uses: actions/checkout@v4 @@ -145,13 +147,8 @@ jobs: sed -i 's/^RANKED_ONLY=.*$/RANKED_ONLY=0/' "${{ env.GENERATOR_ENV }}" fi - scores: - name: Setup scores - needs: environment - runs-on: self-hosted - steps: - - name: Query latest data - id: query + - name: Query latest scores + id: query-scores run: | ruleset=$(cat ${{ env.GENERATOR_ENV }} | grep -E '^RULESET=' | cut -d '=' -f2-) performance_data_name=$(curl -s "https://data.ppy.sh/" | grep "performance_${ruleset}_top_1000\b" | tail -1 | awk -F "'" '{print $2}' | sed 's/\.tar\.bz2//g') @@ -160,31 +157,26 @@ jobs: echo "DATA_NAME=${performance_data_name}" >> "${GITHUB_OUTPUT}" echo "DATA_PKG=${performance_data_name}.tar.bz2" >> "${GITHUB_OUTPUT}" - - name: Restore cache - id: restore-cache + - name: Restore score cache + id: restore-score-cache uses: maxnowack/local-cache@720e69c948191660a90aa1cf6a42fc4d2dacdf30 # v2 with: - path: ${{ steps.query.outputs.DATA_PKG }} - key: ${{ steps.query.outputs.DATA_NAME }} + path: ${{ steps.query-scores.outputs.DATA_PKG }} + key: ${{ steps.query-scores.outputs.DATA_NAME }} - - name: Download - if: steps.restore-cache.outputs.cache-hit != 'true' + - name: Download scores + if: steps.restore-score-cache.outputs.cache-hit != 'true' run: | - wget -q -O "${{ steps.query.outputs.DATA_PKG }}" "https://data.ppy.sh/${{ steps.query.outputs.DATA_PKG }}" + wget -q -O "${{ steps.query-scores.outputs.DATA_PKG }}" "https://data.ppy.sh/${{ steps.query-scores.outputs.DATA_PKG }}" - - name: Extract + - name: Extract scores run: | - tar -I lbzip2 -xf "${{ steps.query.outputs.DATA_PKG }}" - rm -r "${{ steps.query.outputs.TARGET_DIR }}" - mv "${{ steps.query.outputs.DATA_NAME }}" "${{ steps.query.outputs.TARGET_DIR }}" + tar -I lbzip2 -xf "${{ steps.query-scores.outputs.DATA_PKG }}" + rm -r "${{ steps.query-scores.outputs.TARGET_DIR }}" + mv "${{ steps.query-scores.outputs.DATA_NAME }}" "${{ steps.query-scores.outputs.TARGET_DIR }}" - beatmaps: - name: Setup beatmaps - needs: environment - runs-on: self-hosted - steps: - - name: Query latest data - id: query + - name: Query latest beatmaps + id: query-beatmaps run: | beatmaps_data_name=$(curl -s "https://data.ppy.sh/" | grep "osu_files" | tail -1 | awk -F "'" '{print $2}' | sed 's/\.tar\.bz2//g') @@ -192,33 +184,24 @@ jobs: echo "DATA_NAME=${beatmaps_data_name}" >> "${GITHUB_OUTPUT}" echo "DATA_PKG=${beatmaps_data_name}.tar.bz2" >> "${GITHUB_OUTPUT}" - - name: Restore cache - id: restore-cache + - name: Restore beatmap cache + id: restore-beatmap-cache uses: maxnowack/local-cache@720e69c948191660a90aa1cf6a42fc4d2dacdf30 # v2 with: - path: ${{ steps.query.outputs.DATA_PKG }} - key: ${{ steps.query.outputs.DATA_NAME }} + path: ${{ steps.query-beatmaps.outputs.DATA_PKG }} + key: ${{ steps.query-beatmaps.outputs.DATA_NAME }} - - name: Download - if: steps.restore-cache.outputs.cache-hit != 'true' + - name: Download beatmap + if: steps.restore-beatmap-cache.outputs.cache-hit != 'true' run: | - wget -q -O "${{ steps.query.outputs.DATA_PKG }}" "https://data.ppy.sh/${{ steps.query.outputs.DATA_PKG }}" + wget -q -O "${{ steps.query-beatmaps.outputs.DATA_PKG }}" "https://data.ppy.sh/${{ steps.query-beatmaps.outputs.DATA_PKG }}" - - name: Extract + - name: Extract beatmap run: | - tar -I lbzip2 -xf "${{ steps.query.outputs.DATA_PKG }}" - rm -r "${{ steps.query.outputs.TARGET_DIR }}" - mv "${{ steps.query.outputs.DATA_NAME }}" "${{ steps.query.outputs.TARGET_DIR }}" + tar -I lbzip2 -xf "${{ steps.query-beatmaps.outputs.DATA_PKG }}" + rm -r "${{ steps.query-beatmaps.outputs.TARGET_DIR }}" + mv "${{ steps.query-beatmaps.outputs.DATA_NAME }}" "${{ steps.query-beatmaps.outputs.TARGET_DIR }}" - generator: - name: Run generator - needs: [ environment, scores, beatmaps ] - runs-on: self-hosted - timeout-minutes: 720 - outputs: - target: ${{ steps.run.outputs.target }} - sheet: ${{ steps.run.outputs.sheet }} - steps: - name: Run id: run run: | @@ -242,13 +225,4 @@ jobs: run: | cd "${{ env.GENERATOR_DIR }}" docker compose down --volumes - - cleanup: - name: Cleanup - needs: [ environment, scores, beatmaps, generator ] - runs-on: self-hosted - if: ${{ always() }} - steps: - - name: Cleanup - run: | rm -rf "${{ env.GENERATOR_DIR }}" From 91d9c0a7e8a09904656daee5e7d41b9c17d9bb6f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 11 Nov 2024 13:44:11 +0900 Subject: [PATCH 077/120] Adjust job name --- .github/workflows/_diffcalc_processor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/_diffcalc_processor.yml b/.github/workflows/_diffcalc_processor.yml index ac2c1b7e3f6b..4e221d05509a 100644 --- a/.github/workflows/_diffcalc_processor.yml +++ b/.github/workflows/_diffcalc_processor.yml @@ -34,7 +34,7 @@ defaults: jobs: generator: - name: Setup environment + name: Run runs-on: self-hosted timeout-minutes: 720 From e1d93a7d9c819eb2ae792934c299fbf00139aab3 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 11 Nov 2024 15:09:13 +0900 Subject: [PATCH 078/120] Merge implementations of ConvertHitObjectParser Having these be separate implementations sounded awesome at the time, but it only ever led to confusion. There's no practical difference if, for example, catch sees hitobjects with `IHasPosition` instead of `IHasXPosition`. --- .../Beatmaps/Formats/LegacyBeatmapDecoder.cs | 45 ++----- .../Objects/Legacy/Catch/ConvertHit.cs | 20 --- .../Legacy/Catch/ConvertHitObjectParser.cs | 63 --------- .../Objects/Legacy/Catch/ConvertSlider.cs | 20 --- .../Objects/Legacy/Catch/ConvertSpinner.cs | 19 --- .../Objects/Legacy/ConvertHitCircle.cs | 13 ++ .../Objects/Legacy/ConvertHitObject.cs | 14 +- .../Objects/Legacy/ConvertHitObjectParser.cs | 123 ++++++++++++------ .../ConvertSpinner.cs => ConvertHold.cs} | 11 +- .../Rulesets/Objects/Legacy/ConvertSlider.cs | 12 +- .../Legacy/{Taiko => }/ConvertSpinner.cs | 7 +- .../Objects/Legacy/Mania/ConvertHit.cs | 15 --- .../Legacy/Mania/ConvertHitObjectParser.cs | 58 --------- .../Objects/Legacy/Mania/ConvertHold.cs | 16 --- .../Objects/Legacy/Mania/ConvertSlider.cs | 15 --- .../Rulesets/Objects/Legacy/Osu/ConvertHit.cs | 20 --- .../Legacy/Osu/ConvertHitObjectParser.cs | 64 --------- .../Objects/Legacy/Osu/ConvertSlider.cs | 22 ---- .../Objects/Legacy/Osu/ConvertSpinner.cs | 24 ---- .../Objects/Legacy/Taiko/ConvertHit.cs | 12 -- .../Legacy/Taiko/ConvertHitObjectParser.cs | 51 -------- .../Objects/Legacy/Taiko/ConvertSlider.cs | 12 -- 22 files changed, 134 insertions(+), 522 deletions(-) delete mode 100644 osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHit.cs delete mode 100644 osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHitObjectParser.cs delete mode 100644 osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSlider.cs delete mode 100644 osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSpinner.cs create mode 100644 osu.Game/Rulesets/Objects/Legacy/ConvertHitCircle.cs rename osu.Game/Rulesets/Objects/Legacy/{Mania/ConvertSpinner.cs => ConvertHold.cs} (54%) rename osu.Game/Rulesets/Objects/Legacy/{Taiko => }/ConvertSpinner.cs (70%) delete mode 100644 osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHit.cs delete mode 100644 osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHitObjectParser.cs delete mode 100644 osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHold.cs delete mode 100644 osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSlider.cs delete mode 100644 osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHit.cs delete mode 100644 osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHitObjectParser.cs delete mode 100644 osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSlider.cs delete mode 100644 osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSpinner.cs delete mode 100644 osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHit.cs delete mode 100644 osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHitObjectParser.cs delete mode 100644 osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSlider.cs diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index b068c87fbb10..3d8c8a6e7aa3 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -38,8 +38,7 @@ public class LegacyBeatmapDecoder : LegacyDecoder internal static RulesetStore? RulesetStore; private Beatmap beatmap = null!; - - private ConvertHitObjectParser? parser; + private ConvertHitObjectParser parser = null!; private LegacySampleBank defaultSampleBank; private int defaultSampleVolume = 100; @@ -80,6 +79,7 @@ protected override void ParseStreamInto(LineBufferedReader stream, Beatmap beatm { this.beatmap = beatmap; this.beatmap.BeatmapInfo.BeatmapVersion = FormatVersion; + parser = new ConvertHitObjectParser(getOffsetTime(), FormatVersion); applyLegacyDefaults(this.beatmap.BeatmapInfo); @@ -162,7 +162,8 @@ private void applySamples(HitObject hitObject) { if (hitObject is IHasRepeats hasRepeats) { - SampleControlPoint sampleControlPoint = (beatmap.ControlPointInfo as LegacyControlPointInfo)?.SamplePointAt(hitObject.StartTime + CONTROL_POINT_LENIENCY + 1) ?? SampleControlPoint.DEFAULT; + SampleControlPoint sampleControlPoint = (beatmap.ControlPointInfo as LegacyControlPointInfo)?.SamplePointAt(hitObject.StartTime + CONTROL_POINT_LENIENCY + 1) + ?? SampleControlPoint.DEFAULT; hitObject.Samples = hitObject.Samples.Select(o => sampleControlPoint.ApplyTo(o)).ToList(); for (int i = 0; i < hasRepeats.NodeSamples.Count; i++) @@ -175,7 +176,8 @@ private void applySamples(HitObject hitObject) } else { - SampleControlPoint sampleControlPoint = (beatmap.ControlPointInfo as LegacyControlPointInfo)?.SamplePointAt(hitObject.GetEndTime() + CONTROL_POINT_LENIENCY) ?? SampleControlPoint.DEFAULT; + SampleControlPoint sampleControlPoint = (beatmap.ControlPointInfo as LegacyControlPointInfo)?.SamplePointAt(hitObject.GetEndTime() + CONTROL_POINT_LENIENCY) + ?? SampleControlPoint.DEFAULT; hitObject.Samples = hitObject.Samples.Select(o => sampleControlPoint.ApplyTo(o)).ToList(); } } @@ -263,29 +265,7 @@ private void handleGeneral(string line) break; case @"Mode": - int rulesetID = Parsing.ParseInt(pair.Value); - - beatmap.BeatmapInfo.Ruleset = RulesetStore?.GetRuleset(rulesetID) ?? throw new ArgumentException("Ruleset is not available locally."); - - switch (rulesetID) - { - case 0: - parser = new Rulesets.Objects.Legacy.Osu.ConvertHitObjectParser(getOffsetTime(), FormatVersion); - break; - - case 1: - parser = new Rulesets.Objects.Legacy.Taiko.ConvertHitObjectParser(getOffsetTime(), FormatVersion); - break; - - case 2: - parser = new Rulesets.Objects.Legacy.Catch.ConvertHitObjectParser(getOffsetTime(), FormatVersion); - break; - - case 3: - parser = new Rulesets.Objects.Legacy.Mania.ConvertHitObjectParser(getOffsetTime(), FormatVersion); - break; - } - + beatmap.BeatmapInfo.Ruleset = RulesetStore?.GetRuleset(Parsing.ParseInt(pair.Value)) ?? throw new ArgumentException("Ruleset is not available locally."); break; case @"LetterboxInBreaks": @@ -617,17 +597,10 @@ private void flushPendingPoints() private void handleHitObject(string line) { - // If the ruleset wasn't specified, assume the osu!standard ruleset. - parser ??= new Rulesets.Objects.Legacy.Osu.ConvertHitObjectParser(getOffsetTime(), FormatVersion); - var obj = parser.Parse(line); + obj.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty); - if (obj != null) - { - obj.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty); - - beatmap.HitObjects.Add(obj); - } + beatmap.HitObjects.Add(obj); } private int getOffsetTime(int time) => time + (ApplyOffsets ? offset : 0); diff --git a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHit.cs b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHit.cs deleted file mode 100644 index 96c779e79bbd..000000000000 --- a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHit.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Game.Rulesets.Objects.Types; -using osuTK; - -namespace osu.Game.Rulesets.Objects.Legacy.Catch -{ - /// - /// Legacy osu!catch Hit-type, used for parsing Beatmaps. - /// - internal sealed class ConvertHit : ConvertHitObject, IHasPosition - { - public float X => Position.X; - - public float Y => Position.Y; - - public Vector2 Position { get; set; } - } -} diff --git a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHitObjectParser.cs deleted file mode 100644 index a5c1a73fa78d..000000000000 --- a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHitObjectParser.cs +++ /dev/null @@ -1,63 +0,0 @@ -// 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 osuTK; -using osu.Game.Audio; -using System.Collections.Generic; - -namespace osu.Game.Rulesets.Objects.Legacy.Catch -{ - /// - /// A HitObjectParser to parse legacy osu!catch Beatmaps. - /// - public class ConvertHitObjectParser : Legacy.ConvertHitObjectParser - { - private ConvertHitObject lastObject; - - public ConvertHitObjectParser(double offset, int formatVersion) - : base(offset, formatVersion) - { - } - - protected override HitObject CreateHit(Vector2 position, bool newCombo, int comboOffset) - { - return lastObject = new ConvertHit - { - Position = position, - NewCombo = FirstObject || lastObject is ConvertSpinner || newCombo, - ComboOffset = newCombo ? comboOffset : 0 - }; - } - - protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, PathControlPoint[] controlPoints, double? length, int repeatCount, - IList> nodeSamples) - { - return lastObject = new ConvertSlider - { - Position = position, - NewCombo = FirstObject || lastObject is ConvertSpinner || newCombo, - ComboOffset = newCombo ? comboOffset : 0, - Path = new SliderPath(controlPoints, length), - NodeSamples = nodeSamples, - RepeatCount = repeatCount - }; - } - - protected override HitObject CreateSpinner(Vector2 position, bool newCombo, int comboOffset, double duration) - { - return lastObject = new ConvertSpinner - { - Duration = duration, - NewCombo = newCombo - // Spinners cannot have combo offset. - }; - } - - protected override HitObject CreateHold(Vector2 position, bool newCombo, int comboOffset, double duration) - { - return lastObject = null; - } - } -} diff --git a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSlider.cs b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSlider.cs deleted file mode 100644 index bcf1c7fae24b..000000000000 --- a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSlider.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Game.Rulesets.Objects.Types; -using osuTK; - -namespace osu.Game.Rulesets.Objects.Legacy.Catch -{ - /// - /// Legacy osu!catch Slider-type, used for parsing Beatmaps. - /// - internal sealed class ConvertSlider : Legacy.ConvertSlider, IHasPosition - { - public float X => Position.X; - - public float Y => Position.Y; - - public Vector2 Position { get; set; } - } -} diff --git a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSpinner.cs b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSpinner.cs deleted file mode 100644 index 5ef3d51cb352..000000000000 --- a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSpinner.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Game.Rulesets.Objects.Types; - -namespace osu.Game.Rulesets.Objects.Legacy.Catch -{ - /// - /// Legacy osu!catch Spinner-type, used for parsing Beatmaps. - /// - internal sealed class ConvertSpinner : ConvertHitObject, IHasDuration, IHasXPosition - { - public double EndTime => StartTime + Duration; - - public double Duration { get; set; } - - public float X => 256; // Required for CatchBeatmapConverter - } -} diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitCircle.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitCircle.cs new file mode 100644 index 000000000000..d1852d7032d8 --- /dev/null +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitCircle.cs @@ -0,0 +1,13 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Rulesets.Objects.Legacy +{ + /// + /// Legacy "HitCircle" hit object type. + /// + /// + /// Only used for parsing beatmaps and not gameplay. + /// + internal sealed class ConvertHitCircle : ConvertHitObject; +} diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObject.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObject.cs index bb36aab0b336..d1b0d3c6c23d 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObject.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObject.cs @@ -4,18 +4,28 @@ using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Scoring; +using osuTK; namespace osu.Game.Rulesets.Objects.Legacy { /// - /// A hit object only used for conversion, not actual gameplay. + /// Represents a legacy hit object. /// - internal abstract class ConvertHitObject : HitObject, IHasCombo + /// + /// Only used for parsing beatmaps and not gameplay. + /// + internal abstract class ConvertHitObject : HitObject, IHasCombo, IHasPosition { public bool NewCombo { get; set; } public int ComboOffset { get; set; } + public float X => Position.X; + + public float Y => Position.Y; + + public Vector2 Position { get; set; } + public override Judgement CreateJudgement() => new IgnoreJudgement(); protected override HitWindows CreateHitWindows() => HitWindows.Empty; diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index 89fd5f73846e..646431c0715e 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.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 osuTK; using osu.Game.Rulesets.Objects.Types; using System; @@ -11,7 +9,6 @@ using osu.Game.Beatmaps.Formats; using osu.Game.Audio; using System.Linq; -using JetBrains.Annotations; using osu.Framework.Utils; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.Legacy; @@ -24,24 +21,32 @@ namespace osu.Game.Rulesets.Objects.Legacy /// /// A HitObjectParser to parse legacy Beatmaps. /// - public abstract class ConvertHitObjectParser : HitObjectParser + public class ConvertHitObjectParser : HitObjectParser { /// /// The offset to apply to all time values. /// - protected readonly double Offset; + private readonly double offset; /// /// The .osu format (beatmap) version. /// - protected readonly int FormatVersion; + private readonly int formatVersion; + + /// + /// Whether the current hitobject is the first hitobject in the beatmap. + /// + private bool firstObject = true; - protected bool FirstObject { get; private set; } = true; + /// + /// The last parsed hitobject. + /// + private ConvertHitObject? lastObject; - protected ConvertHitObjectParser(double offset, int formatVersion) + internal ConvertHitObjectParser(double offset, int formatVersion) { - Offset = offset; - FormatVersion = formatVersion; + this.offset = offset; + this.formatVersion = formatVersion; } public override HitObject Parse(string text) @@ -49,11 +54,11 @@ public override HitObject Parse(string text) string[] split = text.Split(','); Vector2 pos = - FormatVersion >= LegacyBeatmapEncoder.FIRST_LAZER_VERSION + formatVersion >= LegacyBeatmapEncoder.FIRST_LAZER_VERSION ? new Vector2(Parsing.ParseFloat(split[0], Parsing.MAX_COORDINATE_VALUE), Parsing.ParseFloat(split[1], Parsing.MAX_COORDINATE_VALUE)) : new Vector2((int)Parsing.ParseFloat(split[0], Parsing.MAX_COORDINATE_VALUE), (int)Parsing.ParseFloat(split[1], Parsing.MAX_COORDINATE_VALUE)); - double startTime = Parsing.ParseDouble(split[2]) + Offset; + double startTime = Parsing.ParseDouble(split[2]) + offset; LegacyHitObjectType type = (LegacyHitObjectType)Parsing.ParseInt(split[3]); @@ -66,11 +71,11 @@ public override HitObject Parse(string text) var soundType = (LegacyHitSoundType)Parsing.ParseInt(split[4]); var bankInfo = new SampleBankInfo(); - HitObject result = null; + HitObject? result = null; if (type.HasFlag(LegacyHitObjectType.Circle)) { - result = CreateHit(pos, combo, comboOffset); + result = createHitCircle(pos, combo, comboOffset); if (split.Length > 5) readCustomSampleBanks(split[5], bankInfo); @@ -145,13 +150,13 @@ public override HitObject Parse(string text) for (int i = 0; i < nodes; i++) nodeSamples.Add(convertSoundType(nodeSoundTypes[i], nodeBankInfos[i])); - result = CreateSlider(pos, combo, comboOffset, convertPathString(split[5], pos), length, repeatCount, nodeSamples); + result = createSlider(pos, combo, comboOffset, convertPathString(split[5], pos), length, repeatCount, nodeSamples); } else if (type.HasFlag(LegacyHitObjectType.Spinner)) { - double duration = Math.Max(0, Parsing.ParseDouble(split[5]) + Offset - startTime); + double duration = Math.Max(0, Parsing.ParseDouble(split[5]) + offset - startTime); - result = CreateSpinner(new Vector2(512, 384) / 2, combo, comboOffset, duration); + result = createSpinner(new Vector2(512, 384) / 2, combo, comboOffset, duration); if (split.Length > 6) readCustomSampleBanks(split[6], bankInfo); @@ -169,7 +174,7 @@ public override HitObject Parse(string text) readCustomSampleBanks(string.Join(':', ss.Skip(1)), bankInfo); } - result = CreateHold(pos, combo, comboOffset, endTime + Offset - startTime); + result = createHold(pos, combo, comboOffset, endTime + offset - startTime); } if (result == null) @@ -180,7 +185,7 @@ public override HitObject Parse(string text) if (result.Samples.Count == 0) result.Samples = convertSoundType(soundType, bankInfo); - FirstObject = false; + firstObject = false; return result; } @@ -200,10 +205,11 @@ private void readCustomSampleBanks(string str, SampleBankInfo bankInfo, bool ban if (!Enum.IsDefined(addBank)) addBank = LegacySampleBank.Normal; - string stringBank = bank.ToString().ToLowerInvariant(); + string? stringBank = bank.ToString().ToLowerInvariant(); + string? stringAddBank = addBank.ToString().ToLowerInvariant(); + if (stringBank == @"none") stringBank = null; - string stringAddBank = addBank.ToString().ToLowerInvariant(); if (stringAddBank == @"none") { @@ -357,7 +363,7 @@ private IEnumerable> convertPoints(PathType type, { int endPointLength = endPoint == null ? 0 : 1; - if (FormatVersion < LegacyBeatmapEncoder.FIRST_LAZER_VERSION) + if (formatVersion < LegacyBeatmapEncoder.FIRST_LAZER_VERSION) { if (vertices.Length + endPointLength != 3) type = PathType.BEZIER; @@ -393,7 +399,7 @@ private IEnumerable> convertPoints(PathType type, // Legacy CATMULL sliders don't support multiple segments, so adjacent CATMULL segments should be treated as a single one. // Importantly, this is not applied to the first control point, which may duplicate the slider path's position // resulting in a duplicate (0,0) control point in the resultant list. - if (type == PathType.CATMULL && endIndex > 1 && FormatVersion < LegacyBeatmapEncoder.FIRST_LAZER_VERSION) + if (type == PathType.CATMULL && endIndex > 1 && formatVersion < LegacyBeatmapEncoder.FIRST_LAZER_VERSION) continue; // The last control point of each segment is not allowed to start a new implicit segment. @@ -442,7 +448,15 @@ private PathControlPoint[] mergeControlPointsLists(ListWhether the hit object creates a new combo. /// When starting a new combo, the offset of the new combo relative to the current one. /// The hit object. - protected abstract HitObject CreateHit(Vector2 position, bool newCombo, int comboOffset); + private HitObject createHitCircle(Vector2 position, bool newCombo, int comboOffset) + { + return lastObject = new ConvertHitCircle + { + Position = position, + NewCombo = firstObject || lastObject is ConvertSpinner || newCombo, + ComboOffset = newCombo ? comboOffset : 0 + }; + } /// /// Creats a legacy Slider-type hit object. @@ -455,8 +469,19 @@ private PathControlPoint[] mergeControlPointsLists(ListThe slider repeat count. /// The samples to be played when the slider nodes are hit. This includes the head and tail of the slider. /// The hit object. - protected abstract HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, PathControlPoint[] controlPoints, double? length, int repeatCount, - IList> nodeSamples); + private HitObject createSlider(Vector2 position, bool newCombo, int comboOffset, PathControlPoint[] controlPoints, double? length, int repeatCount, + IList> nodeSamples) + { + return lastObject = new ConvertSlider + { + Position = position, + NewCombo = firstObject || lastObject is ConvertSpinner || newCombo, + ComboOffset = newCombo ? comboOffset : 0, + Path = new SliderPath(controlPoints, length), + NodeSamples = nodeSamples, + RepeatCount = repeatCount + }; + } /// /// Creates a legacy Spinner-type hit object. @@ -466,7 +491,16 @@ protected abstract HitObject CreateSlider(Vector2 position, bool newCombo, int c /// When starting a new combo, the offset of the new combo relative to the current one. /// The spinner duration. /// The hit object. - protected abstract HitObject CreateSpinner(Vector2 position, bool newCombo, int comboOffset, double duration); + private HitObject createSpinner(Vector2 position, bool newCombo, int comboOffset, double duration) + { + return lastObject = new ConvertSpinner + { + Position = position, + Duration = duration, + NewCombo = newCombo + // Spinners cannot have combo offset. + }; + } /// /// Creates a legacy Hold-type hit object. @@ -475,7 +509,14 @@ protected abstract HitObject CreateSlider(Vector2 position, bool newCombo, int c /// Whether the hit object creates a new combo. /// When starting a new combo, the offset of the new combo relative to the current one. /// The hold duration. - protected abstract HitObject CreateHold(Vector2 position, bool newCombo, int comboOffset, double duration); + private HitObject createHold(Vector2 position, bool newCombo, int comboOffset, double duration) + { + return lastObject = new ConvertHold + { + Position = position, + Duration = duration + }; + } private List convertSoundType(LegacyHitSoundType type, SampleBankInfo bankInfo) { @@ -511,21 +552,19 @@ private class SampleBankInfo /// /// An optional overriding filename which causes all bank/sample specifications to be ignored. /// - public string Filename; + public string? Filename; /// /// The bank identifier to use for the base ("hitnormal") sample. /// Transferred to when appropriate. /// - [CanBeNull] - public string BankForNormal; + public string? BankForNormal; /// /// The bank identifier to use for additions ("hitwhistle", "hitfinish", "hitclap"). /// Transferred to when appropriate. /// - [CanBeNull] - public string BankForAdditions; + public string? BankForAdditions; /// /// Hit sample volume (0-100). @@ -548,8 +587,6 @@ private class SampleBankInfo public SampleBankInfo Clone() => (SampleBankInfo)MemberwiseClone(); } -#nullable enable - public class LegacyHitSampleInfo : HitSampleInfo, IEquatable { public readonly int CustomSampleBank; @@ -577,13 +614,14 @@ public LegacyHitSampleInfo(string name, string? bank = null, int volume = 0, boo IsLayered = isLayered; } - public sealed override HitSampleInfo With(Optional newName = default, Optional newBank = default, Optional newSuffix = default, Optional newVolume = default, Optional newEditorAutoBank = default) + public sealed override HitSampleInfo With(Optional newName = default, Optional newBank = default, Optional newSuffix = default, Optional newVolume = default, + Optional newEditorAutoBank = default) => With(newName, newBank, newVolume, newEditorAutoBank); - public virtual LegacyHitSampleInfo With(Optional newName = default, Optional newBank = default, Optional newVolume = default, Optional newEditorAutoBank = default, - Optional newCustomSampleBank = default, - Optional newIsLayered = default) - => new LegacyHitSampleInfo(newName.GetOr(Name), newBank.GetOr(Bank), newVolume.GetOr(Volume), newEditorAutoBank.GetOr(EditorAutoBank), newCustomSampleBank.GetOr(CustomSampleBank), newIsLayered.GetOr(IsLayered)); + public virtual LegacyHitSampleInfo With(Optional newName = default, Optional newBank = default, Optional newVolume = default, + Optional newEditorAutoBank = default, Optional newCustomSampleBank = default, Optional newIsLayered = default) + => new LegacyHitSampleInfo(newName.GetOr(Name), newBank.GetOr(Bank), newVolume.GetOr(Volume), newEditorAutoBank.GetOr(EditorAutoBank), newCustomSampleBank.GetOr(CustomSampleBank), + newIsLayered.GetOr(IsLayered)); public bool Equals(LegacyHitSampleInfo? other) // The additions to equality checks here are *required* to ensure that pooling works correctly. @@ -615,9 +653,8 @@ public FileHitSampleInfo(string filename, int volume) Path.ChangeExtension(Filename, null) }; - public sealed override LegacyHitSampleInfo With(Optional newName = default, Optional newBank = default, Optional newVolume = default, Optional newEditorAutoBank = default, - Optional newCustomSampleBank = default, - Optional newIsLayered = default) + public sealed override LegacyHitSampleInfo With(Optional newName = default, Optional newBank = default, Optional newVolume = default, + Optional newEditorAutoBank = default, Optional newCustomSampleBank = default, Optional newIsLayered = default) => new FileHitSampleInfo(Filename, newVolume.GetOr(Volume)); public bool Equals(FileHitSampleInfo? other) diff --git a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSpinner.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHold.cs similarity index 54% rename from osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSpinner.cs rename to osu.Game/Rulesets/Objects/Legacy/ConvertHold.cs index c05aaceb9c24..d74224892bf6 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSpinner.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHold.cs @@ -3,17 +3,18 @@ using osu.Game.Rulesets.Objects.Types; -namespace osu.Game.Rulesets.Objects.Legacy.Mania +namespace osu.Game.Rulesets.Objects.Legacy { /// - /// Legacy osu!mania Spinner-type, used for parsing Beatmaps. + /// Legacy "Hold" hit object type. Generally only valid in the mania ruleset. /// - internal sealed class ConvertSpinner : ConvertHitObject, IHasDuration, IHasXPosition + /// + /// Only used for parsing beatmaps and not gameplay. + /// + internal sealed class ConvertHold : ConvertHitObject, IHasDuration { public double Duration { get; set; } public double EndTime => StartTime + Duration; - - public float X { get; set; } } } diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs index 683eefa8f4e8..fee68f2f1148 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.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.Game.Rulesets.Objects.Types; using System.Collections.Generic; using Newtonsoft.Json; @@ -13,7 +11,13 @@ namespace osu.Game.Rulesets.Objects.Legacy { - internal abstract class ConvertSlider : ConvertHitObject, IHasPathWithRepeats, IHasSliderVelocity + /// + /// Legacy "Slider" hit object type. + /// + /// + /// Only used for parsing beatmaps and not gameplay. + /// + internal class ConvertSlider : ConvertHitObject, IHasPathWithRepeats, IHasSliderVelocity, IHasGenerateTicks { /// /// Scoring distance with a speed-adjusted beat length of 1 second. @@ -50,6 +54,8 @@ public double SliderVelocityMultiplier set => SliderVelocityMultiplierBindable.Value = value; } + public bool GenerateTicks { get; set; } = true; + protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty) { base.ApplyDefaultsToSelf(controlPointInfo, difficulty); diff --git a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSpinner.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertSpinner.cs similarity index 70% rename from osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSpinner.cs rename to osu.Game/Rulesets/Objects/Legacy/ConvertSpinner.cs index 1d5ecb1ef36b..59551cd37a71 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSpinner.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertSpinner.cs @@ -3,11 +3,14 @@ using osu.Game.Rulesets.Objects.Types; -namespace osu.Game.Rulesets.Objects.Legacy.Taiko +namespace osu.Game.Rulesets.Objects.Legacy { /// - /// Legacy osu!taiko Spinner-type, used for parsing Beatmaps. + /// Legacy "Spinner" hit object type. /// + /// + /// Only used for parsing beatmaps and not gameplay. + /// internal sealed class ConvertSpinner : ConvertHitObject, IHasDuration { public double Duration { get; set; } diff --git a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHit.cs b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHit.cs deleted file mode 100644 index 0b69817c13b7..000000000000 --- a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHit.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Game.Rulesets.Objects.Types; - -namespace osu.Game.Rulesets.Objects.Legacy.Mania -{ - /// - /// Legacy osu!mania Hit-type, used for parsing Beatmaps. - /// - internal sealed class ConvertHit : ConvertHitObject, IHasXPosition - { - public float X { get; set; } - } -} diff --git a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHitObjectParser.cs deleted file mode 100644 index 386eb8d3eeb2..000000000000 --- a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHitObjectParser.cs +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osuTK; -using osu.Game.Audio; -using System.Collections.Generic; - -namespace osu.Game.Rulesets.Objects.Legacy.Mania -{ - /// - /// A HitObjectParser to parse legacy osu!mania Beatmaps. - /// - public class ConvertHitObjectParser : Legacy.ConvertHitObjectParser - { - public ConvertHitObjectParser(double offset, int formatVersion) - : base(offset, formatVersion) - { - } - - protected override HitObject CreateHit(Vector2 position, bool newCombo, int comboOffset) - { - return new ConvertHit - { - X = position.X - }; - } - - protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, PathControlPoint[] controlPoints, double? length, int repeatCount, - IList> nodeSamples) - { - return new ConvertSlider - { - X = position.X, - Path = new SliderPath(controlPoints, length), - NodeSamples = nodeSamples, - RepeatCount = repeatCount - }; - } - - protected override HitObject CreateSpinner(Vector2 position, bool newCombo, int comboOffset, double duration) - { - return new ConvertSpinner - { - X = position.X, - Duration = duration - }; - } - - protected override HitObject CreateHold(Vector2 position, bool newCombo, int comboOffset, double duration) - { - return new ConvertHold - { - X = position.X, - Duration = duration - }; - } - } -} diff --git a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHold.cs b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHold.cs deleted file mode 100644 index 2fa4766c1d53..000000000000 --- a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHold.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Game.Rulesets.Objects.Types; - -namespace osu.Game.Rulesets.Objects.Legacy.Mania -{ - internal sealed class ConvertHold : ConvertHitObject, IHasXPosition, IHasDuration - { - public float X { get; set; } - - public double Duration { get; set; } - - public double EndTime => StartTime + Duration; - } -} diff --git a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSlider.cs b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSlider.cs deleted file mode 100644 index 84cde5fa95ba..000000000000 --- a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSlider.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Game.Rulesets.Objects.Types; - -namespace osu.Game.Rulesets.Objects.Legacy.Mania -{ - /// - /// Legacy osu!mania Slider-type, used for parsing Beatmaps. - /// - internal sealed class ConvertSlider : Legacy.ConvertSlider, IHasXPosition - { - public float X { get; set; } - } -} diff --git a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHit.cs b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHit.cs deleted file mode 100644 index b7cd4b0dcc97..000000000000 --- a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHit.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Game.Rulesets.Objects.Types; -using osuTK; - -namespace osu.Game.Rulesets.Objects.Legacy.Osu -{ - /// - /// Legacy osu! Hit-type, used for parsing Beatmaps. - /// - internal sealed class ConvertHit : ConvertHitObject, IHasPosition - { - public Vector2 Position { get; set; } - - public float X => Position.X; - - public float Y => Position.Y; - } -} diff --git a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHitObjectParser.cs deleted file mode 100644 index 43c346b621f7..000000000000 --- a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHitObjectParser.cs +++ /dev/null @@ -1,64 +0,0 @@ -// 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 osuTK; -using System.Collections.Generic; -using osu.Game.Audio; - -namespace osu.Game.Rulesets.Objects.Legacy.Osu -{ - /// - /// A HitObjectParser to parse legacy osu! Beatmaps. - /// - public class ConvertHitObjectParser : Legacy.ConvertHitObjectParser - { - private ConvertHitObject lastObject; - - public ConvertHitObjectParser(double offset, int formatVersion) - : base(offset, formatVersion) - { - } - - protected override HitObject CreateHit(Vector2 position, bool newCombo, int comboOffset) - { - return lastObject = new ConvertHit - { - Position = position, - NewCombo = FirstObject || lastObject is ConvertSpinner || newCombo, - ComboOffset = newCombo ? comboOffset : 0 - }; - } - - protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, PathControlPoint[] controlPoints, double? length, int repeatCount, - IList> nodeSamples) - { - return lastObject = new ConvertSlider - { - Position = position, - NewCombo = FirstObject || lastObject is ConvertSpinner || newCombo, - ComboOffset = newCombo ? comboOffset : 0, - Path = new SliderPath(controlPoints, length), - NodeSamples = nodeSamples, - RepeatCount = repeatCount - }; - } - - protected override HitObject CreateSpinner(Vector2 position, bool newCombo, int comboOffset, double duration) - { - return lastObject = new ConvertSpinner - { - Position = position, - Duration = duration, - NewCombo = newCombo - // Spinners cannot have combo offset. - }; - } - - protected override HitObject CreateHold(Vector2 position, bool newCombo, int comboOffset, double duration) - { - return lastObject = null; - } - } -} diff --git a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSlider.cs b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSlider.cs deleted file mode 100644 index 8c37154f9584..000000000000 --- a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSlider.cs +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Game.Rulesets.Objects.Types; -using osuTK; - -namespace osu.Game.Rulesets.Objects.Legacy.Osu -{ - /// - /// Legacy osu! Slider-type, used for parsing Beatmaps. - /// - internal sealed class ConvertSlider : Legacy.ConvertSlider, IHasPosition, IHasGenerateTicks - { - public Vector2 Position { get; set; } - - public float X => Position.X; - - public float Y => Position.Y; - - public bool GenerateTicks { get; set; } = true; - } -} diff --git a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSpinner.cs b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSpinner.cs deleted file mode 100644 index d6e24b6bbfa1..000000000000 --- a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSpinner.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Game.Rulesets.Objects.Types; -using osuTK; - -namespace osu.Game.Rulesets.Objects.Legacy.Osu -{ - /// - /// Legacy osu! Spinner-type, used for parsing Beatmaps. - /// - internal sealed class ConvertSpinner : ConvertHitObject, IHasDuration, IHasPosition - { - public double Duration { get; set; } - - public double EndTime => StartTime + Duration; - - public Vector2 Position { get; set; } - - public float X => Position.X; - - public float Y => Position.Y; - } -} diff --git a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHit.cs b/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHit.cs deleted file mode 100644 index cb5178ce488c..000000000000 --- a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHit.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -namespace osu.Game.Rulesets.Objects.Legacy.Taiko -{ - /// - /// Legacy osu!taiko Hit-type, used for parsing Beatmaps. - /// - internal sealed class ConvertHit : ConvertHitObject - { - } -} diff --git a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHitObjectParser.cs deleted file mode 100644 index d62e8cd04c3a..000000000000 --- a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHitObjectParser.cs +++ /dev/null @@ -1,51 +0,0 @@ -// 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 osuTK; -using System.Collections.Generic; -using osu.Game.Audio; - -namespace osu.Game.Rulesets.Objects.Legacy.Taiko -{ - /// - /// A HitObjectParser to parse legacy osu!taiko Beatmaps. - /// - public class ConvertHitObjectParser : Legacy.ConvertHitObjectParser - { - public ConvertHitObjectParser(double offset, int formatVersion) - : base(offset, formatVersion) - { - } - - protected override HitObject CreateHit(Vector2 position, bool newCombo, int comboOffset) - { - return new ConvertHit(); - } - - protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, PathControlPoint[] controlPoints, double? length, int repeatCount, - IList> nodeSamples) - { - return new ConvertSlider - { - Path = new SliderPath(controlPoints, length), - NodeSamples = nodeSamples, - RepeatCount = repeatCount - }; - } - - protected override HitObject CreateSpinner(Vector2 position, bool newCombo, int comboOffset, double duration) - { - return new ConvertSpinner - { - Duration = duration - }; - } - - protected override HitObject CreateHold(Vector2 position, bool newCombo, int comboOffset, double duration) - { - return null; - } - } -} diff --git a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSlider.cs b/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSlider.cs deleted file mode 100644 index 821554f7eefd..000000000000 --- a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSlider.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -namespace osu.Game.Rulesets.Objects.Legacy.Taiko -{ - /// - /// Legacy osu!taiko Slider-type, used for parsing Beatmaps. - /// - internal sealed class ConvertSlider : Legacy.ConvertSlider - { - } -} From 7206e97b7beb637c411a39f0418c50db0f89c427 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 11 Nov 2024 15:23:23 +0900 Subject: [PATCH 079/120] Add `IHasLegacyHitObjectType` to ConvertHitObject --- .../Beatmaps/Legacy/LegacyHitObjectType.cs | 2 +- .../Objects/Legacy/ConvertHitObject.cs | 5 ++++- .../Objects/Legacy/ConvertHitObjectParser.cs | 13 +++++++------ .../Objects/Legacy/IHasLegacyHitObjectType.cs | 18 ++++++++++++++++++ 4 files changed, 30 insertions(+), 8 deletions(-) create mode 100644 osu.Game/Rulesets/Objects/Legacy/IHasLegacyHitObjectType.cs diff --git a/osu.Game/Beatmaps/Legacy/LegacyHitObjectType.cs b/osu.Game/Beatmaps/Legacy/LegacyHitObjectType.cs index 07f170f996d7..6fab66bf7058 100644 --- a/osu.Game/Beatmaps/Legacy/LegacyHitObjectType.cs +++ b/osu.Game/Beatmaps/Legacy/LegacyHitObjectType.cs @@ -6,7 +6,7 @@ namespace osu.Game.Beatmaps.Legacy { [Flags] - internal enum LegacyHitObjectType + public enum LegacyHitObjectType { Circle = 1, Slider = 1 << 1, diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObject.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObject.cs index d1b0d3c6c23d..28683583eef1 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObject.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObject.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 osu.Game.Beatmaps.Legacy; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Scoring; @@ -14,7 +15,7 @@ namespace osu.Game.Rulesets.Objects.Legacy /// /// Only used for parsing beatmaps and not gameplay. /// - internal abstract class ConvertHitObject : HitObject, IHasCombo, IHasPosition + internal abstract class ConvertHitObject : HitObject, IHasCombo, IHasPosition, IHasLegacyHitObjectType { public bool NewCombo { get; set; } @@ -26,6 +27,8 @@ internal abstract class ConvertHitObject : HitObject, IHasCombo, IHasPosition public Vector2 Position { get; set; } + public LegacyHitObjectType LegacyType { get; set; } + public override Judgement CreateJudgement() => new IgnoreJudgement(); protected override HitWindows CreateHitWindows() => HitWindows.Empty; diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index 646431c0715e..1c933392c0aa 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -71,7 +71,7 @@ public override HitObject Parse(string text) var soundType = (LegacyHitSoundType)Parsing.ParseInt(split[4]); var bankInfo = new SampleBankInfo(); - HitObject? result = null; + ConvertHitObject? result = null; if (type.HasFlag(LegacyHitObjectType.Circle)) { @@ -181,6 +181,7 @@ public override HitObject Parse(string text) throw new InvalidDataException($"Unknown hit object type: {split[3]}"); result.StartTime = startTime; + result.LegacyType = type; if (result.Samples.Count == 0) result.Samples = convertSoundType(soundType, bankInfo); @@ -448,7 +449,7 @@ private PathControlPoint[] mergeControlPointsLists(ListWhether the hit object creates a new combo. /// When starting a new combo, the offset of the new combo relative to the current one. /// The hit object. - private HitObject createHitCircle(Vector2 position, bool newCombo, int comboOffset) + private ConvertHitObject createHitCircle(Vector2 position, bool newCombo, int comboOffset) { return lastObject = new ConvertHitCircle { @@ -469,8 +470,8 @@ private HitObject createHitCircle(Vector2 position, bool newCombo, int comboOffs /// The slider repeat count. /// The samples to be played when the slider nodes are hit. This includes the head and tail of the slider. /// The hit object. - private HitObject createSlider(Vector2 position, bool newCombo, int comboOffset, PathControlPoint[] controlPoints, double? length, int repeatCount, - IList> nodeSamples) + private ConvertHitObject createSlider(Vector2 position, bool newCombo, int comboOffset, PathControlPoint[] controlPoints, double? length, int repeatCount, + IList> nodeSamples) { return lastObject = new ConvertSlider { @@ -491,7 +492,7 @@ private HitObject createSlider(Vector2 position, bool newCombo, int comboOffset, /// When starting a new combo, the offset of the new combo relative to the current one. /// The spinner duration. /// The hit object. - private HitObject createSpinner(Vector2 position, bool newCombo, int comboOffset, double duration) + private ConvertHitObject createSpinner(Vector2 position, bool newCombo, int comboOffset, double duration) { return lastObject = new ConvertSpinner { @@ -509,7 +510,7 @@ private HitObject createSpinner(Vector2 position, bool newCombo, int comboOffset /// Whether the hit object creates a new combo. /// When starting a new combo, the offset of the new combo relative to the current one. /// The hold duration. - private HitObject createHold(Vector2 position, bool newCombo, int comboOffset, double duration) + private ConvertHitObject createHold(Vector2 position, bool newCombo, int comboOffset, double duration) { return lastObject = new ConvertHold { diff --git a/osu.Game/Rulesets/Objects/Legacy/IHasLegacyHitObjectType.cs b/osu.Game/Rulesets/Objects/Legacy/IHasLegacyHitObjectType.cs new file mode 100644 index 000000000000..71af57700dc1 --- /dev/null +++ b/osu.Game/Rulesets/Objects/Legacy/IHasLegacyHitObjectType.cs @@ -0,0 +1,18 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Beatmaps.Legacy; + +namespace osu.Game.Rulesets.Objects.Legacy +{ + /// + /// A hit object from a legacy beatmap representation. + /// + public interface IHasLegacyHitObjectType + { + /// + /// The hit object type. + /// + LegacyHitObjectType LegacyType { get; } + } +} From 8c68db0a369bbfafe726accf1ff23ff902d3c6ed Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 11 Nov 2024 15:27:45 +0900 Subject: [PATCH 080/120] Remove unused params --- .../Rulesets/Objects/Legacy/ConvertHitObjectParser.cs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index 1c933392c0aa..f8bc0ce112ac 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -156,7 +156,7 @@ public override HitObject Parse(string text) { double duration = Math.Max(0, Parsing.ParseDouble(split[5]) + offset - startTime); - result = createSpinner(new Vector2(512, 384) / 2, combo, comboOffset, duration); + result = createSpinner(new Vector2(512, 384) / 2, combo, duration); if (split.Length > 6) readCustomSampleBanks(split[6], bankInfo); @@ -174,7 +174,7 @@ public override HitObject Parse(string text) readCustomSampleBanks(string.Join(':', ss.Skip(1)), bankInfo); } - result = createHold(pos, combo, comboOffset, endTime + offset - startTime); + result = createHold(pos, endTime + offset - startTime); } if (result == null) @@ -489,10 +489,9 @@ private ConvertHitObject createSlider(Vector2 position, bool newCombo, int combo /// /// The position of the hit object. /// Whether the hit object creates a new combo. - /// When starting a new combo, the offset of the new combo relative to the current one. /// The spinner duration. /// The hit object. - private ConvertHitObject createSpinner(Vector2 position, bool newCombo, int comboOffset, double duration) + private ConvertHitObject createSpinner(Vector2 position, bool newCombo, double duration) { return lastObject = new ConvertSpinner { @@ -507,10 +506,8 @@ private ConvertHitObject createSpinner(Vector2 position, bool newCombo, int comb /// Creates a legacy Hold-type hit object. /// /// The position of the hit object. - /// Whether the hit object creates a new combo. - /// When starting a new combo, the offset of the new combo relative to the current one. /// The hold duration. - private ConvertHitObject createHold(Vector2 position, bool newCombo, int comboOffset, double duration) + private ConvertHitObject createHold(Vector2 position, double duration) { return lastObject = new ConvertHold { From 06380f91fca4656f96a3c32c82960d3c55aab9bc Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 11 Nov 2024 16:25:05 +0900 Subject: [PATCH 081/120] Update test --- .../SongSelect/TestSceneBeatmapInfoWedge.cs | 38 +++++++------------ 1 file changed, 13 insertions(+), 25 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs index fd102da0262e..d8573b2d0341 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs @@ -1,12 +1,9 @@ // 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.Collections.Generic; using System.Linq; -using JetBrains.Annotations; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -23,7 +20,6 @@ using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Legacy; -using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Taiko; @@ -35,15 +31,11 @@ namespace osu.Game.Tests.Visual.SongSelect [TestFixture] public partial class TestSceneBeatmapInfoWedge : OsuTestScene { - private RulesetStore rulesets; - private TestBeatmapInfoWedge infoWedge; - private readonly List beatmaps = new List(); + [Resolved] + private RulesetStore rulesets { get; set; } = null!; - [BackgroundDependencyLoader] - private void load(RulesetStore rulesets) - { - this.rulesets = rulesets; - } + private TestBeatmapInfoWedge infoWedge = null!; + private readonly List beatmaps = new List(); protected override void LoadComplete() { @@ -156,7 +148,7 @@ public void TestBPMUpdates() IBeatmap beatmap = createTestBeatmap(new OsuRuleset().RulesetInfo); beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 60 * 1000 / bpm }); - OsuModDoubleTime doubleTime = null; + OsuModDoubleTime doubleTime = null!; selectBeatmap(beatmap); checkDisplayedBPM($"{bpm}"); @@ -173,7 +165,7 @@ public void TestBPMUpdates() [TestCase(120, 120.4, null, "120")] [TestCase(120, 120.6, "DT", "180-182 (mostly 180)")] [TestCase(120, 120.4, "DT", "180")] - public void TestVaryingBPM(double commonBpm, double otherBpm, string mod, string expectedDisplay) + public void TestVaryingBPM(double commonBpm, double otherBpm, string? mod, string expectedDisplay) { IBeatmap beatmap = createTestBeatmap(new OsuRuleset().RulesetInfo); beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 60 * 1000 / commonBpm }); @@ -203,7 +195,7 @@ public void TestLengthUpdates() double drain = beatmap.CalculateDrainLength(); beatmap.BeatmapInfo.Length = drain; - OsuModDoubleTime doubleTime = null; + OsuModDoubleTime doubleTime = null!; selectBeatmap(beatmap); checkDisplayedLength(drain); @@ -221,14 +213,15 @@ private void checkDisplayedLength(double drain) AddUntilStep($"check map drain ({displayedLength})", () => { - var label = infoWedge.DisplayedContent.ChildrenOfType().Single(l => l.Statistic.Name == BeatmapsetsStrings.ShowStatsTotalLength(displayedLength)); + var label = infoWedge.DisplayedContent.ChildrenOfType() + .Single(l => l.Statistic.Name == BeatmapsetsStrings.ShowStatsTotalLength(displayedLength)); return label.Statistic.Content == displayedLength.ToString(); }); } private void setRuleset(RulesetInfo rulesetInfo) { - Container containerBefore = null; + Container? containerBefore = null; AddStep("set ruleset", () => { @@ -242,9 +235,9 @@ private void setRuleset(RulesetInfo rulesetInfo) AddUntilStep("wait for async load", () => infoWedge.DisplayedContent != containerBefore); } - private void selectBeatmap([CanBeNull] IBeatmap b) + private void selectBeatmap(IBeatmap? b) { - Container containerBefore = null; + Container? containerBefore = null; AddStep($"select {b?.Metadata.Title ?? "null"} beatmap", () => { @@ -307,11 +300,6 @@ private partial class TestBeatmapInfoWedge : BeatmapInfoWedge public new WedgeInfoText Info => base.Info; } - private class TestHitObject : ConvertHitObject, IHasPosition - { - public float X => 0; - public float Y => 0; - public Vector2 Position { get; } = Vector2.Zero; - } + private class TestHitObject : ConvertHitObject; } } From 1c3a30a2976a19fb6aa099281919e871cf7c6ca1 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 11 Nov 2024 16:30:26 +0900 Subject: [PATCH 082/120] Fix the other test class too --- .../Visual/SongSelectV2/TestSceneBeatmapInfoWedge.cs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapInfoWedge.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapInfoWedge.cs index 35bd4ee9583a..fbbab3a60405 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapInfoWedge.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapInfoWedge.cs @@ -14,9 +14,7 @@ using osu.Game.Rulesets; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Legacy; -using osu.Game.Rulesets.Objects.Types; using osu.Game.Screens.Select; -using osuTK; namespace osu.Game.Tests.Visual.SongSelectV2 { @@ -209,11 +207,6 @@ private partial class TestBeatmapInfoWedgeV2 : BeatmapInfoWedgeV2 public new WedgeInfoText? Info => base.Info; } - private class TestHitObject : ConvertHitObject, IHasPosition - { - public float X => 0; - public float Y => 0; - public Vector2 Position { get; } = Vector2.Zero; - } + private class TestHitObject : ConvertHitObject; } } From f8ac54d61cf5165c2e8cf3eb33d4edf32e6d16f9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 Nov 2024 16:59:02 +0900 Subject: [PATCH 083/120] Fix weird local variable typo --- osu.Game/Screens/Select/BeatmapCarousel.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 44f91b4df1ee..412e44cbdecc 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -216,7 +216,7 @@ private void loadNewRoot() private int visibleSetsCount; - public BeatmapCarousel(FilterCriteria initialCriterial) + public BeatmapCarousel(FilterCriteria initialCriteria) { root = new CarouselRoot(this); InternalChild = new Container @@ -239,7 +239,7 @@ public BeatmapCarousel(FilterCriteria initialCriterial) } }; - activeCriteria = initialCriterial; + activeCriteria = initialCriteria; } [BackgroundDependencyLoader] From e8b69581b727410244faeaed3d4e36079be2352c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 Nov 2024 17:49:34 +0900 Subject: [PATCH 084/120] Fix top rank display not showing up on beatmaps with many difficulties --- osu.Game/Screens/Select/BeatmapCarousel.cs | 38 ++++++++++++++++++---- 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 412e44cbdecc..ba200315097f 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -222,12 +222,6 @@ public BeatmapCarousel(FilterCriteria initialCriteria) InternalChild = new Container { RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding - { - // Avoid clash between scrollbar and osu! logo. - Top = 10, - Bottom = 100, - }, Children = new Drawable[] { setPool, @@ -1271,6 +1265,38 @@ protected override bool OnDragStart(DragStartEvent e) return base.OnDragStart(e); } + + protected override ScrollbarContainer CreateScrollbar(Direction direction) + { + return new PaddedScrollbar(); + } + + protected partial class PaddedScrollbar : OsuScrollbar + { + public PaddedScrollbar() + : base(Direction.Vertical) + { + } + } + + private const float top_padding = 10; + private const float bottom_padding = 80; + + protected override float ToScrollbarPosition(float scrollPosition) + { + if (Precision.AlmostEquals(0, ScrollableExtent)) + return 0; + + return top_padding + (ScrollbarMovementExtent - bottom_padding) * (scrollPosition / ScrollableExtent); + } + + protected override float FromScrollbarPosition(float scrollbarPosition) + { + if (Precision.AlmostEquals(0, ScrollbarMovementExtent)) + return 0; + + return ScrollableExtent * ((scrollbarPosition - top_padding) / (ScrollbarMovementExtent - bottom_padding)); + } } protected override void Dispose(bool isDisposing) From 0cddb93dda4275c592436c71f07c2e514e1635da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 11 Nov 2024 09:57:17 +0100 Subject: [PATCH 085/120] Move setting to user config --- osu.Game/Configuration/OsuConfigManager.cs | 2 ++ osu.Game/Localisation/EditorStrings.cs | 4 ++-- osu.Game/Screens/Edit/Editor.cs | 4 ++-- osu.Game/Screens/Edit/EditorBeatmap.cs | 2 -- osu.Game/Screens/Edit/Timing/GroupSection.cs | 6 +++++- osu.Game/Screens/Edit/Timing/TapTimingControl.cs | 8 ++++++-- osu.Game/Screens/Edit/Timing/TimingSection.cs | 6 +++++- 7 files changed, 22 insertions(+), 10 deletions(-) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index f642d23bb03c..af6fd61a3d1b 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -196,6 +196,7 @@ protected override void InitialiseDefaults() SetDefault(OsuSetting.EditorShowSpeedChanges, false); SetDefault(OsuSetting.EditorScaleOrigin, EditorOrigin.GridCentre); SetDefault(OsuSetting.EditorRotationOrigin, EditorOrigin.GridCentre); + SetDefault(OsuSetting.EditorAdjustExistingObjectsOnTimingChanges, true); SetDefault(OsuSetting.HideCountryFlags, false); @@ -442,5 +443,6 @@ public enum OsuSetting EditorScaleOrigin, EditorRotationOrigin, EditorTimelineShowBreaks, + EditorAdjustExistingObjectsOnTimingChanges, } } diff --git a/osu.Game/Localisation/EditorStrings.cs b/osu.Game/Localisation/EditorStrings.cs index 19b783de9222..127bdd835519 100644 --- a/osu.Game/Localisation/EditorStrings.cs +++ b/osu.Game/Localisation/EditorStrings.cs @@ -40,9 +40,9 @@ public static class EditorStrings public static LocalisableString SetPreviewPointToCurrent => new TranslatableString(getKey(@"set_preview_point_to_current"), @"Set preview point to current time"); /// - /// "Move already placed notes when changing the offset / BPM" + /// "Move already placed objects when changing timing" /// - public static LocalisableString AdjustNotesOnOffsetBPMChange => new TranslatableString(getKey(@"adjust_notes_on_offset_bpm_change"), @"Move already placed notes when changing the offset / BPM"); + public static LocalisableString AdjustExistingObjectsOnTimingChanges => new TranslatableString(getKey(@"adjust_existing_objects_on_timing_changes"), @"Move already placed objects when changing timing"); /// /// "For editing (.olz)" diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 178b3f57b04c..8de0c7c33d4e 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -422,9 +422,9 @@ private void load(OsuConfigManager config) Items = new MenuItem[] { new EditorMenuItem(EditorStrings.SetPreviewPointToCurrent, MenuItemType.Standard, SetPreviewPointToCurrentTime), - new ToggleMenuItem(EditorStrings.AdjustNotesOnOffsetBPMChange) + new ToggleMenuItem(EditorStrings.AdjustExistingObjectsOnTimingChanges) { - State = { BindTarget = editorBeatmap.AdjustNotesOnOffsetBPMChange }, + State = { BindTarget = config.GetBindable(OsuSetting.EditorAdjustExistingObjectsOnTimingChanges) }, } } } diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index 154ebf3c8898..ad31c2ccc324 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -89,8 +89,6 @@ public partial class EditorBeatmap : TransactionalCommitComponent, IBeatmap, IBe public BindableInt PreviewTime { get; } - public Bindable AdjustNotesOnOffsetBPMChange { get; } = new Bindable(false); - private readonly IBeatmapProcessor beatmapProcessor; private readonly Dictionary> startTimeBindables = new Dictionary>(); diff --git a/osu.Game/Screens/Edit/Timing/GroupSection.cs b/osu.Game/Screens/Edit/Timing/GroupSection.cs index abcdf7e4ff19..13e802a8e46d 100644 --- a/osu.Game/Screens/Edit/Timing/GroupSection.cs +++ b/osu.Game/Screens/Edit/Timing/GroupSection.cs @@ -7,6 +7,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Configuration; using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; using osuTK; @@ -25,6 +26,9 @@ internal partial class GroupSection : CompositeDrawable [Resolved] protected EditorBeatmap Beatmap { get; private set; } = null!; + [Resolved] + private OsuConfigManager configManager { get; set; } = null!; + [Resolved] private EditorClock clock { get; set; } = null!; @@ -112,7 +116,7 @@ private void changeSelectedGroupTime(in double time) foreach (var cp in currentGroupItems) { // Only adjust hit object offsets if the group contains a timing control point - if (Beatmap.AdjustNotesOnOffsetBPMChange.Value && cp is TimingControlPoint tp) + if (cp is TimingControlPoint tp && configManager.Get(OsuSetting.EditorAdjustExistingObjectsOnTimingChanges)) { TimingSectionAdjustments.AdjustHitObjectOffset(Beatmap, tp, time - SelectedGroup.Value.Time); Beatmap.UpdateAllHitObjects(); diff --git a/osu.Game/Screens/Edit/Timing/TapTimingControl.cs b/osu.Game/Screens/Edit/Timing/TapTimingControl.cs index 91a0a43d62d0..f105c0072631 100644 --- a/osu.Game/Screens/Edit/Timing/TapTimingControl.cs +++ b/osu.Game/Screens/Edit/Timing/TapTimingControl.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; @@ -26,6 +27,9 @@ public partial class TapTimingControl : CompositeDrawable [Resolved] private EditorBeatmap beatmap { get; set; } = null!; + [Resolved] + private OsuConfigManager configManager { get; set; } = null!; + [Resolved] private Bindable selectedGroup { get; set; } = null!; @@ -209,7 +213,7 @@ private void adjustOffset(double adjust) foreach (var cp in currentGroupItems) { - if (beatmap.AdjustNotesOnOffsetBPMChange.Value && cp is TimingControlPoint tp) + if (cp is TimingControlPoint tp) { TimingSectionAdjustments.AdjustHitObjectOffset(beatmap, tp, adjust); beatmap.UpdateAllHitObjects(); @@ -236,7 +240,7 @@ private void adjustBpm(double adjust) double oldBeatLength = timing.BeatLength; timing.BeatLength = 60000 / (timing.BPM + adjust); - if (beatmap.AdjustNotesOnOffsetBPMChange.Value) + if (configManager.Get(OsuSetting.EditorAdjustExistingObjectsOnTimingChanges)) { beatmap.BeginChange(); TimingSectionAdjustments.SetHitObjectBPM(beatmap, timing, oldBeatLength); diff --git a/osu.Game/Screens/Edit/Timing/TimingSection.cs b/osu.Game/Screens/Edit/Timing/TimingSection.cs index e668120d0d4d..6a89dc134117 100644 --- a/osu.Game/Screens/Edit/Timing/TimingSection.cs +++ b/osu.Game/Screens/Edit/Timing/TimingSection.cs @@ -6,6 +6,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Configuration; using osu.Game.Graphics.UserInterfaceV2; namespace osu.Game.Screens.Edit.Timing @@ -16,6 +17,9 @@ internal partial class TimingSection : Section private LabelledSwitchButton omitBarLine = null!; private BPMTextBox bpmTextEntry = null!; + [Resolved] + private OsuConfigManager configManager { get; set; } = null!; + [BackgroundDependencyLoader] private void load() { @@ -46,7 +50,7 @@ void saveChanges() bpmTextEntry.OnCommit = (oldBeatLength, _) => { - if (!Beatmap.AdjustNotesOnOffsetBPMChange.Value || ControlPoint.Value == null) + if (!configManager.Get(OsuSetting.EditorAdjustExistingObjectsOnTimingChanges) || ControlPoint.Value == null) return; Beatmap.BeginChange(); From f84ee3996fdfa962885ee162257908b9c1bdfff5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 Nov 2024 17:54:43 +0900 Subject: [PATCH 086/120] Reduce semi-opaque layers at song select I made these changes while working on https://github.com/ppy/osu/pull/30579. Basically, it's hard to fix the ranks not loading while underneath the footer, and the transparency both looks bad, and is going away in the redesign. I've chosen values here that are moving *in the direction* of the new design without overhauling everything. - I know that there's still some transparency. I did this because it helps keep all current elements / colours contrasting without too much effort. - I completely removed the transparency adjustments on the beatmap panels. This always looked bad due to being applied per-layer, and I don't think it added much. --- osu.Game/Screens/Select/BeatmapCarousel.cs | 5 ----- osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs | 2 -- osu.Game/Screens/Select/FilterControl.cs | 4 ++-- osu.Game/Screens/Select/Footer.cs | 6 ++++-- 4 files changed, 6 insertions(+), 11 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 412e44cbdecc..e8fe191a2455 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -1116,11 +1116,6 @@ private void updateItem(DrawableCarouselItem item, DrawableCarouselItem? parent // adjusting the item's overall X position can cause it to become masked away when // child items (difficulties) are still visible. item.Header.X = offsetX(dist, visibleHalfHeight) - (parent?.X ?? 0); - - // We are applying a multiplicative alpha (which is internally done by nesting an - // additional container and setting that container's alpha) such that we can - // layer alpha transformations on top. - item.SetMultiplicativeAlpha(Math.Clamp(1.75f - 1.5f * dist, 0, 1)); } private enum PendingScrollOperation diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs index 755008d37096..10921c331eff 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs @@ -86,8 +86,6 @@ protected DrawableCarouselItem() }; } - public void SetMultiplicativeAlpha(float alpha) => Header.BorderContainer.Alpha = alpha; - protected override void LoadComplete() { base.LoadComplete(); diff --git a/osu.Game/Screens/Select/FilterControl.cs b/osu.Game/Screens/Select/FilterControl.cs index 877db7531721..be91aef1a3e2 100644 --- a/osu.Game/Screens/Select/FilterControl.cs +++ b/osu.Game/Screens/Select/FilterControl.cs @@ -97,8 +97,8 @@ private void load(OsuColour colours, OsuConfigManager config) { new Box { - Colour = Color4.Black, - Alpha = 0.8f, + Colour = OsuColour.Gray(0.05f), + Alpha = 0.96f, Width = 2, RelativeSizeAxes = Axes.Both, }, diff --git a/osu.Game/Screens/Select/Footer.cs b/osu.Game/Screens/Select/Footer.cs index 933df2464aa1..2d919c3247ee 100644 --- a/osu.Game/Screens/Select/Footer.cs +++ b/osu.Game/Screens/Select/Footer.cs @@ -12,6 +12,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; +using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; namespace osu.Game.Screens.Select @@ -82,14 +83,15 @@ public Footer() { RelativeSizeAxes = Axes.Both, Size = Vector2.One, - Colour = Color4.Black.Opacity(0.5f), + Colour = OsuColour.Gray(0.1f), + Alpha = 0.96f, }, modeLight = new Box { RelativeSizeAxes = Axes.X, Height = 3, Position = new Vector2(0, -3), - Colour = Color4.Black, + Colour = OsuColour.Gray(0.1f), }, new FillFlowContainer { From c37e4877e24ea295b788e54f3048643629975791 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 11 Nov 2024 10:08:26 +0100 Subject: [PATCH 087/120] Move setting back to timing panel --- osu.Game/Screens/Edit/Editor.cs | 4 ---- osu.Game/Screens/Edit/Timing/TimingSection.cs | 7 +++++++ 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 8de0c7c33d4e..644e1afb3bf5 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -422,10 +422,6 @@ private void load(OsuConfigManager config) Items = new MenuItem[] { new EditorMenuItem(EditorStrings.SetPreviewPointToCurrent, MenuItemType.Standard, SetPreviewPointToCurrentTime), - new ToggleMenuItem(EditorStrings.AdjustExistingObjectsOnTimingChanges) - { - State = { BindTarget = config.GetBindable(OsuSetting.EditorAdjustExistingObjectsOnTimingChanges) }, - } } } } diff --git a/osu.Game/Screens/Edit/Timing/TimingSection.cs b/osu.Game/Screens/Edit/Timing/TimingSection.cs index 6a89dc134117..ae1ac02dd647 100644 --- a/osu.Game/Screens/Edit/Timing/TimingSection.cs +++ b/osu.Game/Screens/Edit/Timing/TimingSection.cs @@ -8,6 +8,7 @@ using osu.Game.Beatmaps.ControlPoints; using osu.Game.Configuration; using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Localisation; namespace osu.Game.Screens.Edit.Timing { @@ -25,6 +26,12 @@ private void load() { Flow.AddRange(new Drawable[] { + new LabelledSwitchButton + { + Label = EditorStrings.AdjustExistingObjectsOnTimingChanges, + FixedLabelWidth = 220, + Current = configManager.GetBindable(OsuSetting.EditorAdjustExistingObjectsOnTimingChanges), + }, new TapTimingControl(), bpmTextEntry = new BPMTextBox(), timeSignature = new LabelledTimeSignature From d29dd2c223fb71befd212fa6d0573faf9c3554dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 11 Nov 2024 11:04:02 +0100 Subject: [PATCH 088/120] Remove unused using directives --- osu.Game/Screens/Select/FilterControl.cs | 1 - osu.Game/Screens/Select/Footer.cs | 2 -- 2 files changed, 3 deletions(-) diff --git a/osu.Game/Screens/Select/FilterControl.cs b/osu.Game/Screens/Select/FilterControl.cs index be91aef1a3e2..b221296ba825 100644 --- a/osu.Game/Screens/Select/FilterControl.cs +++ b/osu.Game/Screens/Select/FilterControl.cs @@ -28,7 +28,6 @@ using osu.Game.Rulesets.Mods; using osu.Game.Screens.Select.Filter; using osuTK; -using osuTK.Graphics; using osuTK.Input; namespace osu.Game.Screens.Select diff --git a/osu.Game/Screens/Select/Footer.cs b/osu.Game/Screens/Select/Footer.cs index 2d919c3247ee..1d05f644b791 100644 --- a/osu.Game/Screens/Select/Footer.cs +++ b/osu.Game/Screens/Select/Footer.cs @@ -6,8 +6,6 @@ using System.Collections.Generic; using System.Linq; using osuTK; -using osuTK.Graphics; -using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; From ee4d58544d7a756fea19f0d59a64a2b0a0849674 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 Nov 2024 21:57:01 +0900 Subject: [PATCH 089/120] Update framework (and other common packages) --- osu.Desktop/osu.Desktop.csproj | 2 +- osu.Game/osu.Game.csproj | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj index 904f5edf2bfb..2cdbaddf723b 100644 --- a/osu.Desktop/osu.Desktop.csproj +++ b/osu.Desktop/osu.Desktop.csproj @@ -26,7 +26,7 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 7405a7c587fa..855ba3374b48 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -20,7 +20,7 @@ - + @@ -35,9 +35,9 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + - + From 8c5785fdf63e30bb58d9ff587a3718892278c8be Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 Nov 2024 22:57:26 +0900 Subject: [PATCH 090/120] Make math more logical --- osu.Game/Screens/Select/BeatmapCarousel.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index ba200315097f..5716b2c918be 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -1280,14 +1280,14 @@ public PaddedScrollbar() } private const float top_padding = 10; - private const float bottom_padding = 80; + private const float bottom_padding = 70; protected override float ToScrollbarPosition(float scrollPosition) { if (Precision.AlmostEquals(0, ScrollableExtent)) return 0; - return top_padding + (ScrollbarMovementExtent - bottom_padding) * (scrollPosition / ScrollableExtent); + return top_padding + (ScrollbarMovementExtent - (top_padding + bottom_padding)) * (scrollPosition / ScrollableExtent); } protected override float FromScrollbarPosition(float scrollbarPosition) @@ -1295,7 +1295,7 @@ protected override float FromScrollbarPosition(float scrollbarPosition) if (Precision.AlmostEquals(0, ScrollbarMovementExtent)) return 0; - return ScrollableExtent * ((scrollbarPosition - top_padding) / (ScrollbarMovementExtent - bottom_padding)); + return ScrollableExtent * ((scrollbarPosition - top_padding) / (ScrollbarMovementExtent - (top_padding + bottom_padding))); } } From 3268008d215a7bc1e32fdcc7741688bbca8eed29 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Tue, 12 Nov 2024 04:07:27 -0500 Subject: [PATCH 091/120] Fix stage line alignment in mania not matching stable --- .../Skinning/Legacy/LegacyStageBackground.cs | 5 +++-- osu.Game.Rulesets.Mania/UI/ColumnFlow.cs | 6 ++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyStageBackground.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyStageBackground.cs index 758c8dd34752..71618a4bc368 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyStageBackground.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyStageBackground.cs @@ -54,7 +54,8 @@ private void load(ISkinSource skin, StageDefinition stageDefinition) }, columnBackgrounds = new ColumnFlow(stageDefinition) { - RelativeSizeAxes = Axes.Y + RelativeSizeAxes = Axes.Y, + Masking = false, }, new HitTargetInsetContainer { @@ -126,8 +127,8 @@ private void load(ISkinSource skin) }, new Container { + X = isLastColumn ? -0.16f : 0, Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, RelativeSizeAxes = Axes.Y, Width = rightLineWidth, Scale = new Vector2(0.740f, 1), diff --git a/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs b/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs index f44444879743..5614a13a4819 100644 --- a/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs +++ b/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs @@ -28,6 +28,12 @@ public partial class ColumnFlow : CompositeDrawable private readonly FillFlowContainer> columns; private readonly StageDefinition stageDefinition; + public new bool Masking + { + get => base.Masking; + set => base.Masking = value; + } + public ColumnFlow(StageDefinition stageDefinition) { this.stageDefinition = stageDefinition; From 2ae8c36d17fb6a5067a1ccb03e0fb7cd194496e4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 13 Nov 2024 17:39:21 +0900 Subject: [PATCH 092/120] Increase idle time before gameplay loads when hovering controls --- osu.Game/Screens/Play/PlayerLoader.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 9b4bf7d63365..f5671cd9343f 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -224,7 +224,7 @@ private void load(SessionStatics sessionStatics, OsuConfigManager config) } }, }, - idleTracker = new IdleTracker(750), + idleTracker = new IdleTracker(1500), sampleRestart = new SkinnableSound(new SampleInfo(@"Gameplay/restart", @"pause-retry-click")) }; From 2dbc275bb8e27cab2370eff682422a1be1d1fc8c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 13 Nov 2024 17:54:05 +0900 Subject: [PATCH 093/120] Make quick restart even faster --- osu.Game/Screens/Play/PlayerLoader.cs | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 9b4bf7d63365..51268e5dcedb 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -51,6 +51,8 @@ public partial class PlayerLoader : ScreenWithBeatmapBackground public override bool? AllowGlobalTrackControl => false; + public override float BackgroundParallaxAmount => quickRestart ? 0 : 1; + // Here because IsHovered will not update unless we do so. public override bool HandlePositionalInput => true; @@ -387,7 +389,7 @@ protected override void Update() // We need to perform this check here rather than in OnHover as any number of children of VisualSettings // may also be handling the hover events. - if (inputManager.HoveredDrawables.Contains(VisualSettings)) + if (inputManager.HoveredDrawables.Contains(VisualSettings) || quickRestart) { // Preview user-defined background dim and blur when hovered on the visual settings panel. ApplyToBackground(b => @@ -458,8 +460,9 @@ private void contentIn(double delayBeforeSideDisplays = 0) if (quickRestart) { - prepareNewPlayer(); - content.ScaleTo(1, 650, Easing.OutQuint); + // A slight delay is added here to avoid an awkward stutter during the initial animation. + Scheduler.AddDelayed(prepareNewPlayer, 100); + content.ScaleTo(1); } else { @@ -467,15 +470,15 @@ private void contentIn(double delayBeforeSideDisplays = 0) .ScaleTo(1, 650, Easing.OutQuint) .Then() .Schedule(prepareNewPlayer); - } - using (BeginDelayedSequence(delayBeforeSideDisplays)) - { - settingsScroll.FadeInFromZero(500, Easing.Out) - .MoveToX(0, 500, Easing.OutQuint); + using (BeginDelayedSequence(delayBeforeSideDisplays)) + { + settingsScroll.FadeInFromZero(500, Easing.Out) + .MoveToX(0, 500, Easing.OutQuint); - disclaimers.FadeInFromZero(500, Easing.Out) - .MoveToX(0, 500, Easing.OutQuint); + disclaimers.FadeInFromZero(500, Easing.Out) + .MoveToX(0, 500, Easing.OutQuint); + } } AddRangeInternal(new[] @@ -565,7 +568,7 @@ private void pushWhenLoaded() else this.Exit(); }); - }, 500); + }, quickRestart ? 0 : 500); } private void cancelLoad() From 68945daa4094455cb3361c0b1151e14cdadf82da Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 13 Nov 2024 19:27:20 +0900 Subject: [PATCH 094/120] Add confirmation when pressing 'q' to quit at the main menu Kinda a weird key. I feel like this deserves a confirmation step unlike mouse clicking the exit button. Addresses https://github.com/ppy/osu/discussions/30471. --- .../TestSceneDailyChallengeIntro.cs | 2 +- .../UserInterface/TestSceneButtonSystem.cs | 2 +- .../UserInterface/TestSceneMainMenuButton.cs | 6 ++--- osu.Game/Screens/Menu/ButtonSystem.cs | 26 +++++++++---------- osu.Game/Screens/Menu/DailyChallengeButton.cs | 3 ++- osu.Game/Screens/Menu/MainMenu.cs | 4 +-- osu.Game/Screens/Menu/MainMenuButton.cs | 12 ++++----- 7 files changed, 28 insertions(+), 27 deletions(-) diff --git a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeIntro.cs b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeIntro.cs index f1a2d6b5f2a3..7619328e688b 100644 --- a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeIntro.cs +++ b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeIntro.cs @@ -38,7 +38,7 @@ private void load() Add(metadataClient); // add button to observe for daily challenge changes and perform its logic. - Add(new DailyChallengeButton(@"button-default-select", new Color4(102, 68, 204, 255), _ => { }, 0, Key.D)); + Add(new DailyChallengeButton(@"button-default-select", new Color4(102, 68, 204, 255), (_, _) => { }, 0, Key.D)); } [Test] diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneButtonSystem.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneButtonSystem.cs index 8f72be37df6f..d8baca6d232f 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneButtonSystem.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneButtonSystem.cs @@ -110,7 +110,7 @@ public void TestShortcutKeys(Key key, Key? subMenuEnterKey) break; case Key.Q: - buttons.OnExit = action; + buttons.OnExit = _ => action(); break; case Key.O: diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneMainMenuButton.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneMainMenuButton.cs index 41543669ebbe..4925facd8a8a 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneMainMenuButton.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneMainMenuButton.cs @@ -30,7 +30,7 @@ public partial class TestSceneMainMenuButton : OsuTestScene public void TestStandardButton() { AddStep("add button", () => Child = new MainMenuButton( - ButtonSystemStrings.Solo, @"button-default-select", OsuIcon.Player, new Color4(102, 68, 204, 255), _ => { }, 0, Key.P) + ButtonSystemStrings.Solo, @"button-default-select", OsuIcon.Player, new Color4(102, 68, 204, 255), (_, _) => { }, 0, Key.P) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -87,7 +87,7 @@ public void TestDailyChallengeButton() Origin = Anchor.Centre, AutoSizeAxes = Axes.Both, CachedDependencies = [(typeof(INotificationOverlay), notificationOverlay)], - Child = new DailyChallengeButton(@"button-default-select", new Color4(102, 68, 204, 255), _ => { }, 0, Key.D) + Child = new DailyChallengeButton(@"button-default-select", new Color4(102, 68, 204, 255), (_, _) => { }, 0, Key.D) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -161,7 +161,7 @@ public void TestDailyChallengeButtonOldChallenge() Origin = Anchor.Centre, AutoSizeAxes = Axes.Both, CachedDependencies = [(typeof(INotificationOverlay), notificationOverlay)], - Child = new DailyChallengeButton(@"button-default-select", new Color4(102, 68, 204, 255), _ => { }, 0, Key.D) + Child = new DailyChallengeButton(@"button-default-select", new Color4(102, 68, 204, 255), (_, _) => { }, 0, Key.D) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index 0997ab80039f..41920605b07e 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -41,7 +41,7 @@ public partial class ButtonSystem : Container, IStateful, IKe public Action? OnEditBeatmap; public Action? OnEditSkin; - public Action? OnExit; + public Action? OnExit; public Action? OnBeatmapListing; public Action? OnSolo; public Action? OnSettings; @@ -104,11 +104,11 @@ public ButtonSystem() buttonArea.AddRange(new Drawable[] { - new MainMenuButton(ButtonSystemStrings.Settings, string.Empty, OsuIcon.Settings, new Color4(85, 85, 85, 255), _ => OnSettings?.Invoke(), Key.O, Key.S) + new MainMenuButton(ButtonSystemStrings.Settings, string.Empty, OsuIcon.Settings, new Color4(85, 85, 85, 255), (_, _) => OnSettings?.Invoke(), Key.O, Key.S) { Padding = new MarginPadding { Right = WEDGE_WIDTH }, }, - backButton = new MainMenuButton(ButtonSystemStrings.Back, @"back-to-top", OsuIcon.PrevCircle, new Color4(51, 58, 94, 255), _ => State = ButtonSystemState.TopLevel) + backButton = new MainMenuButton(ButtonSystemStrings.Back, @"back-to-top", OsuIcon.PrevCircle, new Color4(51, 58, 94, 255), (_, _) => State = ButtonSystemState.TopLevel) { Padding = new MarginPadding { Right = WEDGE_WIDTH }, VisibleStateMin = ButtonSystemState.Play, @@ -132,7 +132,7 @@ public ButtonSystem() [BackgroundDependencyLoader] private void load(AudioManager audio, IdleTracker? idleTracker, GameHost host) { - buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Solo, @"button-default-select", OsuIcon.Player, new Color4(102, 68, 204, 255), _ => OnSolo?.Invoke(), Key.P) + buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Solo, @"button-default-select", OsuIcon.Player, new Color4(102, 68, 204, 255), (_, _) => OnSolo?.Invoke(), Key.P) { Padding = new MarginPadding { Left = WEDGE_WIDTH }, }); @@ -141,22 +141,22 @@ private void load(AudioManager audio, IdleTracker? idleTracker, GameHost host) buttonsPlay.Add(new DailyChallengeButton(@"button-daily-select", new Color4(94, 63, 186, 255), onDailyChallenge, Key.D)); buttonsPlay.ForEach(b => b.VisibleState = ButtonSystemState.Play); - buttonsEdit.Add(new MainMenuButton(EditorStrings.BeatmapEditor.ToLower(), @"button-default-select", OsuIcon.Beatmap, new Color4(238, 170, 0, 255), _ => OnEditBeatmap?.Invoke(), Key.B, Key.E) + buttonsEdit.Add(new MainMenuButton(EditorStrings.BeatmapEditor.ToLower(), @"button-default-select", OsuIcon.Beatmap, new Color4(238, 170, 0, 255), (_, _) => OnEditBeatmap?.Invoke(), Key.B, Key.E) { Padding = new MarginPadding { Left = WEDGE_WIDTH }, }); - buttonsEdit.Add(new MainMenuButton(SkinEditorStrings.SkinEditor.ToLower(), @"button-default-select", OsuIcon.SkinB, new Color4(220, 160, 0, 255), _ => OnEditSkin?.Invoke(), Key.S)); + buttonsEdit.Add(new MainMenuButton(SkinEditorStrings.SkinEditor.ToLower(), @"button-default-select", OsuIcon.SkinB, new Color4(220, 160, 0, 255), (_, _) => OnEditSkin?.Invoke(), Key.S)); buttonsEdit.ForEach(b => b.VisibleState = ButtonSystemState.Edit); - buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Play, @"button-play-select", OsuIcon.Logo, new Color4(102, 68, 204, 255), _ => State = ButtonSystemState.Play, Key.P, Key.M, Key.L) + buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Play, @"button-play-select", OsuIcon.Logo, new Color4(102, 68, 204, 255), (_, _) => State = ButtonSystemState.Play, Key.P, Key.M, Key.L) { Padding = new MarginPadding { Left = WEDGE_WIDTH }, }); - buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Edit, @"button-play-select", OsuIcon.EditCircle, new Color4(238, 170, 0, 255), _ => State = ButtonSystemState.Edit, Key.E)); - buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Browse, @"button-default-select", OsuIcon.Beatmap, new Color4(165, 204, 0, 255), _ => OnBeatmapListing?.Invoke(), Key.B, Key.D)); + buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Edit, @"button-play-select", OsuIcon.EditCircle, new Color4(238, 170, 0, 255), (_, _) => State = ButtonSystemState.Edit, Key.E)); + buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Browse, @"button-default-select", OsuIcon.Beatmap, new Color4(165, 204, 0, 255), (_, _) => OnBeatmapListing?.Invoke(), Key.B, Key.D)); if (host.CanExit) - buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Exit, string.Empty, OsuIcon.CrossCircle, new Color4(238, 51, 153, 255), _ => OnExit?.Invoke(), Key.Q)); + buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Exit, string.Empty, OsuIcon.CrossCircle, new Color4(238, 51, 153, 255), (_, e) => OnExit?.Invoke(e), Key.Q)); buttonArea.AddRange(buttonsPlay); buttonArea.AddRange(buttonsEdit); @@ -179,7 +179,7 @@ private void load(AudioManager audio, IdleTracker? idleTracker, GameHost host) sampleLogoSwoosh = audio.Samples.Get(@"Menu/osu-logo-swoosh"); } - private void onMultiplayer(MainMenuButton _) + private void onMultiplayer(MainMenuButton mainMenuButton, UIEvent uiEvent) { if (api.State.Value != APIState.Online) { @@ -190,7 +190,7 @@ private void onMultiplayer(MainMenuButton _) OnMultiplayer?.Invoke(); } - private void onPlaylists(MainMenuButton _) + private void onPlaylists(MainMenuButton mainMenuButton, UIEvent uiEvent) { if (api.State.Value != APIState.Online) { @@ -201,7 +201,7 @@ private void onPlaylists(MainMenuButton _) OnPlaylists?.Invoke(); } - private void onDailyChallenge(MainMenuButton button) + private void onDailyChallenge(MainMenuButton button, UIEvent uiEvent) { if (api.State.Value != APIState.Online) { diff --git a/osu.Game/Screens/Menu/DailyChallengeButton.cs b/osu.Game/Screens/Menu/DailyChallengeButton.cs index 4dbebf0ae9ec..44a53efa7b95 100644 --- a/osu.Game/Screens/Menu/DailyChallengeButton.cs +++ b/osu.Game/Screens/Menu/DailyChallengeButton.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Events; using osu.Framework.Threading; using osu.Framework.Utils; using osu.Game.Beatmaps.Drawables; @@ -50,7 +51,7 @@ public partial class DailyChallengeButton : MainMenuButton [Resolved] private SessionStatics statics { get; set; } = null!; - public DailyChallengeButton(string sampleName, Color4 colour, Action? clickAction = null, params Key[] triggerKeys) + public DailyChallengeButton(string sampleName, Color4 colour, Action? clickAction = null, params Key[] triggerKeys) : base(ButtonSystemStrings.DailyChallenge, sampleName, OsuIcon.DailyChallenge, colour, clickAction, triggerKeys) { BaseSize = new Vector2(ButtonSystem.BUTTON_WIDTH * 1.3f, ButtonArea.BUTTON_AREA_HEIGHT); diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index 346bdcb75117..35c6bab81b7b 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -153,9 +153,9 @@ private void load(BeatmapListingOverlay beatmapListing, SettingsOverlay settings else this.Push(new DailyChallengeIntro(room)); }, - OnExit = () => + OnExit = e => { - exitConfirmedViaHoldOrClick = true; + exitConfirmedViaHoldOrClick = e is MouseEvent; this.Exit(); } } diff --git a/osu.Game/Screens/Menu/MainMenuButton.cs b/osu.Game/Screens/Menu/MainMenuButton.cs index 4df5e6d309c9..f8824795d8b0 100644 --- a/osu.Game/Screens/Menu/MainMenuButton.cs +++ b/osu.Game/Screens/Menu/MainMenuButton.cs @@ -64,7 +64,7 @@ public ButtonSystemState VisibleState protected Vector2 BaseSize { get; init; } = new Vector2(ButtonSystem.BUTTON_WIDTH, ButtonArea.BUTTON_AREA_HEIGHT); - private readonly Action? clickAction; + private readonly Action? clickAction; private readonly Container background; private readonly Drawable backgroundContent; @@ -84,7 +84,7 @@ public ButtonSystemState VisibleState public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => background.ReceivePositionalInputAt(screenSpacePos); - public MainMenuButton(LocalisableString text, string sampleName, IconUsage symbol, Color4 colour, Action? clickAction = null, params Key[] triggerKeys) + public MainMenuButton(LocalisableString text, string sampleName, IconUsage symbol, Color4 colour, Action? clickAction = null, params Key[] triggerKeys) { this.sampleName = sampleName; this.clickAction = clickAction; @@ -263,7 +263,7 @@ protected override void OnMouseUp(MouseUpEvent e) protected override bool OnClick(ClickEvent e) { - trigger(); + trigger(e); return true; } @@ -274,19 +274,19 @@ protected override bool OnKeyDown(KeyDownEvent e) if (TriggerKeys.Contains(e.Key)) { - trigger(); + trigger(e); return true; } return false; } - private void trigger() + private void trigger(UIEvent e) { sampleChannel = sampleClick?.GetChannel(); sampleChannel?.Play(); - clickAction?.Invoke(this); + clickAction?.Invoke(this, e); boxHoverLayer.ClearTransforms(); boxHoverLayer.Alpha = 0.9f; From f15b6b1d71a0b9d45b66a0861fc5d3878755a4fa Mon Sep 17 00:00:00 2001 From: OliBomby Date: Wed, 13 Nov 2024 14:09:19 +0100 Subject: [PATCH 095/120] Create LegacyBeatmapExporterTest --- .../Beatmaps/IO/LegacyBeatmapExporterTest.cs | 64 ++++++++++++++++++ .../Archives/decimal-timing-beatmap.olz | Bin 0 -> 990 bytes 2 files changed, 64 insertions(+) create mode 100644 osu.Game.Tests/Beatmaps/IO/LegacyBeatmapExporterTest.cs create mode 100644 osu.Game.Tests/Resources/Archives/decimal-timing-beatmap.olz diff --git a/osu.Game.Tests/Beatmaps/IO/LegacyBeatmapExporterTest.cs b/osu.Game.Tests/Beatmaps/IO/LegacyBeatmapExporterTest.cs new file mode 100644 index 000000000000..7973867114f9 --- /dev/null +++ b/osu.Game.Tests/Beatmaps/IO/LegacyBeatmapExporterTest.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 System; +using System.IO; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Extensions; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Database; +using osu.Game.Tests.Resources; +using osu.Game.Tests.Visual; +using MemoryStream = System.IO.MemoryStream; + +namespace osu.Game.Tests.Beatmaps.IO +{ + [HeadlessTest] + public partial class LegacyBeatmapExporterTest : OsuTestScene + { + [Resolved] + private BeatmapManager beatmaps { get; set; } = null!; + + [Test] + public void TestObjectsSnappedAfterTruncatingExport() + { + IWorkingBeatmap beatmap = null!; + MemoryStream outStream = null!; + + // Ensure importer encoding is correct + AddStep("import beatmap", () => beatmap = importBeatmapFromArchives(@"decimal-timing-beatmap.olz")); + AddAssert("timing point has decimal offset", () => Math.Abs(beatmap.Beatmap.ControlPointInfo.TimingPoints[0].Time - 284.725) < 0.001); + AddAssert("kiai has decimal offset", () => Math.Abs(beatmap.Beatmap.ControlPointInfo.EffectPoints[0].Time - 28520.019) < 0.001); + AddAssert("hit object has decimal offset", () => Math.Abs(beatmap.Beatmap.HitObjects[0].StartTime - 28520.019) < 0.001); + + // Ensure exporter legacy conversion is correct + AddStep("export", () => + { + outStream = new MemoryStream(); + + new LegacyBeatmapExporter(LocalStorage) + .ExportToStream((BeatmapSetInfo)beatmap.BeatmapInfo.BeatmapSet!, outStream, null); + }); + + AddStep("import beatmap again", () => beatmap = importBeatmapFromStream(outStream)); + AddAssert("timing point has truncated offset", () => Math.Abs(beatmap.Beatmap.ControlPointInfo.TimingPoints[0].Time - 284) < 0.001); + AddAssert("kiai is snapped", () => Math.Abs(beatmap.Beatmap.ControlPointInfo.EffectPoints[0].Time - 28519) < 0.001); + AddAssert("hit object is snapped", () => Math.Abs(beatmap.Beatmap.HitObjects[0].StartTime - 28519) < 0.001); + } + + private IWorkingBeatmap importBeatmapFromStream(Stream stream) + { + var imported = beatmaps.Import(new ImportTask(stream, "filename.osz")).GetResultSafely(); + return imported.AsNonNull().PerformRead(s => beatmaps.GetWorkingBeatmap(s.Beatmaps[0])); + } + + private IWorkingBeatmap importBeatmapFromArchives(string filename) + { + var imported = beatmaps.Import(new ImportTask(TestResources.OpenResource($@"Archives/{filename}"), filename)).GetResultSafely(); + return imported.AsNonNull().PerformRead(s => beatmaps.GetWorkingBeatmap(s.Beatmaps[0])); + } + } +} diff --git a/osu.Game.Tests/Resources/Archives/decimal-timing-beatmap.olz b/osu.Game.Tests/Resources/Archives/decimal-timing-beatmap.olz new file mode 100644 index 0000000000000000000000000000000000000000..38dedc35d15f1a11baa49f00917acf93ee2b23eb GIT binary patch literal 990 zcmWIWW@Zs#U|`^2m|l<@`RKWA-YF&q1~FC!26v!nXiFO#b zw&N&j>KL znO3-0PWb%6t>+K57#VpNeE9Hmt9*dd^{Hx?cku7*jW;-d@=k8K>={M>7u^ek+@IYv zoj>3CgTO4~DH^7hy}CR}=M~dEPXG8CSux{4?Ys5|*7u$6t27v#H=L`x*!%6$qWV`w zn`f53%jw*-K8Ec~qH=#!{|i>jkF{kB8-JcNwOeBtS@cOD#_l8g{o^JJC)s%JoBKQ8 zU{1T6mHY$ep4)*hMK)!}mT{kX%IA3eUq;NuJ&xgD&i&r8yq8@A-FR8}pm; zlYYnlVAouHmh*1ASkV0=EPq|!I6cYAnl-^HB0l#sTh;x^+h=?>{4?)k_V!~p9V5#3 z-MmM?2AGz!%` zar*FwypriXi3$8=yG3F*OL}&lvF`WtH+)+@x81~N&!*$6&1PkWetyODN*{pU6T+^F&&vvb6;ZXM071Pt! z{XC79`erV<5EK^S7rR{LeA<#Vqq literal 0 HcmV?d00001 From b6eff38520b632d1fe311458ce761e27e0be1ce5 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Wed, 13 Nov 2024 14:23:28 +0100 Subject: [PATCH 096/120] Fix timing point truncation in legacy beatmap export --- osu.Game/Database/LegacyBeatmapExporter.cs | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/osu.Game/Database/LegacyBeatmapExporter.cs b/osu.Game/Database/LegacyBeatmapExporter.cs index 17c2c8c88de7..eb4842558855 100644 --- a/osu.Game/Database/LegacyBeatmapExporter.cs +++ b/osu.Game/Database/LegacyBeatmapExporter.cs @@ -59,7 +59,25 @@ public LegacyBeatmapExporter(Storage storage) }; // Convert beatmap elements to be compatible with legacy format - // So we truncate time and position values to integers, and convert paths with multiple segments to bezier curves + // So we truncate time and position values to integers, and convert paths with multiple segments to Bézier curves + + // We must first truncate all timing points and move all objects in the timing section with it to ensure everything stays snapped + for (int i = 0; i < playableBeatmap.ControlPointInfo.TimingPoints.Count; i++) + { + var timingPoint = playableBeatmap.ControlPointInfo.TimingPoints[i]; + double offset = Math.Floor(timingPoint.Time) - timingPoint.Time; + double nextTimingPointTime = i + 1 < playableBeatmap.ControlPointInfo.TimingPoints.Count + ? playableBeatmap.ControlPointInfo.TimingPoints[i + 1].Time + : double.PositiveInfinity; + + // Offset all control points in the timing section (including the current one) + foreach (var controlPoint in playableBeatmap.ControlPointInfo.AllControlPoints.Where(o => o.Time >= timingPoint.Time && o.Time < nextTimingPointTime)) + controlPoint.Time += offset; + + // Offset all hit objects in the timing section + foreach (var hitObject in playableBeatmap.HitObjects.Where(o => o.StartTime >= timingPoint.Time && o.StartTime < nextTimingPointTime)) + hitObject.StartTime += offset; + } foreach (var controlPoint in playableBeatmap.ControlPointInfo.AllControlPoints) controlPoint.Time = Math.Floor(controlPoint.Time); From f597568476bdce8930bbdb30fa6fc937c1f2964f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 14 Nov 2024 01:23:21 +0900 Subject: [PATCH 097/120] Fix test failure due to restart happening too fast --- osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs index 1949808dfe51..cf813cfd5154 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs @@ -523,7 +523,7 @@ public void TestQuickRetry() AddUntilStep("restart completed", () => getCurrentPlayer() != null && getCurrentPlayer() != previousPlayer); AddStep("release quick retry key", () => InputManager.ReleaseKey(Key.Tilde)); - AddUntilStep("wait for player", () => getCurrentPlayer()?.LoadState == LoadState.Ready); + AddUntilStep("wait for player", () => getCurrentPlayer()?.LoadState >= LoadState.Ready); AddUntilStep("time reached zero", () => getCurrentPlayer()?.GameplayClockContainer.CurrentTime > 0); AddUntilStep("skip button not visible", () => !checkSkipButtonVisible()); From 7e96ae6da4492fc26fe7e4c0cf8f13cfb26c6739 Mon Sep 17 00:00:00 2001 From: Shin Morisawa Date: Thu, 14 Nov 2024 06:54:29 +0900 Subject: [PATCH 098/120] change stuff. (idk what exactly) --- osu.Game/Beatmaps/Drawables/DifficultyIconTooltip.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/Drawables/DifficultyIconTooltip.cs b/osu.Game/Beatmaps/Drawables/DifficultyIconTooltip.cs index 36ddb6030ebb..5d84bbc8ad21 100644 --- a/osu.Game/Beatmaps/Drawables/DifficultyIconTooltip.cs +++ b/osu.Game/Beatmaps/Drawables/DifficultyIconTooltip.cs @@ -146,7 +146,7 @@ public void SetContent(DifficultyIconTooltipContent content) approachRate.Text = @" AR: " + adjustedDifficulty.ApproachRate.ToString(@"0.##"); overallDifficulty.Text = @" OD: " + adjustedDifficulty.OverallDifficulty.ToString(@"0.##"); - length.Text = "Length: " + TimeSpan.FromMilliseconds(displayedContent.BeatmapInfo.Length / rate).ToString(@"mm\:ss"); + length.Text = "Length: " + TimeSpan.FromMilliseconds(displayedContent.BeatmapInfo.Length / rate).ToString(@"hh\:mm\:ss"); bpm.Text = " BPM: " + Math.Round(bpmAdjusted, 0); } From a3da10ff44979139349f8dc2a59c2ccbcbb12b77 Mon Sep 17 00:00:00 2001 From: Shin Morisawa Date: Thu, 14 Nov 2024 07:19:18 +0900 Subject: [PATCH 099/120] fixed said issue --- osu.Game/Beatmaps/Drawables/DifficultyIconTooltip.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/Drawables/DifficultyIconTooltip.cs b/osu.Game/Beatmaps/Drawables/DifficultyIconTooltip.cs index 5d84bbc8ad21..6c74ce573806 100644 --- a/osu.Game/Beatmaps/Drawables/DifficultyIconTooltip.cs +++ b/osu.Game/Beatmaps/Drawables/DifficultyIconTooltip.cs @@ -146,7 +146,10 @@ public void SetContent(DifficultyIconTooltipContent content) approachRate.Text = @" AR: " + adjustedDifficulty.ApproachRate.ToString(@"0.##"); overallDifficulty.Text = @" OD: " + adjustedDifficulty.OverallDifficulty.ToString(@"0.##"); - length.Text = "Length: " + TimeSpan.FromMilliseconds(displayedContent.BeatmapInfo.Length / rate).ToString(@"hh\:mm\:ss"); + TimeSpan lengthTimeSpan = TimeSpan.FromMilliseconds(displayedContent.BeatmapInfo.Length / rate); + length.Text = "Length: " + (lengthTimeSpan.Hours > 0 + ? lengthTimeSpan.ToString(@"hh\:mm\:ss") + : lengthTimeSpan.ToString(@"mm\:ss")); bpm.Text = " BPM: " + Math.Round(bpmAdjusted, 0); } From d37c1bb6d04269f32d9739dcc74fb781bb937358 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 14 Nov 2024 13:37:53 +0900 Subject: [PATCH 100/120] Remove redundant null initialisation and apply nullability --- osu.Game/Beatmaps/StarDifficulty.cs | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/osu.Game/Beatmaps/StarDifficulty.cs b/osu.Game/Beatmaps/StarDifficulty.cs index 51358fcd7f11..9f7a92fe469b 100644 --- a/osu.Game/Beatmaps/StarDifficulty.cs +++ b/osu.Game/Beatmaps/StarDifficulty.cs @@ -1,9 +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 JetBrains.Annotations; using osu.Framework.Utils; using osu.Game.Rulesets.Difficulty; @@ -25,20 +22,18 @@ public readonly struct StarDifficulty /// The difficulty attributes computed for the given beatmap. /// Might not be available if the star difficulty is associated with a beatmap that's not locally available. /// - [CanBeNull] - public readonly DifficultyAttributes DifficultyAttributes; + public readonly DifficultyAttributes? DifficultyAttributes; /// /// The performance attributes computed for a perfect score on the given beatmap. /// Might not be available if the star difficulty is associated with a beatmap that's not locally available. /// - [CanBeNull] - public readonly PerformanceAttributes PerformanceAttributes; + public readonly PerformanceAttributes? PerformanceAttributes; /// /// Creates a structure. /// - public StarDifficulty([NotNull] DifficultyAttributes difficulty, [NotNull] PerformanceAttributes performance) + public StarDifficulty(DifficultyAttributes difficulty, PerformanceAttributes performance) { Stars = double.IsFinite(difficulty.StarRating) ? difficulty.StarRating : 0; MaxCombo = difficulty.MaxCombo; @@ -55,7 +50,6 @@ public StarDifficulty(double starDifficulty, int maxCombo) { Stars = double.IsFinite(starDifficulty) ? starDifficulty : 0; MaxCombo = maxCombo; - DifficultyAttributes = null; } public DifficultyRating DifficultyRating => GetDifficultyRating(Stars); From 695f156f5f405c6c729c26e73572a1523f922944 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 14 Nov 2024 14:12:13 +0900 Subject: [PATCH 101/120] Change collection management dialog to handle changes more correctly --- .../Collections/DrawableCollectionList.cs | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/osu.Game/Collections/DrawableCollectionList.cs b/osu.Game/Collections/DrawableCollectionList.cs index 6fe38a322927..96013314c786 100644 --- a/osu.Game/Collections/DrawableCollectionList.cs +++ b/osu.Game/Collections/DrawableCollectionList.cs @@ -43,8 +43,25 @@ protected override void LoadComplete() private void collectionsChanged(IRealmCollection collections, ChangeSet? changes) { - Items.Clear(); - Items.AddRange(collections.AsEnumerable().Select(c => c.ToLive(realm))); + if (changes == null) + { + Items.AddRange(collections.AsEnumerable().Select(c => c.ToLive(realm))); + return; + } + + foreach (int i in changes.DeletedIndices.OrderDescending()) + Items.RemoveAt(i); + + foreach (int i in changes.InsertedIndices) + Items.Insert(i, collections[i].ToLive(realm)); + + foreach (int i in changes.NewModifiedIndices) + { + var updatedItem = collections[i]; + + Items.RemoveAt(i); + Items.Insert(i, updatedItem.ToLive(realm)); + } } protected override OsuRearrangeableListItem> CreateOsuDrawable(Live item) From ebe9a9f0837cd4e039069b46371b7ff3e6339182 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 14 Nov 2024 14:12:31 +0900 Subject: [PATCH 102/120] Avoid writing changes to realm when collection name wasn't actually changed --- .../Collections/DrawableCollectionList.cs | 25 ++++++++++++++++- .../Collections/DrawableCollectionListItem.cs | 27 +++++++++---------- 2 files changed, 36 insertions(+), 16 deletions(-) diff --git a/osu.Game/Collections/DrawableCollectionList.cs b/osu.Game/Collections/DrawableCollectionList.cs index 96013314c786..e87a79f2e8f8 100644 --- a/osu.Game/Collections/DrawableCollectionList.cs +++ b/osu.Game/Collections/DrawableCollectionList.cs @@ -140,12 +140,35 @@ public DrawableCollectionListItem ReplacePlaceholder() var previous = PlaceholderItem; placeholderContainer.Clear(false); - placeholderContainer.Add(PlaceholderItem = new DrawableCollectionListItem(new BeatmapCollection().ToLiveUnmanaged(), false)); + placeholderContainer.Add(PlaceholderItem = new NewCollectionEntryItem()); return previous; } } + private class NewCollectionEntryItem : DrawableCollectionListItem + { + [Resolved] + private RealmAccess realm { get; set; } = null!; + + public NewCollectionEntryItem() + : base(new BeatmapCollection().ToLiveUnmanaged(), false) + { + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + TextBox.OnCommit += (sender, newText) => + { + if (newText && !string.IsNullOrEmpty(TextBox.Current.Value)) + realm.Write(r => r.Add(new BeatmapCollection(TextBox.Current.Value))); + TextBox.Text = string.Empty; + }; + } + } + /// /// The flow of . Disables layout easing unless a drag is in progress. /// diff --git a/osu.Game/Collections/DrawableCollectionListItem.cs b/osu.Game/Collections/DrawableCollectionListItem.cs index e71368c079b4..a17a50e232c8 100644 --- a/osu.Game/Collections/DrawableCollectionListItem.cs +++ b/osu.Game/Collections/DrawableCollectionListItem.cs @@ -28,6 +28,10 @@ public partial class DrawableCollectionListItem : OsuRearrangeableListItem content.TextBox; + + private ItemContent content = null!; + /// /// Creates a new . /// @@ -48,7 +52,7 @@ public DrawableCollectionListItem(Live item, bool isCreated) CornerRadius = item_height / 2; } - protected override Drawable CreateContent() => new ItemContent(Model); + protected override Drawable CreateContent() => content = new ItemContent(Model); /// /// The main content of the . @@ -57,10 +61,7 @@ private partial class ItemContent : CompositeDrawable { private readonly Live collection; - private ItemTextBox textBox = null!; - - [Resolved] - private RealmAccess realm { get; set; } = null!; + public ItemTextBox TextBox { get; private set; } = null!; public ItemContent(Live collection) { @@ -80,7 +81,7 @@ private void load() { Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, - IsTextBoxHovered = v => textBox.ReceivePositionalInputAt(v) + IsTextBoxHovered = v => TextBox.ReceivePositionalInputAt(v) } : Empty(), new Container @@ -89,7 +90,7 @@ private void load() Padding = new MarginPadding { Right = collection.IsManaged ? button_width : 0 }, Children = new Drawable[] { - textBox = new ItemTextBox + TextBox = new ItemTextBox { RelativeSizeAxes = Axes.Both, Size = Vector2.One, @@ -107,18 +108,14 @@ protected override void LoadComplete() base.LoadComplete(); // Bind late, as the collection name may change externally while still loading. - textBox.Current.Value = collection.PerformRead(c => c.IsValid ? c.Name : string.Empty); - textBox.OnCommit += onCommit; + TextBox.Current.Value = collection.PerformRead(c => c.IsValid ? c.Name : string.Empty); + TextBox.OnCommit += onCommit; } private void onCommit(TextBox sender, bool newText) { - if (collection.IsManaged) - collection.PerformWrite(c => c.Name = textBox.Current.Value); - else if (!string.IsNullOrEmpty(textBox.Current.Value)) - realm.Write(r => r.Add(new BeatmapCollection(textBox.Current.Value))); - - textBox.Text = string.Empty; + if (collection.IsManaged && newText) + collection.PerformWrite(c => c.Name = TextBox.Current.Value); } } From 17b1888c597434326db9db5da21f022be5b26fe3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 14 Nov 2024 14:23:43 +0900 Subject: [PATCH 103/120] Avoid using `newTexst` as it doesn't work well with tests --- osu.Game/Collections/DrawableCollectionList.cs | 6 ++++-- osu.Game/Collections/DrawableCollectionListItem.cs | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game/Collections/DrawableCollectionList.cs b/osu.Game/Collections/DrawableCollectionList.cs index e87a79f2e8f8..aaef41eee137 100644 --- a/osu.Game/Collections/DrawableCollectionList.cs +++ b/osu.Game/Collections/DrawableCollectionList.cs @@ -162,8 +162,10 @@ protected override void LoadComplete() TextBox.OnCommit += (sender, newText) => { - if (newText && !string.IsNullOrEmpty(TextBox.Current.Value)) - realm.Write(r => r.Add(new BeatmapCollection(TextBox.Current.Value))); + if (string.IsNullOrEmpty(TextBox.Text)) + return; + + realm.Write(r => r.Add(new BeatmapCollection(TextBox.Text))); TextBox.Text = string.Empty; }; } diff --git a/osu.Game/Collections/DrawableCollectionListItem.cs b/osu.Game/Collections/DrawableCollectionListItem.cs index a17a50e232c8..f07ec87353fe 100644 --- a/osu.Game/Collections/DrawableCollectionListItem.cs +++ b/osu.Game/Collections/DrawableCollectionListItem.cs @@ -114,7 +114,7 @@ protected override void LoadComplete() private void onCommit(TextBox sender, bool newText) { - if (collection.IsManaged && newText) + if (collection.IsManaged && collection.Value.Name != TextBox.Current.Value) collection.PerformWrite(c => c.Name = TextBox.Current.Value); } } From d66daf15a56579b1bf6ae905555df944fd846fdf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 14 Nov 2024 14:36:58 +0900 Subject: [PATCH 104/120] Fix tests clicking wrong delete buttons due to internal drawable ordering --- .../Collections/TestSceneManageCollectionsDialog.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs b/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs index 747cf73bafe7..46bf6dfafd58 100644 --- a/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs +++ b/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs @@ -205,7 +205,9 @@ public void TestRemoveCollectionViaButton() AddStep("click first delete button", () => { - InputManager.MoveMouseTo(dialog.ChildrenOfType().First(), new Vector2(5, 0)); + InputManager.MoveMouseTo(dialog + .ChildrenOfType().Single(i => i.Model.Value.Name == "1") + .ChildrenOfType().Single(), new Vector2(5, 0)); InputManager.Click(MouseButton.Left); }); @@ -213,9 +215,11 @@ public void TestRemoveCollectionViaButton() assertCollectionCount(1); assertCollectionName(0, "2"); - AddStep("click first delete button", () => + AddStep("click second delete button", () => { - InputManager.MoveMouseTo(dialog.ChildrenOfType().First(), new Vector2(5, 0)); + InputManager.MoveMouseTo(dialog + .ChildrenOfType().Single(i => i.Model.Value.Name == "2") + .ChildrenOfType().Single(), new Vector2(5, 0)); InputManager.Click(MouseButton.Left); }); @@ -310,7 +314,7 @@ public void TestCollectionRenamedOnTextChange(bool commitWithEnter) AddStep("focus first collection", () => { - InputManager.MoveMouseTo(firstItem = dialog.ChildrenOfType().First()); + InputManager.MoveMouseTo(firstItem = dialog.ChildrenOfType().Single(i => i.Model.Value.Name == "1")); InputManager.Click(MouseButton.Left); }); From fea6a544325854877637d13e587864afeb4ca1ab Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 14 Nov 2024 14:46:58 +0900 Subject: [PATCH 105/120] Fix more tests reading in wrong order --- .../Visual/Collections/TestSceneManageCollectionsDialog.cs | 3 ++- osu.Game/Collections/DrawableCollectionList.cs | 7 ++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs b/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs index 46bf6dfafd58..56e7b4d39fa4 100644 --- a/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs +++ b/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs @@ -265,6 +265,7 @@ public void TestCollectionNotRemovedWhenDialogCancelled() } [Test] + [Solo] public void TestCollectionRenamedExternal() { BeatmapCollection first = null!; @@ -341,6 +342,6 @@ private void assertCollectionCount(int count) => AddUntilStep($"{count} collections shown", () => dialog.ChildrenOfType().Count() == count + 1); // +1 for placeholder private void assertCollectionName(int index, string name) - => AddUntilStep($"item {index + 1} has correct name", () => dialog.ChildrenOfType().ElementAt(index).ChildrenOfType().First().Text == name); + => AddUntilStep($"item {index + 1} has correct name", () => dialog.ChildrenOfType().Single().OrderedItems.ElementAt(index).ChildrenOfType().First().Text == name); } } diff --git a/osu.Game/Collections/DrawableCollectionList.cs b/osu.Game/Collections/DrawableCollectionList.cs index aaef41eee137..9f7494b5561d 100644 --- a/osu.Game/Collections/DrawableCollectionList.cs +++ b/osu.Game/Collections/DrawableCollectionList.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.Diagnostics; using System.Linq; using osu.Framework.Allocation; @@ -29,7 +30,11 @@ public partial class DrawableCollectionList : OsuRearrangeableListContainer>> CreateListFillFlowContainer() => new Flow + private Flow flow = null!; + + public IEnumerable OrderedItems => flow.FlowingChildren; + + protected override FillFlowContainer>> CreateListFillFlowContainer() => flow = new Flow { DragActive = { BindTarget = DragActive } }; From ce818f59e74b21e2ebffe8afd3690d7701f72f3f Mon Sep 17 00:00:00 2001 From: Nathen Date: Thu, 14 Nov 2024 01:42:07 -0500 Subject: [PATCH 106/120] Fix NaN PP values when SR is 0 --- osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs b/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs index 1cb6b69f91e8..1944b54e0c35 100644 --- a/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs +++ b/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs @@ -70,10 +70,11 @@ public sealed override void Process(DifficultyHitObject current) /// public virtual double CountTopWeightedStrains() { - if (ObjectStrains.Count == 0) + double consistentTopStrain = DifficultyValue() / 10; // What would the top strain be if all strain values were identical + + if (ObjectStrains.Count == 0 || consistentTopStrain == 0) return 0.0; - double consistentTopStrain = DifficultyValue() / 10; // What would the top strain be if all strain values were identical // Use a weighted sum of all strains. Constants are arbitrary and give nice values return ObjectStrains.Sum(s => 1.1 / (1 + Math.Exp(-10 * (s / consistentTopStrain - 0.88)))); } From 2ea2e5f1db40dacf02a297f571a481eec3351c6f Mon Sep 17 00:00:00 2001 From: Nathen Date: Thu, 14 Nov 2024 01:47:24 -0500 Subject: [PATCH 107/120] Be doubly careful --- osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs b/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs index 1944b54e0c35..56249f19a7f8 100644 --- a/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs +++ b/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs @@ -70,9 +70,12 @@ public sealed override void Process(DifficultyHitObject current) /// public virtual double CountTopWeightedStrains() { + if (ObjectStrains.Count == 0) + return 0.0; + double consistentTopStrain = DifficultyValue() / 10; // What would the top strain be if all strain values were identical - if (ObjectStrains.Count == 0 || consistentTopStrain == 0) + if (consistentTopStrain == 0) return 0.0; // Use a weighted sum of all strains. Constants are arbitrary and give nice values From d0e793a3b395026d2b26bc51538fd5f9a015ac7c Mon Sep 17 00:00:00 2001 From: Nathen Date: Thu, 14 Nov 2024 01:50:05 -0500 Subject: [PATCH 108/120] More correct but not too important --- osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs b/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs index 56249f19a7f8..3ba67793dc44 100644 --- a/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs +++ b/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs @@ -76,7 +76,7 @@ public virtual double CountTopWeightedStrains() double consistentTopStrain = DifficultyValue() / 10; // What would the top strain be if all strain values were identical if (consistentTopStrain == 0) - return 0.0; + return ObjectStrains.Count; // Use a weighted sum of all strains. Constants are arbitrary and give nice values return ObjectStrains.Sum(s => 1.1 / (1 + Math.Exp(-10 * (s / consistentTopStrain - 0.88)))); From 9fcf8342f0430c3c17a9fc9c5a2a00e3dc902cef Mon Sep 17 00:00:00 2001 From: Givikap120 Date: Thu, 14 Nov 2024 08:59:03 +0200 Subject: [PATCH 109/120] initial commit --- .../Difficulty/Skills/OsuStrainSkill.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs index 559a871df1f0..10b5ee01339b 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs @@ -23,8 +23,6 @@ public abstract class OsuStrainSkill : StrainSkill /// protected virtual double ReducedStrainBaseline => 0.75; - protected double Difficulty; - protected OsuStrainSkill(Mod[] mods) : base(mods) { @@ -32,7 +30,6 @@ protected OsuStrainSkill(Mod[] mods) public override double DifficultyValue() { - Difficulty = 0; double weight = 1; // Sections with 0 strain are excluded to avoid worst-case time complexity of the following sort (e.g. /b/2351871). @@ -48,15 +45,17 @@ public override double DifficultyValue() strains[i] *= Interpolation.Lerp(ReducedStrainBaseline, 1.0, scale); } + double difficulty = 0; + // Difficulty is the weighted sum of the highest strains from every section. // We're sorting from highest to lowest strain. foreach (double strain in strains.OrderDescending()) { - Difficulty += strain * weight; + difficulty += strain * weight; weight *= DecayWeight; } - return Difficulty; + return difficulty; } public static double DifficultyToPerformance(double difficulty) => Math.Pow(5.0 * Math.Max(1.0, difficulty / 0.0675) - 4.0, 3.0) / 100000.0; From 1b6952c42ab4977fa54026d8794af4a85210d677 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 14 Nov 2024 16:03:00 +0900 Subject: [PATCH 110/120] Add failing test showing crash when adjusting offset with no `HitEvent`s --- .../Ranking/TestSceneHitEventTimingDistributionGraph.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs b/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs index 3e38b660292d..760210c37006 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs @@ -40,6 +40,13 @@ public void TestManyDistributedEvents() AddSliderStep("height", 0.0f, 1000.0f, height.Value, height.Set); } + [Test] + public void TestZeroEvents() + { + createTest(new List()); + AddStep("update offset", () => graph.UpdateOffset(10)); + } + [Test] public void TestManyDistributedEventsOffset() { From b086c276ad82d4d983aa41837bdf6c2748be45cb Mon Sep 17 00:00:00 2001 From: Givikap120 Date: Thu, 14 Nov 2024 09:03:08 +0200 Subject: [PATCH 111/120] moved back to the top --- osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs index 10b5ee01339b..6823512cef12 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs @@ -30,6 +30,7 @@ protected OsuStrainSkill(Mod[] mods) public override double DifficultyValue() { + double difficulty = 0; double weight = 1; // Sections with 0 strain are excluded to avoid worst-case time complexity of the following sort (e.g. /b/2351871). @@ -45,8 +46,6 @@ public override double DifficultyValue() strains[i] *= Interpolation.Lerp(ReducedStrainBaseline, 1.0, scale); } - double difficulty = 0; - // Difficulty is the weighted sum of the highest strains from every section. // We're sorting from highest to lowest strain. foreach (double strain in strains.OrderDescending()) From 88d220f4c5227467227c1ab2df6ad545121fbe79 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 14 Nov 2024 16:03:12 +0900 Subject: [PATCH 112/120] Fix crash when resetting offset after a play with no hit events Closes https://github.com/ppy/osu/issues/30573. --- .../Ranking/Statistics/HitEventTimingDistributionGraph.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs index 207e19a71664..a9b93e0ffc3c 100644 --- a/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs +++ b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs @@ -176,7 +176,7 @@ private void createBarDrawables() for (int i = 1; i <= axis_points; i++) { double axisValue = i * axisValueStep; - float position = (float)(axisValue / maxValue); + float position = maxValue == 0 ? 0 : (float)(axisValue / maxValue); float alpha = 1f - position * 0.8f; axisFlow.Add(new OsuSpriteText @@ -348,7 +348,7 @@ private void drawAdjustmentBar() boxAdjustment.FadeTo(!hasAdjustment ? 0 : 1, duration, Easing.OutQuint); } - private float offsetForValue(float value) => (1 - minimum_height) * value / maxValue; + private float offsetForValue(float value) => maxValue == 0 ? 0 : (1 - minimum_height) * value / maxValue; private float heightForValue(float value) => minimum_height + offsetForValue(value); } From 64e7e44f28c961c9de6bf0e0987a0f06b6cd392a Mon Sep 17 00:00:00 2001 From: Shin Morisawa Date: Thu, 14 Nov 2024 16:29:03 +0900 Subject: [PATCH 113/120] fix issue --- osu.Game/Beatmaps/Drawables/DifficultyIconTooltip.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game/Beatmaps/Drawables/DifficultyIconTooltip.cs b/osu.Game/Beatmaps/Drawables/DifficultyIconTooltip.cs index 6c74ce573806..491882b53a52 100644 --- a/osu.Game/Beatmaps/Drawables/DifficultyIconTooltip.cs +++ b/osu.Game/Beatmaps/Drawables/DifficultyIconTooltip.cs @@ -147,9 +147,7 @@ public void SetContent(DifficultyIconTooltipContent content) overallDifficulty.Text = @" OD: " + adjustedDifficulty.OverallDifficulty.ToString(@"0.##"); TimeSpan lengthTimeSpan = TimeSpan.FromMilliseconds(displayedContent.BeatmapInfo.Length / rate); - length.Text = "Length: " + (lengthTimeSpan.Hours > 0 - ? lengthTimeSpan.ToString(@"hh\:mm\:ss") - : lengthTimeSpan.ToString(@"mm\:ss")); + length.Text = "Length: " + (lengthTimeSpan.TotalHours >= 1 ? lengthTimeSpan.ToString(@"hh\:mm\:ss") : lengthTimeSpan.ToString(@"mm\:ss")); bpm.Text = " BPM: " + Math.Round(bpmAdjusted, 0); } From 4957a517aacca9975cb168155e3aa3ca04bab396 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 14 Nov 2024 16:38:26 +0900 Subject: [PATCH 114/120] 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 f271bdfaaf07..757e5659d0f5 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 ecb9ae2c8dae..25639f84f429 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -17,6 +17,6 @@ -all - + From 7dff243e6a48a7b6c429042075dce13cea9f793f Mon Sep 17 00:00:00 2001 From: Shin Morisawa Date: Thu, 14 Nov 2024 16:59:03 +0900 Subject: [PATCH 115/120] use .ToFormattedDuration --- osu.Game/Beatmaps/Drawables/DifficultyIconTooltip.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/Drawables/DifficultyIconTooltip.cs b/osu.Game/Beatmaps/Drawables/DifficultyIconTooltip.cs index 491882b53a52..8182fe24b231 100644 --- a/osu.Game/Beatmaps/Drawables/DifficultyIconTooltip.cs +++ b/osu.Game/Beatmaps/Drawables/DifficultyIconTooltip.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; +using osu.Game.Extensions; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Rulesets; @@ -147,7 +148,7 @@ public void SetContent(DifficultyIconTooltipContent content) overallDifficulty.Text = @" OD: " + adjustedDifficulty.OverallDifficulty.ToString(@"0.##"); TimeSpan lengthTimeSpan = TimeSpan.FromMilliseconds(displayedContent.BeatmapInfo.Length / rate); - length.Text = "Length: " + (lengthTimeSpan.TotalHours >= 1 ? lengthTimeSpan.ToString(@"hh\:mm\:ss") : lengthTimeSpan.ToString(@"mm\:ss")); + length.Text = "Length: " + lengthTimeSpan.ToFormattedDuration(); bpm.Text = " BPM: " + Math.Round(bpmAdjusted, 0); } From 88aea70429513151076b97dd8a2790aaedc9d27c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 14 Nov 2024 10:07:09 +0100 Subject: [PATCH 116/120] Do not permit new combo toggle to remain in indeterminate state on deselect --- .../Screens/Edit/Compose/Components/EditorSelectionHandler.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs index a252649bb98d..a9dbfc29a963 100644 --- a/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs @@ -258,6 +258,8 @@ private void createStateBindables() private void resetTernaryStates() { + if (SelectionNewComboState.Value == TernaryState.Indeterminate) + SelectionNewComboState.Value = TernaryState.False; AutoSelectionBankEnabled.Value = true; SelectionAdditionBanksEnabled.Value = true; SelectionBankStates[HIT_BANK_AUTO].Value = TernaryState.True; From 9849a88eefc7d9e8534eba47ac230b947b1cb0c1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 14 Nov 2024 18:27:26 +0900 Subject: [PATCH 117/120] Adjust transition further to avoid brief "jumpscare" display of metadata --- osu.Game/Screens/Play/PlayerLoader.cs | 86 +++++++++++++++++++-------- 1 file changed, 60 insertions(+), 26 deletions(-) diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 51268e5dcedb..5843c28f3a80 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -10,6 +10,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Transforms; using osu.Framework.Input; @@ -88,9 +89,13 @@ public partial class PlayerLoader : ScreenWithBeatmapBackground private SkinnableSound sampleRestart = null!; + private Box? quickRestartBlackLayer; + [Cached] private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); + private const double quick_restart_initial_delay = 500; + protected bool BackgroundBrightnessReduction { set @@ -307,6 +312,9 @@ public override void OnSuspending(ScreenTransitionEvent e) { base.OnSuspending(e); + quickRestartBlackLayer?.FadeOut(500, Easing.OutQuint); + quickRestartBlackLayer = null; + BackgroundBrightnessReduction = false; // we're moving to player, so a period of silence is upcoming. @@ -350,7 +358,14 @@ protected override void LogoArriving(OsuLogo logo, bool resuming) if (!resuming) logo.MoveTo(new Vector2(0.5f), duration, Easing.OutQuint); logo.ScaleTo(new Vector2(0.15f), duration, Easing.OutQuint); - logo.FadeIn(350); + + if (quickRestart) + { + logo.Delay(quick_restart_initial_delay) + .FadeIn(350); + } + else + logo.FadeIn(350); Scheduler.AddDelayed(() => { @@ -456,17 +471,33 @@ private void contentIn(double delayBeforeSideDisplays = 0) { MetadataInfo.Loading = true; - content.FadeInFromZero(500, Easing.OutQuint); - if (quickRestart) { - // A slight delay is added here to avoid an awkward stutter during the initial animation. - Scheduler.AddDelayed(prepareNewPlayer, 100); - content.ScaleTo(1); + // A quick restart starts by triggering a fade to black + AddInternal(quickRestartBlackLayer = new Box + { + Colour = Color4.Black, + RelativeSizeAxes = Axes.Both, + Depth = float.MaxValue + }); + + quickRestartBlackLayer + .Delay(50) + .FadeOut(5000, Easing.OutQuint); + + prepareNewPlayer(); + + content + .Delay(quick_restart_initial_delay) + .ScaleTo(1) + .FadeInFromZero(500, Easing.OutQuint); } else { + content.FadeInFromZero(500, Easing.OutQuint); + content + .ScaleTo(0.7f) .ScaleTo(1, 650, Easing.OutQuint) .Then() .Schedule(prepareNewPlayer); @@ -542,33 +573,36 @@ private void pushWhenLoaded() highPerformanceSession ??= highPerformanceSessionManager?.BeginSession(); scheduledPushPlayer = Scheduler.AddDelayed(() => - { - // ensure that once we have reached this "point of no return", readyForPush will be false for all future checks (until a new player instance is prepared). - var consumedPlayer = consumePlayer(); + { + // ensure that once we have reached this "point of no return", readyForPush will be false for all future checks (until a new player instance is prepared). + var consumedPlayer = consumePlayer(); - ContentOut(); + ContentOut(); - TransformSequence pushSequence = this.Delay(0); + TransformSequence pushSequence = this.Delay(0); - // This goes hand-in-hand with the restoration of low pass filter in contentOut(). - this.TransformBindableTo(volumeAdjustment, 0, CONTENT_OUT_DURATION, Easing.OutCubic); + // This goes hand-in-hand with the restoration of low pass filter in contentOut(). + this.TransformBindableTo(volumeAdjustment, 0, CONTENT_OUT_DURATION, Easing.OutCubic); - pushSequence.Schedule(() => - { - if (!this.IsCurrentScreen()) return; + pushSequence.Schedule(() => + { + if (!this.IsCurrentScreen()) return; - LoadTask = null; + LoadTask = null; - // By default, we want to load the player and never be returned to. - // Note that this may change if the player we load requested a re-run. - ValidForResume = false; + // By default, we want to load the player and never be returned to. + // Note that this may change if the player we load requested a re-run. + ValidForResume = false; - if (consumedPlayer.LoadedBeatmapSuccessfully) - this.Push(consumedPlayer); - else - this.Exit(); - }); - }, quickRestart ? 0 : 500); + if (consumedPlayer.LoadedBeatmapSuccessfully) + this.Push(consumedPlayer); + else + this.Exit(); + }); + }, + // When a quick restart is activated, the metadata content will display some time later if it's taking too long. + // To avoid it appearing too briefly, if it begins to fade in let's induce a standard delay. + quickRestart && content.Alpha == 0 ? 0 : 500); } private void cancelLoad() From 36d48a01c841e9997fbc8a46adaa1962ed7f9c06 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 14 Nov 2024 19:33:34 +0900 Subject: [PATCH 118/120] Add missing expire call MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- osu.Game/Screens/Play/PlayerLoader.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 5843c28f3a80..cf867e31f8d5 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -312,7 +312,7 @@ public override void OnSuspending(ScreenTransitionEvent e) { base.OnSuspending(e); - quickRestartBlackLayer?.FadeOut(500, Easing.OutQuint); + quickRestartBlackLayer?.FadeOut(500, Easing.OutQuint).Expire(); quickRestartBlackLayer = null; BackgroundBrightnessReduction = false; From f9489690cb64bfc30ed1b2c96c8cb1b011364efa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 14 Nov 2024 11:58:57 +0100 Subject: [PATCH 119/120] Fix code inspection --- osu.Game/Collections/DrawableCollectionList.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Collections/DrawableCollectionList.cs b/osu.Game/Collections/DrawableCollectionList.cs index 9f7494b5561d..164ec558a480 100644 --- a/osu.Game/Collections/DrawableCollectionList.cs +++ b/osu.Game/Collections/DrawableCollectionList.cs @@ -151,7 +151,7 @@ public DrawableCollectionListItem ReplacePlaceholder() } } - private class NewCollectionEntryItem : DrawableCollectionListItem + private partial class NewCollectionEntryItem : DrawableCollectionListItem { [Resolved] private RealmAccess realm { get; set; } = null!; From 8df7a6f2f31226dab8e1a1d300968b527e24c00b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 14 Nov 2024 14:24:11 +0100 Subject: [PATCH 120/120] Adjust test to have better assertions --- .../Beatmaps/IO/LegacyBeatmapExporterTest.cs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/IO/LegacyBeatmapExporterTest.cs b/osu.Game.Tests/Beatmaps/IO/LegacyBeatmapExporterTest.cs index 7973867114f9..9947def06d5f 100644 --- a/osu.Game.Tests/Beatmaps/IO/LegacyBeatmapExporterTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/LegacyBeatmapExporterTest.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; using System.IO; using NUnit.Framework; using osu.Framework.Allocation; @@ -30,9 +29,9 @@ public void TestObjectsSnappedAfterTruncatingExport() // Ensure importer encoding is correct AddStep("import beatmap", () => beatmap = importBeatmapFromArchives(@"decimal-timing-beatmap.olz")); - AddAssert("timing point has decimal offset", () => Math.Abs(beatmap.Beatmap.ControlPointInfo.TimingPoints[0].Time - 284.725) < 0.001); - AddAssert("kiai has decimal offset", () => Math.Abs(beatmap.Beatmap.ControlPointInfo.EffectPoints[0].Time - 28520.019) < 0.001); - AddAssert("hit object has decimal offset", () => Math.Abs(beatmap.Beatmap.HitObjects[0].StartTime - 28520.019) < 0.001); + AddAssert("timing point has decimal offset", () => beatmap.Beatmap.ControlPointInfo.TimingPoints[0].Time, () => Is.EqualTo(284.725).Within(0.001)); + AddAssert("kiai has decimal offset", () => beatmap.Beatmap.ControlPointInfo.EffectPoints[0].Time, () => Is.EqualTo(28520.019).Within(0.001)); + AddAssert("hit object has decimal offset", () => beatmap.Beatmap.HitObjects[0].StartTime, () => Is.EqualTo(28520.019).Within(0.001)); // Ensure exporter legacy conversion is correct AddStep("export", () => @@ -44,9 +43,9 @@ public void TestObjectsSnappedAfterTruncatingExport() }); AddStep("import beatmap again", () => beatmap = importBeatmapFromStream(outStream)); - AddAssert("timing point has truncated offset", () => Math.Abs(beatmap.Beatmap.ControlPointInfo.TimingPoints[0].Time - 284) < 0.001); - AddAssert("kiai is snapped", () => Math.Abs(beatmap.Beatmap.ControlPointInfo.EffectPoints[0].Time - 28519) < 0.001); - AddAssert("hit object is snapped", () => Math.Abs(beatmap.Beatmap.HitObjects[0].StartTime - 28519) < 0.001); + AddAssert("timing point has truncated offset", () => beatmap.Beatmap.ControlPointInfo.TimingPoints[0].Time, () => Is.EqualTo(284).Within(0.001)); + AddAssert("kiai is snapped", () => beatmap.Beatmap.ControlPointInfo.EffectPoints[0].Time, () => Is.EqualTo(28519).Within(0.001)); + AddAssert("hit object is snapped", () => beatmap.Beatmap.HitObjects[0].StartTime, () => Is.EqualTo(28519).Within(0.001)); } private IWorkingBeatmap importBeatmapFromStream(Stream stream)