From e85869e0b48e7d683933ed821f6ec4baa5ea57a3 Mon Sep 17 00:00:00 2001 From: Aurelian Date: Sun, 2 Jun 2024 16:38:58 +0200 Subject: [PATCH 01/57] 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 8cdbd97ecb..cc0d195626 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.Containers; 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 @@ namespace osu.Game.Screens.Edit.Timing private MetronomeDisplay metronome = null!; + private LabelledSwitchButton adjustPlacedNotes = null!; + [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider, OsuColour colours) { @@ -59,6 +65,7 @@ namespace osu.Game.Screens.Edit.Timing { 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 @@ namespace osu.Game.Screens.Edit.Timing }, }, 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 @@ namespace osu.Game.Screens.Edit.Timing 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 @@ namespace osu.Game.Screens.Edit.Timing 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 @@ namespace osu.Game.Screens.Edit.Timing // 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 @@ namespace osu.Game.Screens.Edit.Timing 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 02/57] 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 cc0d195626..c4d67eaf1a 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 @@ namespace osu.Game.Screens.Edit.Timing 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 03/57] 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 c4d67eaf1a..952bdf0ccc 100644 --- a/osu.Game/Screens/Edit/Timing/TapTimingControl.cs +++ b/osu.Game/Screens/Edit/Timing/TapTimingControl.cs @@ -270,7 +270,7 @@ namespace osu.Game.Screens.Edit.Timing { 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 04/57] 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 952bdf0ccc..6ef7bd1a40 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 @@ namespace osu.Game.Screens.Edit.Timing 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 05/57] 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 6ef7bd1a40..cffa31d117 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.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 @@ -272,6 +272,9 @@ namespace osu.Game.Screens.Edit.Timing 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 06/57] 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 cffa31d117..739b9894ce 100644 --- a/osu.Game/Screens/Edit/Timing/TapTimingControl.cs +++ b/osu.Game/Screens/Edit/Timing/TapTimingControl.cs @@ -252,6 +252,9 @@ namespace osu.Game.Screens.Edit.Timing 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 @@ namespace osu.Game.Screens.Edit.Timing } 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 07/57] 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 739b9894ce..1c7f89c80d 100644 --- a/osu.Game/Screens/Edit/Timing/TapTimingControl.cs +++ b/osu.Game/Screens/Edit/Timing/TapTimingControl.cs @@ -254,7 +254,7 @@ namespace osu.Game.Screens.Edit.Timing 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 @@ namespace osu.Game.Screens.Edit.Timing 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 08/57] 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 4e69486e2d..a3224a6ab9 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 @@ namespace osu.Game.Beatmaps.ControlPoints && 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 1c7f89c80d..49d3df4aef 100644 --- a/osu.Game/Screens/Edit/Timing/TapTimingControl.cs +++ b/osu.Game/Screens/Edit/Timing/TapTimingControl.cs @@ -211,15 +211,6 @@ namespace osu.Game.Screens.Edit.Timing 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 @@ namespace osu.Game.Screens.Edit.Timing 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 @@ namespace osu.Game.Screens.Edit.Timing 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 @@ namespace osu.Game.Screens.Edit.Timing 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 09/57] 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 a3224a6ab9..234a34dc87 100644 --- a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs @@ -128,16 +128,16 @@ namespace osu.Game.Beatmaps.ControlPoints } } - 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 6ad12f54df..b604fc3889 100644 --- a/osu.Game/Localisation/EditorStrings.cs +++ b/osu.Game/Localisation/EditorStrings.cs @@ -39,6 +39,11 @@ namespace osu.Game.Localisation /// 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 07c32983f5..af099413fd 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -367,7 +367,11 @@ namespace osu.Game.Screens.Edit { 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 7a3ea474fb..42a3a6d3b2 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -88,6 +88,8 @@ namespace osu.Game.Screens.Edit 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 49d3df4aef..30e5919411 100644 --- a/osu.Game/Screens/Edit/Timing/TapTimingControl.cs +++ b/osu.Game/Screens/Edit/Timing/TapTimingControl.cs @@ -37,8 +37,6 @@ namespace osu.Game.Screens.Edit.Timing private MetronomeDisplay metronome = null!; - private LabelledSwitchButton adjustPlacedNotes = null!; - [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider, OsuColour colours) { @@ -65,7 +63,6 @@ namespace osu.Game.Screens.Edit.Timing { 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 @@ namespace osu.Game.Screens.Edit.Timing }, }, 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 @@ namespace osu.Game.Screens.Edit.Timing 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 @@ namespace osu.Game.Screens.Edit.Timing if (timing == null) return; - double newBeatLength = 60000 / (timing.BPM + adjust); + timing.BeatLength = 60000 / (timing.BPM + adjust); - if (adjustPlacedNotes.Current.Value) - timing.SetHitObjectBPM(beatmap, newBeatLength); - - 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 10/57] 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 487a871881..7d0eab1f7f 100644 --- a/osu.Game/Screens/Edit/Timing/GroupSection.cs +++ b/osu.Game/Screens/Edit/Timing/GroupSection.cs @@ -109,11 +109,18 @@ namespace osu.Game.Screens.Edit.Timing 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 30e5919411..937235c7bc 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.Containers; 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 @@ namespace osu.Game.Screens.Edit.Timing 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 2757753b07..24cf865ce3 100644 --- a/osu.Game/Screens/Edit/Timing/TimingSection.cs +++ b/osu.Game/Screens/Edit/Timing/TimingSection.cs @@ -42,6 +42,17 @@ namespace osu.Game.Screens.Edit.Timing { 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 11/57] 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 24cf865ce3..1ae85e6c9e 100644 --- a/osu.Game/Screens/Edit/Timing/TimingSection.cs +++ b/osu.Game/Screens/Edit/Timing/TimingSection.cs @@ -45,7 +45,7 @@ namespace osu.Game.Screens.Edit.Timing 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 12/57] 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 937235c7bc..55404702ac 100644 --- a/osu.Game/Screens/Edit/Timing/TapTimingControl.cs +++ b/osu.Game/Screens/Edit/Timing/TapTimingControl.cs @@ -231,9 +231,6 @@ namespace osu.Game.Screens.Edit.Timing 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 1ae85e6c9e..5be1de467d 100644 --- a/osu.Game/Screens/Edit/Timing/TimingSection.cs +++ b/osu.Game/Screens/Edit/Timing/TimingSection.cs @@ -48,10 +48,8 @@ namespace osu.Game.Screens.Edit.Timing 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 13/57] 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 234a34dc87..4e69486e2d 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 @@ namespace osu.Game.Beatmaps.ControlPoints && 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 7d0eab1f7f..c5a4422192 100644 --- a/osu.Game/Screens/Edit/Timing/GroupSection.cs +++ b/osu.Game/Screens/Edit/Timing/GroupSection.cs @@ -112,7 +112,7 @@ namespace osu.Game.Screens.Edit.Timing { // 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 55404702ac..f2e37369d1 100644 --- a/osu.Game/Screens/Edit/Timing/TapTimingControl.cs +++ b/osu.Game/Screens/Edit/Timing/TapTimingControl.cs @@ -209,7 +209,7 @@ namespace osu.Game.Screens.Edit.Timing 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 5be1de467d..ba335d55eb 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 @@ namespace osu.Game.Screens.Edit.Timing 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 @@ namespace osu.Game.Screens.Edit.Timing 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 14/57] 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 4b47ecaab6..139f53a961 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 @@ namespace osu.Game.Screens.Edit.Timing 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 0000000000..11c4be4790 --- /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 15/57] 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 0000000000..2cb9e72c1f --- /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 11c4be4790..65edc47ff5 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 16/57] 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 2cb9e72c1f..5f5a1760ea 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 17/57] 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 18a222f414..abcdf7e4ff 100644 --- a/osu.Game/Screens/Edit/Timing/GroupSection.cs +++ b/osu.Game/Screens/Edit/Timing/GroupSection.cs @@ -113,15 +113,17 @@ namespace osu.Game.Screens.Edit.Timing { // 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 f2e37369d1..91a0a43d62 100644 --- a/osu.Game/Screens/Edit/Timing/TapTimingControl.cs +++ b/osu.Game/Screens/Edit/Timing/TapTimingControl.cs @@ -202,6 +202,7 @@ namespace osu.Game.Screens.Edit.Timing // 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 @@ namespace osu.Game.Screens.Edit.Timing 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 @@ namespace osu.Game.Screens.Edit.Timing 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 139f53a961..e1567d65aa 100644 --- a/osu.Game/Screens/Edit/Timing/TimingSection.cs +++ b/osu.Game/Screens/Edit/Timing/TimingSection.cs @@ -48,8 +48,10 @@ namespace osu.Game.Screens.Edit.Timing 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 18/57] 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 e1567d65aa..e668120d0d 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 @@ namespace osu.Game.Screens.Edit.Timing 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 @@ namespace osu.Game.Screens.Edit.Timing private partial class BPMTextBox : LabelledTextBox { + public new Action? OnCommit { get; set; } + private readonly BindableNumber beatLengthBindable = new TimingControlPoint().BeatLengthBindable; public BPMTextBox() @@ -92,10 +95,12 @@ namespace osu.Game.Screens.Edit.Timing 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 @@ namespace osu.Game.Screens.Edit.Timing // 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 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 19/57] 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 88cc7eb9b3..bd635b1669 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.Platform; 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 @@ namespace osu.Game.Tests.Visual.Multiplayer private Live importedBeatmapSet; + [Resolved] + private OsuConfigManager configManager { get; set; } + [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { @@ -57,10 +62,8 @@ namespace osu.Game.Tests.Visual.Multiplayer Add(detachedBeatmapStore); } - public override void SetUpSteps() + private void setUp() { - base.SetUpSteps(); - AddStep("reset", () => { Ruleset.Value = new OsuRuleset().RulesetInfo; @@ -75,6 +78,8 @@ namespace osu.Game.Tests.Visual.Multiplayer [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 @@ namespace osu.Game.Tests.Visual.Multiplayer { 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 @@ namespace osu.Game.Tests.Visual.Multiplayer [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 @@ namespace osu.Game.Tests.Visual.Multiplayer 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 @@ namespace osu.Game.Tests.Visual.Multiplayer 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 20/57] 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 ea5048ca49..9f7a2c02ff 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -711,12 +711,6 @@ namespace osu.Game.Screens.Select Carousel.AllowSelection = true; - if (pendingFilterApplication) - { - Carousel.Filter(FilterControl.CreateCriteria()); - pendingFilterApplication = false; - } - BeatmapDetails.Refresh(); beginLooping(); @@ -749,6 +743,17 @@ namespace osu.Game.Screens.Select 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 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 21/57] 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 b706e96bdb..be2a5ac144 100644 --- a/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs +++ b/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs @@ -54,17 +54,17 @@ namespace osu.Game.Rulesets.Taiko.Edit 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 22/57] 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 1af4719b02..35c8a02af5 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs @@ -78,6 +78,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { 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 0333fd71a9..64d2020edc 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; 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 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables 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 a5e63c373f..28831a6d2c 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.Objects.Drawables; 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 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { 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 f2fcd185dd..28617b35f6 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.Objects; 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 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables /// 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 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables 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 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { 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 3f4694d71d..0cf9651965 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs @@ -130,7 +130,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables public new TObject HitObject => (TObject)base.HitObject; - protected Vector2 BaseSize; protected SkinnableDrawable MainPiece; protected DrawableTaikoHitObject([CanBeNull] TObject hitObject) @@ -152,8 +151,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables 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 4d7cdf3243..7c3ff4f27e 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.Bindables; 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 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables 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 788ecc1e7b8c2cd0c967e92c6db3078a46109875 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 5 Nov 2024 19:44:29 +0900 Subject: [PATCH 23/57] Replace MultiplayerRoomComposite with local bindings --- .../Online/Multiplayer/MultiplayerClient.cs | 18 +-- .../Multiplayer/MultiplayerRoomExtensions.cs | 39 ++++++ .../Lounge/Components/RankRangePill.cs | 38 ++++-- .../Multiplayer/Match/MatchStartControl.cs | 109 ++++++++------- .../Match/MultiplayerSpectateButton.cs | 45 ++++--- .../Match/Playlist/MultiplayerPlaylist.cs | 67 +++++----- .../Multiplayer/MultiplayerRoomComposite.cs | 125 ------------------ .../Multiplayer/MultiplayerRoomSounds.cs | 80 ++++++----- .../Participants/ParticipantPanel.cs | 48 +++++-- .../Participants/ParticipantsList.cs | 46 ++++--- .../Multiplayer/Participants/TeamDisplay.cs | 48 ++++--- 11 files changed, 336 insertions(+), 327 deletions(-) create mode 100644 osu.Game/Online/Multiplayer/MultiplayerRoomExtensions.cs delete mode 100644 osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomComposite.cs diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 5fd8b8b337..ff147aba10 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -202,7 +202,7 @@ namespace osu.Game.Online.Multiplayer Debug.Assert(joinedRoom.Playlist.Count > 0); APIRoom.Playlist.Clear(); - APIRoom.Playlist.AddRange(joinedRoom.Playlist.Select(createPlaylistItem)); + APIRoom.Playlist.AddRange(joinedRoom.Playlist.Select(item => new PlaylistItem(item))); APIRoom.CurrentPlaylistItem.Value = APIRoom.Playlist.Single(item => item.ID == joinedRoom.Settings.PlaylistItemId); // The server will null out the end date upon the host joining the room, but the null value is never communicated to the client. @@ -734,7 +734,7 @@ namespace osu.Game.Online.Multiplayer Debug.Assert(APIRoom != null); Room.Playlist.Add(item); - APIRoom.Playlist.Add(createPlaylistItem(item)); + APIRoom.Playlist.Add(new PlaylistItem(item)); ItemAdded?.Invoke(item); RoomUpdated?.Invoke(); @@ -780,7 +780,7 @@ namespace osu.Game.Online.Multiplayer int existingIndex = APIRoom.Playlist.IndexOf(APIRoom.Playlist.Single(existing => existing.ID == item.ID)); APIRoom.Playlist.RemoveAt(existingIndex); - APIRoom.Playlist.Insert(existingIndex, createPlaylistItem(item)); + APIRoom.Playlist.Insert(existingIndex, new PlaylistItem(item)); } catch (Exception ex) { @@ -853,18 +853,6 @@ namespace osu.Game.Online.Multiplayer RoomUpdated?.Invoke(); } - private PlaylistItem createPlaylistItem(MultiplayerPlaylistItem item) => new PlaylistItem(new APIBeatmap { OnlineID = item.BeatmapID, StarRating = item.StarRating }) - { - ID = item.ID, - OwnerID = item.OwnerID, - RulesetID = item.RulesetID, - Expired = item.Expired, - PlaylistOrder = item.PlaylistOrder, - PlayedAt = item.PlayedAt, - RequiredMods = item.RequiredMods.ToArray(), - AllowedMods = item.AllowedMods.ToArray() - }; - /// /// For the provided user ID, update whether the user is included in . /// diff --git a/osu.Game/Online/Multiplayer/MultiplayerRoomExtensions.cs b/osu.Game/Online/Multiplayer/MultiplayerRoomExtensions.cs new file mode 100644 index 0000000000..0aeb85d2d8 --- /dev/null +++ b/osu.Game/Online/Multiplayer/MultiplayerRoomExtensions.cs @@ -0,0 +1,39 @@ +// 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.Game.Online.Rooms; + +namespace osu.Game.Online.Multiplayer +{ + public static class MultiplayerRoomExtensions + { + /// + /// Returns all historical/expired items from the , in the order in which they were played. + /// + public static IEnumerable GetHistoricalItems(this MultiplayerRoom room) + => room.Playlist.Where(item => item.Expired).OrderBy(item => item.PlayedAt); + + /// + /// Returns all non-expired items from the , in the order in which they are to be played. + /// + public static IEnumerable GetUpcomingItems(this MultiplayerRoom room) + => room.Playlist.Where(item => !item.Expired).OrderBy(item => item.PlaylistOrder); + + /// + /// Returns the first non-expired in playlist order from the supplied , + /// or the last-played if all items are expired, + /// or if was empty. + /// + public static MultiplayerPlaylistItem? GetCurrentItem(this MultiplayerRoom room) + { + if (room.Playlist.Count == 0) + return null; + + return room.Playlist.All(item => item.Expired) + ? GetHistoricalItems(room).Last() + : GetUpcomingItems(room).First(); + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RankRangePill.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RankRangePill.cs index adfc44fbd4..09aafa415a 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RankRangePill.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RankRangePill.cs @@ -1,23 +1,25 @@ // 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.Linq; using osu.Framework.Allocation; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; using osu.Game.Graphics.Containers; -using osu.Game.Screens.OnlinePlay.Multiplayer; +using osu.Game.Online.Multiplayer; using osuTK; namespace osu.Game.Screens.OnlinePlay.Lounge.Components { - public partial class RankRangePill : MultiplayerRoomComposite + public partial class RankRangePill : CompositeDrawable { - private OsuTextFlowContainer rankFlow; + private OsuTextFlowContainer rankFlow = null!; + + [Resolved] + private MultiplayerClient client { get; set; } = null!; public RankRangePill() { @@ -55,20 +57,28 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components }; } - protected override void OnRoomUpdated() + protected override void LoadComplete() { - base.OnRoomUpdated(); + base.LoadComplete(); + client.RoomUpdated += onRoomUpdated; + updateState(); + } + + private void onRoomUpdated() => Scheduler.AddOnce(updateState); + + private void updateState() + { rankFlow.Clear(); - if (Room == null || Room.Users.All(u => u.User == null)) + if (client.Room == null || client.Room.Users.All(u => u.User == null)) { rankFlow.AddText("-"); return; } - int minRank = Room.Users.Select(u => u.User?.Statistics.GlobalRank ?? 0).DefaultIfEmpty(0).Min(); - int maxRank = Room.Users.Select(u => u.User?.Statistics.GlobalRank ?? 0).DefaultIfEmpty(0).Max(); + int minRank = client.Room.Users.Select(u => u.User?.Statistics.GlobalRank ?? 0).DefaultIfEmpty(0).Min(); + int maxRank = client.Room.Users.Select(u => u.User?.Statistics.GlobalRank ?? 0).DefaultIfEmpty(0).Max(); rankFlow.AddText("#"); rankFlow.AddText(minRank.ToString("#,0"), s => s.Font = s.Font.With(weight: FontWeight.Bold)); @@ -78,5 +88,13 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components rankFlow.AddText("#"); rankFlow.AddText(maxRank.ToString("#,0"), s => s.Font = s.Font.With(weight: FontWeight.Bold)); } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (client.IsNotNull()) + client.RoomUpdated -= onRoomUpdated; + } } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs index ba3508b24f..a82fa6e4bc 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs @@ -1,47 +1,50 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Diagnostics; using System.Linq; -using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Threading; using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer.Countdown; +using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Overlays.Dialog; using osuTK; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { - public partial class MatchStartControl : MultiplayerRoomComposite + public partial class MatchStartControl : CompositeDrawable { [Resolved] - private OngoingOperationTracker ongoingOperationTracker { get; set; } - - [CanBeNull] - private IDisposable clickOperation; + private OngoingOperationTracker ongoingOperationTracker { get; set; } = null!; [Resolved(canBeNull: true)] - private IDialogOverlay dialogOverlay { get; set; } + private IDialogOverlay? dialogOverlay { get; set; } - private Sample sampleReady; - private Sample sampleReadyAll; - private Sample sampleUnready; + [Resolved] + private MultiplayerClient client { get; set; } = null!; + + [Resolved] + private IBindable currentItem { get; set; } = null!; private readonly MultiplayerReadyButton readyButton; private readonly MultiplayerCountdownButton countdownButton; + + private IBindable operationInProgress = null!; + private ScheduledDelegate? readySampleDelegate; + private IDisposable? clickOperation; + private Sample? sampleReady; + private Sample? sampleReadyAll; + private Sample? sampleUnready; private int countReady; - private ScheduledDelegate readySampleDelegate; - private IBindable operationInProgress; public MatchStartControl() { @@ -91,34 +94,29 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { base.LoadComplete(); - CurrentPlaylistItem.BindValueChanged(_ => updateState()); - } - - protected override void OnRoomUpdated() - { - base.OnRoomUpdated(); + currentItem.BindValueChanged(_ => updateState()); + client.RoomUpdated += onRoomUpdated; + client.LoadRequested += onLoadRequested; updateState(); } - protected override void OnRoomLoadRequested() - { - base.OnRoomLoadRequested(); - endOperation(); - } + private void onRoomUpdated() => Scheduler.AddOnce(updateState); + + private void onLoadRequested() => Scheduler.AddOnce(endOperation); private void onReadyButtonClick() { - if (Room == null) + if (client.Room == null) return; Debug.Assert(clickOperation == null); clickOperation = ongoingOperationTracker.BeginOperation(); - if (Client.IsHost) + if (client.IsHost) { - if (Room.State == MultiplayerRoomState.Open) + if (client.Room.State == MultiplayerRoomState.Open) { - if (isReady() && !Room.ActiveCountdowns.Any(c => c is MatchStartCountdown)) + if (isReady() && !client.Room.ActiveCountdowns.Any(c => c is MatchStartCountdown)) startMatch(); else toggleReady(); @@ -131,16 +129,16 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match dialogOverlay.Push(new ConfirmAbortDialog(abortMatch, endOperation)); } } - else if (Room.State != MultiplayerRoomState.Closed) + else if (client.Room.State != MultiplayerRoomState.Closed) toggleReady(); - bool isReady() => Client.LocalUser?.State == MultiplayerUserState.Ready || Client.LocalUser?.State == MultiplayerUserState.Spectating; + bool isReady() => client.LocalUser?.State == MultiplayerUserState.Ready || client.LocalUser?.State == MultiplayerUserState.Spectating; - void toggleReady() => Client.ToggleReady().FireAndForget( + void toggleReady() => client.ToggleReady().FireAndForget( onSuccess: endOperation, onError: _ => endOperation()); - void startMatch() => Client.StartMatch().FireAndForget(onSuccess: () => + void startMatch() => client.StartMatch().FireAndForget(onSuccess: () => { // gameplay is starting, the button will be unblocked on load requested. }, onError: _ => @@ -149,7 +147,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match endOperation(); }); - void abortMatch() => Client.AbortMatch().FireAndForget(endOperation, _ => endOperation()); + void abortMatch() => client.AbortMatch().FireAndForget(endOperation, _ => endOperation()); } private void startCountdown(TimeSpan duration) @@ -157,19 +155,19 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match Debug.Assert(clickOperation == null); clickOperation = ongoingOperationTracker.BeginOperation(); - Client.SendMatchRequest(new StartMatchCountdownRequest { Duration = duration }).ContinueWith(_ => endOperation()); + client.SendMatchRequest(new StartMatchCountdownRequest { Duration = duration }).ContinueWith(_ => endOperation()); } private void cancelCountdown() { - if (Client.Room == null) + if (client.Room == null) return; Debug.Assert(clickOperation == null); clickOperation = ongoingOperationTracker.BeginOperation(); - MultiplayerCountdown countdown = Client.Room.ActiveCountdowns.Single(c => c is MatchStartCountdown); - Client.SendMatchRequest(new StopCountdownRequest(countdown.ID)).ContinueWith(_ => endOperation()); + MultiplayerCountdown countdown = client.Room.ActiveCountdowns.Single(c => c is MatchStartCountdown); + client.SendMatchRequest(new StopCountdownRequest(countdown.ID)).ContinueWith(_ => endOperation()); } private void endOperation() @@ -180,19 +178,19 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match private void updateState() { - if (Room == null) + if (client.Room == null) { readyButton.Enabled.Value = false; countdownButton.Enabled.Value = false; return; } - var localUser = Client.LocalUser; + var localUser = client.LocalUser; - int newCountReady = Room.Users.Count(u => u.State == MultiplayerUserState.Ready); - int newCountTotal = Room.Users.Count(u => u.State != MultiplayerUserState.Spectating); + int newCountReady = client.Room.Users.Count(u => u.State == MultiplayerUserState.Ready); + int newCountTotal = client.Room.Users.Count(u => u.State != MultiplayerUserState.Spectating); - if (!Client.IsHost || Room.Settings.AutoStartEnabled) + if (!client.IsHost || client.Room.Settings.AutoStartEnabled) countdownButton.Hide(); else { @@ -211,21 +209,21 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match } readyButton.Enabled.Value = countdownButton.Enabled.Value = - Room.State != MultiplayerRoomState.Closed - && CurrentPlaylistItem.Value?.ID == Room.Settings.PlaylistItemId - && !Room.Playlist.Single(i => i.ID == Room.Settings.PlaylistItemId).Expired + client.Room.State != MultiplayerRoomState.Closed + && currentItem.Value?.ID == client.Room.Settings.PlaylistItemId + && !client.Room.Playlist.Single(i => i.ID == client.Room.Settings.PlaylistItemId).Expired && !operationInProgress.Value; // When the local user is the host and spectating the match, the ready button should be enabled only if any users are ready. if (localUser?.State == MultiplayerUserState.Spectating) - readyButton.Enabled.Value &= Client.IsHost && newCountReady > 0 && !Room.ActiveCountdowns.Any(c => c is MatchStartCountdown); + readyButton.Enabled.Value &= client.IsHost && newCountReady > 0 && !client.Room.ActiveCountdowns.Any(c => c is MatchStartCountdown); // When the local user is not the host, the button should only be enabled when no match is in progress. - if (!Client.IsHost) - readyButton.Enabled.Value &= Room.State == MultiplayerRoomState.Open; + if (!client.IsHost) + readyButton.Enabled.Value &= client.Room.State == MultiplayerRoomState.Open; // At all times, the countdown button should only be enabled when no match is in progress. - countdownButton.Enabled.Value &= Room.State == MultiplayerRoomState.Open; + countdownButton.Enabled.Value &= client.Room.State == MultiplayerRoomState.Open; if (newCountReady == countReady) return; @@ -249,6 +247,17 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match }); } + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (client.IsNotNull()) + { + client.RoomUpdated -= onRoomUpdated; + client.LoadRequested -= onLoadRequested; + } + } + public partial class ConfirmAbortDialog : DangerousActionDialog { public ConfirmAbortDialog(Action abortMatch, Action cancel) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.cs index ea7ab2dce3..92edc9b979 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.cs @@ -5,7 +5,9 @@ using System.Threading; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Database; @@ -17,7 +19,7 @@ using osuTK; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { - public partial class MultiplayerSpectateButton : MultiplayerRoomComposite + public partial class MultiplayerSpectateButton : CompositeDrawable { [Resolved] private OngoingOperationTracker ongoingOperationTracker { get; set; } = null!; @@ -25,6 +27,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match [Resolved] private OsuColour colours { get; set; } = null!; + [Resolved] + private MultiplayerClient client { get; set; } = null!; + + [Resolved] + private IBindable currentItem { get; set; } = null!; + private IBindable operationInProgress = null!; private readonly RoundedButton button; @@ -44,7 +52,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { var clickOperation = ongoingOperationTracker.BeginOperation(); - Client.ToggleSpectate().ContinueWith(_ => endOperation()); + client.ToggleSpectate().ContinueWith(_ => endOperation()); void endOperation() => clickOperation?.Dispose(); } @@ -63,19 +71,16 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { base.LoadComplete(); - CurrentPlaylistItem.BindValueChanged(_ => Scheduler.AddOnce(checkForAutomaticDownload), true); - } - - protected override void OnRoomUpdated() - { - base.OnRoomUpdated(); - + currentItem.BindValueChanged(_ => Scheduler.AddOnce(checkForAutomaticDownload), true); + client.RoomUpdated += onRoomUpdated; updateState(); } + private void onRoomUpdated() => Scheduler.AddOnce(updateState); + private void updateState() { - switch (Client.LocalUser?.State) + switch (client.LocalUser?.State) { default: button.Text = "Spectate"; @@ -88,8 +93,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match break; } - button.Enabled.Value = Client.Room != null - && Client.Room.State != MultiplayerRoomState.Closed + button.Enabled.Value = client.Room != null + && client.Room.State != MultiplayerRoomState.Closed && !operationInProgress.Value; Scheduler.AddOnce(checkForAutomaticDownload); @@ -112,11 +117,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match private void checkForAutomaticDownload() { - PlaylistItem? currentItem = CurrentPlaylistItem.Value; + PlaylistItem? item = currentItem.Value; downloadCheckCancellation?.Cancel(); - if (currentItem == null) + if (item == null) return; if (!automaticallyDownload.Value) @@ -128,13 +133,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match // // Rather than over-complicating this flow, let's only auto-download when spectating for the time being. // A potential path forward would be to have a local auto-download checkbox above the playlist item list area. - if (Client.LocalUser?.State != MultiplayerUserState.Spectating) + if (client.LocalUser?.State != MultiplayerUserState.Spectating) return; // In a perfect world we'd use BeatmapAvailability, but there's no event-driven flow for when a selection changes. // ie. if selection changes from "not downloaded" to another "not downloaded" we wouldn't get a value changed raised. beatmapLookupCache - .GetBeatmapAsync(currentItem.Beatmap.OnlineID, (downloadCheckCancellation = new CancellationTokenSource()).Token) + .GetBeatmapAsync(item.Beatmap.OnlineID, (downloadCheckCancellation = new CancellationTokenSource()).Token) .ContinueWith(resolved => Schedule(() => { var beatmapSet = resolved.GetResultSafely()?.BeatmapSet; @@ -150,5 +155,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match } #endregion + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (client.IsNotNull()) + client.RoomUpdated -= onRoomUpdated; + } } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs index 2d08d8ecf6..8ba85019d5 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.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 System; using System.Linq; using osu.Framework.Allocation; @@ -17,18 +15,24 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist /// /// The multiplayer playlist, containing lists to show the items from a in both gameplay-order and historical-order. /// - public partial class MultiplayerPlaylist : MultiplayerRoomComposite + public partial class MultiplayerPlaylist : CompositeDrawable { public readonly Bindable DisplayMode = new Bindable(); /// /// Invoked when an item requests to be edited. /// - public Action RequestEdit; + public Action? RequestEdit; - private MultiplayerPlaylistTabControl playlistTabControl; - private MultiplayerQueueList queueList; - private MultiplayerHistoryList historyList; + [Resolved] + private MultiplayerClient client { get; set; } = null!; + + [Resolved] + private IBindable currentItem { get; set; } = null!; + + private MultiplayerPlaylistTabControl playlistTabControl = null!; + private MultiplayerQueueList queueList = null!; + private MultiplayerHistoryList historyList = null!; private bool firstPopulation = true; [BackgroundDependencyLoader] @@ -54,14 +58,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist queueList = new MultiplayerQueueList { RelativeSizeAxes = Axes.Both, - SelectedItem = { BindTarget = CurrentPlaylistItem }, + SelectedItem = { BindTarget = currentItem }, RequestEdit = item => RequestEdit?.Invoke(item) }, historyList = new MultiplayerHistoryList { RelativeSizeAxes = Axes.Both, Alpha = 0, - SelectedItem = { BindTarget = CurrentPlaylistItem } + SelectedItem = { BindTarget = currentItem } } } } @@ -73,7 +77,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist protected override void LoadComplete() { base.LoadComplete(); + DisplayMode.BindValueChanged(onDisplayModeChanged, true); + client.ItemAdded += playlistItemAdded; + client.ItemRemoved += playlistItemRemoved; + client.ItemChanged += playlistItemChanged; + client.RoomUpdated += onRoomUpdated; + updateState(); } private void onDisplayModeChanged(ValueChangedEvent mode) @@ -82,11 +92,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist queueList.FadeTo(mode.NewValue == MultiplayerPlaylistDisplayMode.Queue ? 1 : 0, 100); } - protected override void OnRoomUpdated() - { - base.OnRoomUpdated(); + private void onRoomUpdated() => Scheduler.AddOnce(updateState); - if (Room == null) + private void updateState() + { + if (client.Room == null) { historyList.Items.Clear(); queueList.Items.Clear(); @@ -96,34 +106,27 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist if (firstPopulation) { - foreach (var item in Room.Playlist) + foreach (var item in client.Room.Playlist) addItemToLists(item); firstPopulation = false; } } - protected override void PlaylistItemAdded(MultiplayerPlaylistItem item) - { - base.PlaylistItemAdded(item); - addItemToLists(item); - } + private void playlistItemAdded(MultiplayerPlaylistItem item) => Schedule(() => addItemToLists(item)); - protected override void PlaylistItemRemoved(long item) - { - base.PlaylistItemRemoved(item); - removeItemFromLists(item); - } + private void playlistItemRemoved(long item) => Schedule(() => removeItemFromLists(item)); - protected override void PlaylistItemChanged(MultiplayerPlaylistItem item) + private void playlistItemChanged(MultiplayerPlaylistItem item) => Schedule(() => { - base.PlaylistItemChanged(item); + if (client.Room == null) + return; - var newApiItem = Playlist.SingleOrDefault(i => i.ID == item.ID); + var newApiItem = new PlaylistItem(item); var existingApiItemInQueue = queueList.Items.SingleOrDefault(i => i.ID == item.ID); // Test if the only change between the two playlist items is the order. - if (newApiItem != null && existingApiItemInQueue != null && existingApiItemInQueue.With(playlistOrder: newApiItem.PlaylistOrder).Equals(newApiItem)) + if (existingApiItemInQueue != null && existingApiItemInQueue.With(playlistOrder: newApiItem.PlaylistOrder).Equals(newApiItem)) { // Set the new playlist order directly without refreshing the DrawablePlaylistItem. existingApiItemInQueue.PlaylistOrder = newApiItem.PlaylistOrder; @@ -137,20 +140,20 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist removeItemFromLists(item.ID); addItemToLists(item); } - } + }); private void addItemToLists(MultiplayerPlaylistItem item) { - var apiItem = Playlist.SingleOrDefault(i => i.ID == item.ID); + var apiItem = client.Room?.Playlist.SingleOrDefault(i => i.ID == item.ID); // Item could have been removed from the playlist while the local player was in gameplay. if (apiItem == null) return; if (item.Expired) - historyList.Items.Add(apiItem); + historyList.Items.Add(new PlaylistItem(apiItem)); else - queueList.Items.Add(apiItem); + queueList.Items.Add(new PlaylistItem(apiItem)); } private void removeItemFromLists(long item) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomComposite.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomComposite.cs deleted file mode 100644 index ee5c84bf40..0000000000 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomComposite.cs +++ /dev/null @@ -1,125 +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 JetBrains.Annotations; -using osu.Framework.Allocation; -using osu.Game.Online.Multiplayer; -using osu.Game.Online.Rooms; - -namespace osu.Game.Screens.OnlinePlay.Multiplayer -{ - public abstract partial class MultiplayerRoomComposite : OnlinePlayComposite - { - [CanBeNull] - protected MultiplayerRoom Room => Client.Room; - - [Resolved] - protected MultiplayerClient Client { get; private set; } - - protected override void LoadComplete() - { - base.LoadComplete(); - - Client.RoomUpdated += invokeOnRoomUpdated; - Client.LoadRequested += invokeOnRoomLoadRequested; - Client.UserLeft += invokeUserLeft; - Client.UserKicked += invokeUserKicked; - Client.UserJoined += invokeUserJoined; - Client.ItemAdded += invokeItemAdded; - Client.ItemRemoved += invokeItemRemoved; - Client.ItemChanged += invokeItemChanged; - - OnRoomUpdated(); - } - - private void invokeOnRoomUpdated() => Scheduler.AddOnce(OnRoomUpdated); - private void invokeUserJoined(MultiplayerRoomUser user) => Scheduler.Add(() => UserJoined(user)); - private void invokeUserKicked(MultiplayerRoomUser user) => Scheduler.Add(() => UserKicked(user)); - private void invokeUserLeft(MultiplayerRoomUser user) => Scheduler.Add(() => UserLeft(user)); - private void invokeItemAdded(MultiplayerPlaylistItem item) => Schedule(() => PlaylistItemAdded(item)); - private void invokeItemRemoved(long item) => Schedule(() => PlaylistItemRemoved(item)); - private void invokeItemChanged(MultiplayerPlaylistItem item) => Schedule(() => PlaylistItemChanged(item)); - private void invokeOnRoomLoadRequested() => Scheduler.AddOnce(OnRoomLoadRequested); - - /// - /// Invoked when a user has joined the room. - /// - /// The user. - protected virtual void UserJoined(MultiplayerRoomUser user) - { - } - - /// - /// Invoked when a user has been kicked from the room (including the local user). - /// - /// The user. - protected virtual void UserKicked(MultiplayerRoomUser user) - { - } - - /// - /// Invoked when a user has left the room. - /// - /// The user. - protected virtual void UserLeft(MultiplayerRoomUser user) - { - } - - /// - /// Invoked when a playlist item is added to the room. - /// - /// The added playlist item. - protected virtual void PlaylistItemAdded(MultiplayerPlaylistItem item) - { - } - - /// - /// Invoked when a playlist item is removed from the room. - /// - /// The ID of the removed playlist item. - protected virtual void PlaylistItemRemoved(long item) - { - } - - /// - /// Invoked when a playlist item is changed in the room. - /// - /// The new playlist item, with an existing item's ID. - protected virtual void PlaylistItemChanged(MultiplayerPlaylistItem item) - { - } - - /// - /// Invoked when any change occurs to the multiplayer room. - /// - protected virtual void OnRoomUpdated() - { - } - - /// - /// Invoked when the room requests the local user to load into gameplay. - /// - protected virtual void OnRoomLoadRequested() - { - } - - protected override void Dispose(bool isDisposing) - { - if (Client != null) - { - Client.RoomUpdated -= invokeOnRoomUpdated; - Client.LoadRequested -= invokeOnRoomLoadRequested; - Client.UserLeft -= invokeUserLeft; - Client.UserKicked -= invokeUserKicked; - Client.UserJoined -= invokeUserJoined; - Client.ItemAdded -= invokeItemAdded; - Client.ItemRemoved -= invokeItemRemoved; - Client.ItemChanged -= invokeItemChanged; - } - - base.Dispose(isDisposing); - } - } -} diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomSounds.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomSounds.cs index 90595bc33b..d53e485c86 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomSounds.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomSounds.cs @@ -1,23 +1,26 @@ // 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 osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; -using osu.Framework.Bindables; -using osu.Game.Online.API.Requests.Responses; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Graphics.Containers; using osu.Game.Online.Multiplayer; namespace osu.Game.Screens.OnlinePlay.Multiplayer { - public partial class MultiplayerRoomSounds : MultiplayerRoomComposite + public partial class MultiplayerRoomSounds : CompositeDrawable { - private Sample hostChangedSample; - private Sample userJoinedSample; - private Sample userLeftSample; - private Sample userKickedSample; + [Resolved] + private MultiplayerClient client { get; set; } = null!; + + private Sample? hostChangedSample; + private Sample? userJoinedSample; + private Sample? userLeftSample; + private Sample? userKickedSample; + private MultiplayerRoomUser? host; [BackgroundDependencyLoader] private void load(AudioManager audio) @@ -32,36 +35,47 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { base.LoadComplete(); - Host.BindValueChanged(hostChanged); + client.RoomUpdated += onRoomUpdated; + client.UserJoined += onUserJoined; + client.UserLeft += onUserLeft; + client.UserKicked += onUserKicked; + updateState(); } - protected override void UserJoined(MultiplayerRoomUser user) + private void onRoomUpdated() => Scheduler.AddOnce(updateState); + + private void updateState() { - base.UserJoined(user); + if (EqualityComparer.Default.Equals(host, client.Room?.Host)) + return; - Scheduler.AddOnce(() => userJoinedSample?.Play()); - } - - protected override void UserLeft(MultiplayerRoomUser user) - { - base.UserLeft(user); - - Scheduler.AddOnce(() => userLeftSample?.Play()); - } - - protected override void UserKicked(MultiplayerRoomUser user) - { - base.UserKicked(user); - - Scheduler.AddOnce(() => userKickedSample?.Play()); - } - - private void hostChanged(ValueChangedEvent value) - { // only play sound when the host changes from an already-existing host. - if (value.OldValue == null) return; + if (host != null) + Scheduler.AddOnce(() => hostChangedSample?.Play()); - Scheduler.AddOnce(() => hostChangedSample?.Play()); + host = client.Room?.Host; + } + + private void onUserJoined(MultiplayerRoomUser user) + => Scheduler.AddOnce(() => userJoinedSample?.Play()); + + private void onUserLeft(MultiplayerRoomUser user) + => Scheduler.AddOnce(() => userLeftSample?.Play()); + + private void onUserKicked(MultiplayerRoomUser user) + => Scheduler.AddOnce(() => userKickedSample?.Play()); + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (client.IsNotNull()) + { + client.RoomUpdated -= onRoomUpdated; + client.UserJoined -= onUserJoined; + client.UserLeft -= onUserLeft; + client.UserKicked -= onUserKicked; + } } } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs index c79c210e30..7e42b18240 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; @@ -30,7 +31,7 @@ using osuTK.Graphics; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants { - public partial class ParticipantPanel : MultiplayerRoomComposite, IHasContextMenu + public partial class ParticipantPanel : CompositeDrawable, IHasContextMenu { public readonly MultiplayerRoomUser User; @@ -40,6 +41,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants [Resolved] private IRulesetStore rulesets { get; set; } = null!; + [Resolved] + private MultiplayerClient client { get; set; } = null!; + private SpriteIcon crown = null!; private OsuSpriteText userRankText = null!; @@ -171,23 +175,31 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants Origin = Anchor.Centre, Alpha = 0, Margin = new MarginPadding(4), - Action = () => Client.KickUser(User.UserID).FireAndForget(), + Action = () => client.KickUser(User.UserID).FireAndForget(), }, }, } }; } - protected override void OnRoomUpdated() + protected override void LoadComplete() { - base.OnRoomUpdated(); + base.LoadComplete(); - if (Room == null || Client.LocalUser == null) + client.RoomUpdated += onRoomUpdated; + updateState(); + } + + private void onRoomUpdated() => Scheduler.AddOnce(updateState); + + private void updateState() + { + if (client.Room == null || client.LocalUser == null) return; const double fade_time = 50; - var currentItem = Playlist.GetCurrentItem(); + MultiplayerPlaylistItem? currentItem = client.Room.GetCurrentItem(); Ruleset? ruleset = currentItem != null ? rulesets.GetRuleset(currentItem.RulesetID)?.CreateInstance() : null; int? currentModeRank = ruleset != null ? User.User?.RulesetsStatistics?.GetValueOrDefault(ruleset.ShortName)?.GlobalRank : null; @@ -200,8 +212,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants else userModsDisplay.FadeOut(fade_time); - kickButton.Alpha = Client.IsHost && !User.Equals(Client.LocalUser) ? 1 : 0; - crown.Alpha = Room.Host?.Equals(User) == true ? 1 : 0; + kickButton.Alpha = client.IsHost && !User.Equals(client.LocalUser) ? 1 : 0; + crown.Alpha = client.Room.Host?.Equals(User) == true ? 1 : 0; // If the mods are updated at the end of the frame, the flow container will skip a reflow cycle: https://github.com/ppy/osu-framework/issues/4187 // This looks particularly jarring here, so re-schedule the update to that start of our frame as a fix. @@ -215,7 +227,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants { get { - if (Room == null) + if (client.Room == null) return null; // If the local user is targetted. @@ -223,7 +235,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants return null; // If the local user is not the host of the room. - if (Room.Host?.UserID != api.LocalUser.Value.Id) + if (client.Room.Host?.UserID != api.LocalUser.Value.Id) return null; int targetUser = User.UserID; @@ -233,23 +245,31 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants new OsuMenuItem("Give host", MenuItemType.Standard, () => { // Ensure the local user is still host. - if (!Client.IsHost) + if (!client.IsHost) return; - Client.TransferHost(targetUser).FireAndForget(); + client.TransferHost(targetUser).FireAndForget(); }), new OsuMenuItem("Kick", MenuItemType.Destructive, () => { // Ensure the local user is still host. - if (!Client.IsHost) + if (!client.IsHost) return; - Client.KickUser(targetUser).FireAndForget(); + client.KickUser(targetUser).FireAndForget(); }) }; } } + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (client.IsNotNull()) + client.RoomUpdated -= onRoomUpdated; + } + public partial class KickButton : IconButton { public KickButton() diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantsList.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantsList.cs index 6a7a3758c3..a9d7f4ab52 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantsList.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantsList.cs @@ -1,24 +1,24 @@ // 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.Linq; -using JetBrains.Annotations; using osu.Framework.Allocation; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics.Containers; +using osu.Game.Online.Multiplayer; using osuTK; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants { - public partial class ParticipantsList : MultiplayerRoomComposite + public partial class ParticipantsList : CompositeDrawable { - private FillFlowContainer panels; + private FillFlowContainer panels = null!; + private ParticipantPanel? currentHostPanel; - [CanBeNull] - private ParticipantPanel currentHostPanel; + [Resolved] + private MultiplayerClient client { get; set; } = null!; [BackgroundDependencyLoader] private void load() @@ -37,11 +37,19 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants }; } - protected override void OnRoomUpdated() + protected override void LoadComplete() { - base.OnRoomUpdated(); + base.LoadComplete(); - if (Room == null) + client.RoomUpdated += onRoomUpdated; + updateState(); + } + + private void onRoomUpdated() => Scheduler.AddOnce(updateState); + + private void updateState() + { + if (client.Room == null) panels.Clear(); else { @@ -49,15 +57,15 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants foreach (var p in panels) { // Note that we *must* use reference equality here, as this call is scheduled and a user may have left and joined since it was last run. - if (Room.Users.All(u => !ReferenceEquals(p.User, u))) + if (client.Room.Users.All(u => !ReferenceEquals(p.User, u))) p.Expire(); } // Add panels for all users new to the room. - foreach (var user in Room.Users.Except(panels.Select(p => p.User))) + foreach (var user in client.Room.Users.Except(panels.Select(p => p.User))) panels.Add(new ParticipantPanel(user)); - if (currentHostPanel == null || !currentHostPanel.User.Equals(Room.Host)) + if (currentHostPanel == null || !currentHostPanel.User.Equals(client.Room.Host)) { // Reset position of previous host back to normal, if one existing. if (currentHostPanel != null && panels.Contains(currentHostPanel)) @@ -66,9 +74,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants currentHostPanel = null; // Change position of new host to display above all participants. - if (Room.Host != null) + if (client.Room.Host != null) { - currentHostPanel = panels.SingleOrDefault(u => u.User.Equals(Room.Host)); + currentHostPanel = panels.SingleOrDefault(u => u.User.Equals(client.Room.Host)); if (currentHostPanel != null) panels.SetLayoutPosition(currentHostPanel, -1); @@ -76,5 +84,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants } } } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (client.IsNotNull()) + client.RoomUpdated -= onRoomUpdated; + } } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/TeamDisplay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/TeamDisplay.cs index fe57ad26a5..bd9511d50d 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/TeamDisplay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/TeamDisplay.cs @@ -1,12 +1,11 @@ // 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.Linq; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; @@ -20,27 +19,26 @@ using osuTK.Graphics; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants { - internal partial class TeamDisplay : MultiplayerRoomComposite + internal partial class TeamDisplay : CompositeDrawable { private readonly MultiplayerRoomUser user; - private Drawable box; - - private Sample sampleTeamSwap; + [Resolved] + private OsuColour colours { get; set; } = null!; [Resolved] - private OsuColour colours { get; set; } + private MultiplayerClient client { get; set; } = null!; - private OsuClickableContainer clickableContent; + private OsuClickableContainer clickableContent = null!; + private Drawable box = null!; + private Sample? sampleTeamSwap; public TeamDisplay(MultiplayerRoomUser user) { this.user = user; RelativeSizeAxes = Axes.Y; - AutoSizeAxes = Axes.X; - Margin = new MarginPadding { Horizontal = 3 }; } @@ -71,7 +69,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants } }; - if (Client.LocalUser?.Equals(user) == true) + if (client.LocalUser?.Equals(user) == true) { clickableContent.Action = changeTeam; clickableContent.TooltipText = "Change team"; @@ -80,23 +78,31 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants sampleTeamSwap = audio.Samples.Get(@"Multiplayer/team-swap"); } + protected override void LoadComplete() + { + base.LoadComplete(); + + client.RoomUpdated += onRoomUpdated; + updateState(); + } + private void changeTeam() { - Client.SendMatchRequest(new ChangeTeamRequest + client.SendMatchRequest(new ChangeTeamRequest { - TeamID = ((Client.LocalUser?.MatchState as TeamVersusUserState)?.TeamID + 1) % 2 ?? 0, + TeamID = ((client.LocalUser?.MatchState as TeamVersusUserState)?.TeamID + 1) % 2 ?? 0, }).FireAndForget(); } public int? DisplayedTeam { get; private set; } - protected override void OnRoomUpdated() - { - base.OnRoomUpdated(); + private void onRoomUpdated() => Scheduler.AddOnce(updateState); + private void updateState() + { // we don't have a way of knowing when an individual user's state has updated, so just handle on RoomUpdated for now. - var userRoomState = Room?.Users.FirstOrDefault(u => u.Equals(user))?.MatchState; + var userRoomState = client.Room?.Users.FirstOrDefault(u => u.Equals(user))?.MatchState; const double duration = 400; @@ -138,5 +144,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants return colours.Blue; } } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (client.IsNotNull()) + client.RoomUpdated -= onRoomUpdated; + } } } From 9f08b377920c108b5cfd5d287d0dafaddcd534df Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 6 Nov 2024 16:35:11 +0900 Subject: [PATCH 24/57] Fix up tests --- .../Multiplayer/TestSceneMatchStartControl.cs | 23 ++++++++++--------- .../TestSceneMultiplayerMatchFooter.cs | 6 +++++ .../TestSceneMultiplayerPlaylist.cs | 16 +++++++------ .../TestSceneMultiplayerSpectateButton.cs | 21 ++++++++--------- 4 files changed, 36 insertions(+), 30 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs index 2d61c26a6b..3b10509895 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.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 System; using System.Linq; using Moq; @@ -36,15 +34,18 @@ namespace osu.Game.Tests.Visual.Multiplayer private readonly Bindable beatmapAvailability = new Bindable(); private readonly Bindable room = new Bindable(); - private MultiplayerRoom multiplayerRoom; - private MultiplayerRoomUser localUser; - private OngoingOperationTracker ongoingOperationTracker; + private MultiplayerRoom multiplayerRoom = null!; + private MultiplayerRoomUser localUser = null!; + private OngoingOperationTracker ongoingOperationTracker = null!; - private PopoverContainer content; - private MatchStartControl control; + private PopoverContainer content = null!; + private MatchStartControl control = null!; private OsuButton readyButton => control.ChildrenOfType().Single(); + [Cached(typeof(IBindable))] + private readonly Bindable currentItem = new Bindable(); + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) => new CachedModelDependencyContainer(base.CreateChildDependencies(parent)) { Model = { BindTarget = room } }; @@ -112,15 +113,15 @@ namespace osu.Game.Tests.Visual.Multiplayer beatmapAvailability.Value = BeatmapAvailability.LocallyAvailable(); - var playlistItem = new PlaylistItem(Beatmap.Value.BeatmapInfo) + currentItem.Value = new PlaylistItem(Beatmap.Value.BeatmapInfo) { RulesetID = Beatmap.Value.BeatmapInfo.Ruleset.OnlineID }; room.Value = new Room { - Playlist = { playlistItem }, - CurrentPlaylistItem = { Value = playlistItem } + Playlist = { currentItem.Value }, + CurrentPlaylistItem = { BindTarget = currentItem } }; localUser = new MultiplayerRoomUser(API.LocalUser.Value.Id) { User = API.LocalUser.Value }; @@ -129,7 +130,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { Playlist = { - TestMultiplayerClient.CreateMultiplayerPlaylistItem(playlistItem), + TestMultiplayerClient.CreateMultiplayerPlaylistItem(currentItem.Value), }, Users = { localUser }, Host = localUser, diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchFooter.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchFooter.cs index c2d3b17ccb..9d8ef76e75 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchFooter.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchFooter.cs @@ -1,15 +1,21 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; +using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Multiplayer.Match; namespace osu.Game.Tests.Visual.Multiplayer { public partial class TestSceneMultiplayerMatchFooter : MultiplayerTestScene { + [Cached(typeof(IBindable))] + private readonly Bindable currentItem = new Bindable(); + public override void SetUpSteps() { base.SetUpSteps(); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs index 2100f82886..3baabecd84 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs @@ -1,13 +1,12 @@ // 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.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; +using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Platform; @@ -29,10 +28,13 @@ namespace osu.Game.Tests.Visual.Multiplayer { public partial class TestSceneMultiplayerPlaylist : MultiplayerTestScene { - private MultiplayerPlaylist list; - private BeatmapManager beatmaps; - private BeatmapSetInfo importedSet; - private BeatmapInfo importedBeatmap; + [Cached(typeof(IBindable))] + private readonly Bindable currentItem = new Bindable(); + + private MultiplayerPlaylist list = null!; + private BeatmapManager beatmaps = null!; + private BeatmapSetInfo importedSet = null!; + private BeatmapInfo importedBeatmap = null!; [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) @@ -198,7 +200,7 @@ namespace osu.Game.Tests.Visual.Multiplayer addItemStep(); addItemStep(); - DrawableRoomPlaylistItem[] drawableItems = null; + DrawableRoomPlaylistItem[] drawableItems = null!; AddStep("get drawable items", () => drawableItems = this.ChildrenOfType().ToArray()); // Add 1 item for another user. diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs index 816ba4ca32..5ae5d1e228 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.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 System.Linq; using NUnit.Framework; using osu.Framework.Allocation; @@ -28,13 +26,14 @@ namespace osu.Game.Tests.Visual.Multiplayer { public partial class TestSceneMultiplayerSpectateButton : MultiplayerTestScene { - private MultiplayerSpectateButton spectateButton; - private MatchStartControl startControl; + [Cached(typeof(IBindable))] + private readonly Bindable currentItem = new Bindable(); - private readonly Bindable selectedItem = new Bindable(); + private MultiplayerSpectateButton spectateButton = null!; + private MatchStartControl startControl = null!; - private BeatmapSetInfo importedSet; - private BeatmapManager beatmaps; + private BeatmapSetInfo importedSet = null!; + private BeatmapManager beatmaps = null!; [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) @@ -52,14 +51,12 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("create button", () => { - AvailabilityTracker.SelectedItem.BindTo(selectedItem); + AvailabilityTracker.SelectedItem.BindTo(currentItem); importedSet = beatmaps.GetAllUsableBeatmapSets().First(); Beatmap.Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First()); - selectedItem.Value = new PlaylistItem(Beatmap.Value.BeatmapInfo) - { - RulesetID = Beatmap.Value.BeatmapInfo.Ruleset.OnlineID, - }; + + currentItem.Value = SelectedRoom.Value.Playlist.First(); Child = new PopoverContainer { From 35d004cdc2706420738cf3b96f63eb4147dbab5a Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 6 Nov 2024 20:39:10 +0900 Subject: [PATCH 25/57] Fix intermittent beatmap recommendations test --- .../Visual/SongSelect/TestSceneBeatmapRecommendations.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs index 16c8bc1a6b..82791056b6 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs @@ -191,7 +191,7 @@ namespace osu.Game.Tests.Visual.SongSelect { AddStep("present beatmap", () => Game.PresentBeatmap(getImport())); - AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is Screens.Select.SongSelect); + AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is Screens.Select.SongSelect select && select.BeatmapSetsLoaded); AddUntilStep("recommended beatmap displayed", () => Game.Beatmap.Value.BeatmapInfo.MatchesOnlineID(getImport().Beatmaps[expectedDiff - 1])); } } From bd630c189e16754e630fdbcacccae455fe0239dd Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 7 Nov 2024 17:25:42 +0900 Subject: [PATCH 26/57] Fix tests not working by forgoing beatmap updates --- .../TestSceneBeatmapRecommendations.cs | 34 +++++++++++++++++++ .../Beatmaps/BeatmapOnlineChangeIngest.cs | 4 +-- osu.Game/Beatmaps/BeatmapUpdater.cs | 16 +-------- osu.Game/Beatmaps/IBeatmapUpdater.cs | 30 ++++++++++++++++ .../Database/BackgroundDataStoreProcessor.cs | 2 +- osu.Game/OsuGameBase.cs | 6 ++-- 6 files changed, 72 insertions(+), 20 deletions(-) create mode 100644 osu.Game/Beatmaps/IBeatmapUpdater.cs diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs index 82791056b6..c9ad22e8df 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs @@ -10,9 +10,12 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Framework.Platform; using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Database; using osu.Game.Extensions; +using osu.Game.Online.API; using osu.Game.Rulesets; using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Mania; @@ -194,5 +197,36 @@ namespace osu.Game.Tests.Visual.SongSelect AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is Screens.Select.SongSelect select && select.BeatmapSetsLoaded); AddUntilStep("recommended beatmap displayed", () => Game.Beatmap.Value.BeatmapInfo.MatchesOnlineID(getImport().Beatmaps[expectedDiff - 1])); } + + protected override TestOsuGame CreateTestGame() => new NoBeatmapUpdateGame(LocalStorage, API); + + private class NoBeatmapUpdateGame : TestOsuGame + { + public NoBeatmapUpdateGame(Storage storage, IAPIProvider api, string[] args = null) + : base(storage, api, args) + { + } + + protected override IBeatmapUpdater CreateBeatmapUpdater() => new TestBeatmapUpdater(); + + private class TestBeatmapUpdater : IBeatmapUpdater + { + public void Queue(Live beatmapSet, MetadataLookupScope lookupScope = MetadataLookupScope.LocalCacheFirst) + { + } + + public void Process(BeatmapSetInfo beatmapSet, MetadataLookupScope lookupScope = MetadataLookupScope.LocalCacheFirst) + { + } + + public void ProcessObjectCounts(BeatmapInfo beatmapInfo, MetadataLookupScope lookupScope = MetadataLookupScope.LocalCacheFirst) + { + } + + public void Dispose() + { + } + } + } } } diff --git a/osu.Game/Beatmaps/BeatmapOnlineChangeIngest.cs b/osu.Game/Beatmaps/BeatmapOnlineChangeIngest.cs index b160043820..965f3be0aa 100644 --- a/osu.Game/Beatmaps/BeatmapOnlineChangeIngest.cs +++ b/osu.Game/Beatmaps/BeatmapOnlineChangeIngest.cs @@ -13,11 +13,11 @@ namespace osu.Game.Beatmaps /// public partial class BeatmapOnlineChangeIngest : Component { - private readonly BeatmapUpdater beatmapUpdater; + private readonly IBeatmapUpdater beatmapUpdater; private readonly RealmAccess realm; private readonly MetadataClient metadataClient; - public BeatmapOnlineChangeIngest(BeatmapUpdater beatmapUpdater, RealmAccess realm, MetadataClient metadataClient) + public BeatmapOnlineChangeIngest(IBeatmapUpdater beatmapUpdater, RealmAccess realm, MetadataClient metadataClient) { this.beatmapUpdater = beatmapUpdater; this.realm = realm; diff --git a/osu.Game/Beatmaps/BeatmapUpdater.cs b/osu.Game/Beatmaps/BeatmapUpdater.cs index e897d28916..04ed9c2488 100644 --- a/osu.Game/Beatmaps/BeatmapUpdater.cs +++ b/osu.Game/Beatmaps/BeatmapUpdater.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.Diagnostics; using System.Linq; using System.Threading.Tasks; @@ -15,10 +14,7 @@ using osu.Game.Rulesets.Objects.Types; namespace osu.Game.Beatmaps { - /// - /// Handles all processing required to ensure a local beatmap is in a consistent state with any changes. - /// - public class BeatmapUpdater : IDisposable + public class BeatmapUpdater : IBeatmapUpdater { private readonly IWorkingBeatmapCache workingBeatmapCache; @@ -38,11 +34,6 @@ namespace osu.Game.Beatmaps metadataLookup = new BeatmapUpdaterMetadataLookup(api, storage); } - /// - /// Queue a beatmap for background processing. - /// - /// The managed beatmap set to update. A transaction will be opened to apply changes. - /// The preferred scope to use for metadata lookup. public void Queue(Live beatmapSet, MetadataLookupScope lookupScope = MetadataLookupScope.LocalCacheFirst) { Logger.Log($"Queueing change for local beatmap {beatmapSet}"); @@ -50,11 +41,6 @@ namespace osu.Game.Beatmaps updateScheduler); } - /// - /// Run all processing on a beatmap immediately. - /// - /// The managed beatmap set to update. A transaction will be opened to apply changes. - /// The preferred scope to use for metadata lookup. public void Process(BeatmapSetInfo beatmapSet, MetadataLookupScope lookupScope = MetadataLookupScope.LocalCacheFirst) => beatmapSet.Realm!.Write(_ => { // Before we use below, we want to invalidate. diff --git a/osu.Game/Beatmaps/IBeatmapUpdater.cs b/osu.Game/Beatmaps/IBeatmapUpdater.cs new file mode 100644 index 0000000000..ad543e667e --- /dev/null +++ b/osu.Game/Beatmaps/IBeatmapUpdater.cs @@ -0,0 +1,30 @@ +// 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.Game.Database; + +namespace osu.Game.Beatmaps +{ + /// + /// Handles all processing required to ensure a local beatmap is in a consistent state with any changes. + /// + public interface IBeatmapUpdater : IDisposable + { + /// + /// Queue a beatmap for background processing. + /// + /// The managed beatmap set to update. A transaction will be opened to apply changes. + /// The preferred scope to use for metadata lookup. + void Queue(Live beatmapSet, MetadataLookupScope lookupScope = MetadataLookupScope.LocalCacheFirst); + + /// + /// Run all processing on a beatmap immediately. + /// + /// The managed beatmap set to update. A transaction will be opened to apply changes. + /// The preferred scope to use for metadata lookup. + void Process(BeatmapSetInfo beatmapSet, MetadataLookupScope lookupScope = MetadataLookupScope.LocalCacheFirst); + + void ProcessObjectCounts(BeatmapInfo beatmapInfo, MetadataLookupScope lookupScope = MetadataLookupScope.LocalCacheFirst); + } +} diff --git a/osu.Game/Database/BackgroundDataStoreProcessor.cs b/osu.Game/Database/BackgroundDataStoreProcessor.cs index 3efd4da3aa..1512b6be93 100644 --- a/osu.Game/Database/BackgroundDataStoreProcessor.cs +++ b/osu.Game/Database/BackgroundDataStoreProcessor.cs @@ -46,7 +46,7 @@ namespace osu.Game.Database private RealmAccess realmAccess { get; set; } = null!; [Resolved] - private BeatmapUpdater beatmapUpdater { get; set; } = null!; + private IBeatmapUpdater beatmapUpdater { get; set; } = null!; [Resolved] private IBindable gameBeatmap { get; set; } = null!; diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index d4704d1c72..dc13924b4f 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -198,7 +198,7 @@ namespace osu.Game public readonly Bindable>> AvailableMods = new Bindable>>(new Dictionary>()); private BeatmapDifficultyCache difficultyCache; - private BeatmapUpdater beatmapUpdater; + private IBeatmapUpdater beatmapUpdater; private UserLookupCache userCache; private BeatmapLookupCache beatmapCache; @@ -324,7 +324,7 @@ namespace osu.Game base.Content.Add(difficultyCache); // TODO: OsuGame or OsuGameBase? - dependencies.CacheAs(beatmapUpdater = new BeatmapUpdater(BeatmapManager, difficultyCache, API, Storage)); + dependencies.CacheAs(beatmapUpdater = CreateBeatmapUpdater()); dependencies.CacheAs(SpectatorClient = new OnlineSpectatorClient(endpoints)); dependencies.CacheAs(MultiplayerClient = new OnlineMultiplayerClient(endpoints)); dependencies.CacheAs(metadataClient = new OnlineMetadataClient(endpoints)); @@ -563,6 +563,8 @@ namespace osu.Game } } + protected virtual IBeatmapUpdater CreateBeatmapUpdater() => new BeatmapUpdater(BeatmapManager, difficultyCache, API, Storage); + protected override UserInputManager CreateUserInputManager() => new OsuUserInputManager(); protected virtual BatteryInfo CreateBatteryInfo() => null; From 4d7fd236c5dc74a8abcf1588be43cad2dab8b0c9 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 7 Nov 2024 17:28:31 +0900 Subject: [PATCH 27/57] Make class partial --- .../Visual/SongSelect/TestSceneBeatmapRecommendations.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs index c9ad22e8df..66862e1b78 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs @@ -200,7 +200,7 @@ namespace osu.Game.Tests.Visual.SongSelect protected override TestOsuGame CreateTestGame() => new NoBeatmapUpdateGame(LocalStorage, API); - private class NoBeatmapUpdateGame : TestOsuGame + private partial class NoBeatmapUpdateGame : TestOsuGame { public NoBeatmapUpdateGame(Storage storage, IAPIProvider api, string[] args = null) : base(storage, api, args) From c7d0a7dde216fa7a1fe8be70c0097733aa26a837 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 7 Nov 2024 18:31:06 +0900 Subject: [PATCH 28/57] Update xmldoc and make realm transactions more obvious --- osu.Game/Beatmaps/BeatmapUpdater.cs | 70 +++++++++++++++------------- osu.Game/Beatmaps/IBeatmapUpdater.cs | 5 ++ 2 files changed, 43 insertions(+), 32 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapUpdater.cs b/osu.Game/Beatmaps/BeatmapUpdater.cs index 04ed9c2488..efb432b84e 100644 --- a/osu.Game/Beatmaps/BeatmapUpdater.cs +++ b/osu.Game/Beatmaps/BeatmapUpdater.cs @@ -41,50 +41,56 @@ namespace osu.Game.Beatmaps updateScheduler); } - public void Process(BeatmapSetInfo beatmapSet, MetadataLookupScope lookupScope = MetadataLookupScope.LocalCacheFirst) => beatmapSet.Realm!.Write(_ => + public void Process(BeatmapSetInfo beatmapSet, MetadataLookupScope lookupScope = MetadataLookupScope.LocalCacheFirst) { - // Before we use below, we want to invalidate. - workingBeatmapCache.Invalidate(beatmapSet); - - if (lookupScope != MetadataLookupScope.None) - metadataLookup.Update(beatmapSet, lookupScope == MetadataLookupScope.OnlineFirst); - - foreach (var beatmap in beatmapSet.Beatmaps) + beatmapSet.Realm!.Write(_ => { - difficultyCache.Invalidate(beatmap); + // Before we use below, we want to invalidate. + workingBeatmapCache.Invalidate(beatmapSet); - var working = workingBeatmapCache.GetWorkingBeatmap(beatmap); - var ruleset = working.BeatmapInfo.Ruleset.CreateInstance(); + if (lookupScope != MetadataLookupScope.None) + metadataLookup.Update(beatmapSet, lookupScope == MetadataLookupScope.OnlineFirst); - Debug.Assert(ruleset != null); + foreach (var beatmap in beatmapSet.Beatmaps) + { + difficultyCache.Invalidate(beatmap); - var calculator = ruleset.CreateDifficultyCalculator(working); + var working = workingBeatmapCache.GetWorkingBeatmap(beatmap); + var ruleset = working.BeatmapInfo.Ruleset.CreateInstance(); - beatmap.StarRating = calculator.Calculate().StarRating; - beatmap.Length = working.Beatmap.CalculatePlayableLength(); - beatmap.BPM = 60000 / working.Beatmap.GetMostCommonBeatLength(); - beatmap.EndTimeObjectCount = working.Beatmap.HitObjects.Count(h => h is IHasDuration); - beatmap.TotalObjectCount = working.Beatmap.HitObjects.Count; - } + Debug.Assert(ruleset != null); - // And invalidate again afterwards as re-fetching the most up-to-date database metadata will be required. - workingBeatmapCache.Invalidate(beatmapSet); - }); + var calculator = ruleset.CreateDifficultyCalculator(working); - public void ProcessObjectCounts(BeatmapInfo beatmapInfo, MetadataLookupScope lookupScope = MetadataLookupScope.LocalCacheFirst) => beatmapInfo.Realm!.Write(_ => + beatmap.StarRating = calculator.Calculate().StarRating; + beatmap.Length = working.Beatmap.CalculatePlayableLength(); + beatmap.BPM = 60000 / working.Beatmap.GetMostCommonBeatLength(); + beatmap.EndTimeObjectCount = working.Beatmap.HitObjects.Count(h => h is IHasDuration); + beatmap.TotalObjectCount = working.Beatmap.HitObjects.Count; + } + + // And invalidate again afterwards as re-fetching the most up-to-date database metadata will be required. + workingBeatmapCache.Invalidate(beatmapSet); + }); + } + + public void ProcessObjectCounts(BeatmapInfo beatmapInfo, MetadataLookupScope lookupScope = MetadataLookupScope.LocalCacheFirst) { - // Before we use below, we want to invalidate. - workingBeatmapCache.Invalidate(beatmapInfo); + beatmapInfo.Realm!.Write(_ => + { + // Before we use below, we want to invalidate. + workingBeatmapCache.Invalidate(beatmapInfo); - var working = workingBeatmapCache.GetWorkingBeatmap(beatmapInfo); - var beatmap = working.Beatmap; + var working = workingBeatmapCache.GetWorkingBeatmap(beatmapInfo); + var beatmap = working.Beatmap; - beatmapInfo.EndTimeObjectCount = beatmap.HitObjects.Count(h => h is IHasDuration); - beatmapInfo.TotalObjectCount = beatmap.HitObjects.Count; + beatmapInfo.EndTimeObjectCount = beatmap.HitObjects.Count(h => h is IHasDuration); + beatmapInfo.TotalObjectCount = beatmap.HitObjects.Count; - // And invalidate again afterwards as re-fetching the most up-to-date database metadata will be required. - workingBeatmapCache.Invalidate(beatmapInfo); - }); + // And invalidate again afterwards as re-fetching the most up-to-date database metadata will be required. + workingBeatmapCache.Invalidate(beatmapInfo); + }); + } #region Implementation of IDisposable diff --git a/osu.Game/Beatmaps/IBeatmapUpdater.cs b/osu.Game/Beatmaps/IBeatmapUpdater.cs index ad543e667e..062984adf0 100644 --- a/osu.Game/Beatmaps/IBeatmapUpdater.cs +++ b/osu.Game/Beatmaps/IBeatmapUpdater.cs @@ -25,6 +25,11 @@ namespace osu.Game.Beatmaps /// The preferred scope to use for metadata lookup. void Process(BeatmapSetInfo beatmapSet, MetadataLookupScope lookupScope = MetadataLookupScope.LocalCacheFirst); + /// + /// Runs a subset of processing focused on updating any cached beatmap object counts. + /// + /// The managed beatmap to update. A transaction will be opened to apply changes. + /// The preferred scope to use for metadata lookup. void ProcessObjectCounts(BeatmapInfo beatmapInfo, MetadataLookupScope lookupScope = MetadataLookupScope.LocalCacheFirst); } } From a5e6da76cb090a72932d7c610d037390266c5036 Mon Sep 17 00:00:00 2001 From: Jay Lawton Date: Thu, 7 Nov 2024 19:53:53 +1000 Subject: [PATCH 29/57] 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 b07e8399c0..c28635ef22 100644 --- a/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs +++ b/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs @@ -26,10 +26,10 @@ namespace osu.Game.Rulesets.Difficulty.Skills 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 @@ namespace osu.Game.Rulesets.Difficulty.Skills 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 30/57] 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 96180c0aa1..c36534dcef 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs @@ -60,20 +60,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills 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 cb2e5ac2c4c974fe134f7bce2969acb67704b2ec Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 7 Nov 2024 19:15:58 +0900 Subject: [PATCH 31/57] Skip diffcalc job if target is not valid Same as all other jobs... Just so that it doesn't appear as "completed" in the workflow list. --- .github/workflows/diffcalc.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/diffcalc.yml b/.github/workflows/diffcalc.yml index c2eeff20df..15fbcf38e4 100644 --- a/.github/workflows/diffcalc.yml +++ b/.github/workflows/diffcalc.yml @@ -107,6 +107,7 @@ 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: From 748055ab29d04c53542df0814689b05970110f53 Mon Sep 17 00:00:00 2001 From: Jay Lawton Date: Thu, 7 Nov 2024 20:15:59 +1000 Subject: [PATCH 32/57] 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 b3d530e3af..faf91e4652 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs @@ -34,7 +34,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills { 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 c36534dcef..559a871df1 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs @@ -23,7 +23,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills /// 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 e5aa25c1eb..d2c4bbb618 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs @@ -40,7 +40,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills 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 33/57] 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 c28635ef22..d597a0f6b2 100644 --- a/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs +++ b/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs @@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Difficulty.Skills 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 @@ namespace osu.Game.Rulesets.Difficulty.Skills /// 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 34/57] 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 acf01b2a83..575e03051c 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -48,8 +48,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty 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 d597a0f6b2..1cb6b69f91 100644 --- a/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs +++ b/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs @@ -68,7 +68,7 @@ namespace osu.Game.Rulesets.Difficulty.Skills /// 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 b0c6042b2a418e2dcd4dcdf404f82cb1b5aff315 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 7 Nov 2024 13:58:57 +0100 Subject: [PATCH 35/57] Fix HUD elements shifting in unintended manner when partially off-screen flipped skin elements are present Closes https://github.com/ppy/osu/issues/30286. --- osu.Game/Screens/Play/HUDOverlay.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 292f554483..a6c2405eb6 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -300,7 +300,9 @@ namespace osu.Game.Screens.Play if (element is LegacyHealthDisplay) return; - float bottom = drawable.ScreenSpaceDrawQuad.BottomRight.Y; + // AABB is used here because the drawable can be flipped/rotated arbitrarily, + // so the "bottom right" corner of the raw SSDQ might not necessarily be where one expects it to be. + float bottom = drawable.ScreenSpaceDrawQuad.AABBFloat.BottomRight.Y; bool isRelativeX = drawable.RelativeSizeAxes == Axes.X; @@ -319,7 +321,7 @@ namespace osu.Game.Screens.Play // and align bottom-right components with the top-edge of the highest bottom-anchored hud element. else if (drawable.Anchor.HasFlag(Anchor.BottomRight) || (drawable.Anchor.HasFlag(Anchor.y2) && drawable.RelativeSizeAxes == Axes.X)) { - var topLeft = element.ScreenSpaceDrawQuad.TopLeft; + var topLeft = element.ScreenSpaceDrawQuad.AABBFloat.TopLeft; if (highestBottomScreenSpace == null || topLeft.Y < highestBottomScreenSpace.Value.Y) highestBottomScreenSpace = topLeft; } From 233560b8679ce0c69e1b32b41e631ccb7afc359a Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 7 Nov 2024 18:15:54 +0900 Subject: [PATCH 36/57] 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 c44f46d70a..14f0208fc8 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 37/57] 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 ddabf866ff..9687944817 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -178,7 +178,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty { // 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 38/57] 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 b48046d190..677a3b0278 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 @@ namespace osu.Game.Screens.Menu [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 @@ namespace osu.Game.Screens.Menu 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 39/57] 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 0b09613ba0..1fc04aff59 100644 --- a/osu.Desktop/NVAPI.cs +++ b/osu.Desktop/NVAPI.cs @@ -141,12 +141,12 @@ namespace osu.Desktop // 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 @@ namespace osu.Desktop 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 @@ namespace osu.Desktop 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 @@ namespace osu.Desktop 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 @@ namespace osu.Desktop 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 @@ namespace osu.Desktop 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 @@ namespace osu.Desktop }; // 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 @@ namespace osu.Desktop { 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 @@ namespace osu.Desktop 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 @@ namespace osu.Desktop /// 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 40/57] 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 1fc04aff59..fd372cddc5 100644 --- a/osu.Desktop/NVAPI.cs +++ b/osu.Desktop/NVAPI.cs @@ -258,11 +258,9 @@ namespace osu.Desktop 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 @@ namespace osu.Desktop [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 @@ namespace osu.Desktop 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 @@ namespace osu.Desktop 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 41/57] 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 0000000000..74ce226240 --- /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 15fbcf38e4..d494bbcb48 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 42/57] 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 74ce226240..38ee0c45ea 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 43/57] 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 df71767f82..79b417732a 100644 --- a/osu.Game/Screens/Play/Break/BreakInfoLine.cs +++ b/osu.Game/Screens/Play/Break/BreakInfoLine.cs @@ -63,7 +63,7 @@ namespace osu.Game.Screens.Play.Break 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 44/57] 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 38ee0c45ea..e68449ea68 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 d494bbcb48..8b86650c71 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 45/57] 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 e68449ea68..b193d82317 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 46/57] 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 b193d82317..e08534fbff 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 47/57] 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 8b86650c71..4297a88e89 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 48/57] 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 e08534fbff..ac2c1b7e3f 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 49/57] 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 ac2c1b7e3f..4e221d0550 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 f8ac54d61cf5165c2e8cf3eb33d4edf32e6d16f9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 Nov 2024 16:59:02 +0900 Subject: [PATCH 50/57] 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 44f91b4df1..412e44cbde 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -216,7 +216,7 @@ namespace osu.Game.Screens.Select private int visibleSetsCount; - public BeatmapCarousel(FilterCriteria initialCriterial) + public BeatmapCarousel(FilterCriteria initialCriteria) { root = new CarouselRoot(this); InternalChild = new Container @@ -239,7 +239,7 @@ namespace osu.Game.Screens.Select } }; - 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 51/57] 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 412e44cbde..ba20031509 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -222,12 +222,6 @@ namespace osu.Game.Screens.Select 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 @@ namespace osu.Game.Screens.Select 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 52/57] 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 f642d23bb0..af6fd61a3d 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -196,6 +196,7 @@ namespace osu.Game.Configuration 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 @@ namespace osu.Game.Configuration EditorScaleOrigin, EditorRotationOrigin, EditorTimelineShowBreaks, + EditorAdjustExistingObjectsOnTimingChanges, } } diff --git a/osu.Game/Localisation/EditorStrings.cs b/osu.Game/Localisation/EditorStrings.cs index 19b783de92..127bdd8355 100644 --- a/osu.Game/Localisation/EditorStrings.cs +++ b/osu.Game/Localisation/EditorStrings.cs @@ -40,9 +40,9 @@ namespace osu.Game.Localisation 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 178b3f57b0..8de0c7c33d 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -422,9 +422,9 @@ namespace osu.Game.Screens.Edit 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 154ebf3c88..ad31c2ccc3 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -89,8 +89,6 @@ namespace osu.Game.Screens.Edit 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 abcdf7e4ff..13e802a8e4 100644 --- a/osu.Game/Screens/Edit/Timing/GroupSection.cs +++ b/osu.Game/Screens/Edit/Timing/GroupSection.cs @@ -7,6 +7,7 @@ using osu.Framework.Bindables; 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 @@ namespace osu.Game.Screens.Edit.Timing [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 @@ namespace osu.Game.Screens.Edit.Timing 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 91a0a43d62..f105c00726 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.Shapes; 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 @@ namespace osu.Game.Screens.Edit.Timing [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 @@ namespace osu.Game.Screens.Edit.Timing 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 @@ namespace osu.Game.Screens.Edit.Timing 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 e668120d0d..6a89dc1341 100644 --- a/osu.Game/Screens/Edit/Timing/TimingSection.cs +++ b/osu.Game/Screens/Edit/Timing/TimingSection.cs @@ -6,6 +6,7 @@ using osu.Framework.Allocation; 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 @@ namespace osu.Game.Screens.Edit.Timing private LabelledSwitchButton omitBarLine = null!; private BPMTextBox bpmTextEntry = null!; + [Resolved] + private OsuConfigManager configManager { get; set; } = null!; + [BackgroundDependencyLoader] private void load() { @@ -46,7 +50,7 @@ namespace osu.Game.Screens.Edit.Timing 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 53/57] 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 412e44cbde..e8fe191a24 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -1116,11 +1116,6 @@ namespace osu.Game.Screens.Select // 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 755008d370..10921c331e 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs @@ -86,8 +86,6 @@ namespace osu.Game.Screens.Select.Carousel }; } - 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 877db75317..be91aef1a3 100644 --- a/osu.Game/Screens/Select/FilterControl.cs +++ b/osu.Game/Screens/Select/FilterControl.cs @@ -97,8 +97,8 @@ namespace osu.Game.Screens.Select { 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 933df2464a..2d919c3247 100644 --- a/osu.Game/Screens/Select/Footer.cs +++ b/osu.Game/Screens/Select/Footer.cs @@ -12,6 +12,7 @@ using osu.Framework.Graphics; 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 @@ namespace osu.Game.Screens.Select { 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 54/57] 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 8de0c7c33d..644e1afb3b 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -422,10 +422,6 @@ namespace osu.Game.Screens.Edit 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 6a89dc1341..ae1ac02dd6 100644 --- a/osu.Game/Screens/Edit/Timing/TimingSection.cs +++ b/osu.Game/Screens/Edit/Timing/TimingSection.cs @@ -8,6 +8,7 @@ using osu.Framework.Graphics; 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 @@ namespace osu.Game.Screens.Edit.Timing { 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 55/57] 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 be91aef1a3..b221296ba8 100644 --- a/osu.Game/Screens/Select/FilterControl.cs +++ b/osu.Game/Screens/Select/FilterControl.cs @@ -28,7 +28,6 @@ using osu.Game.Rulesets; 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 2d919c3247..1d05f644b7 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 56/57] 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 904f5edf2b..2cdbaddf72 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 7405a7c587..855ba3374b 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 57/57] 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 ba20031509..5716b2c918 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -1280,14 +1280,14 @@ namespace osu.Game.Screens.Select } 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 @@ namespace osu.Game.Screens.Select 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))); } }